|
1 | 1 | # Authorization |
2 | 2 |
|
3 | | -!!! warning "Under Construction" |
| 3 | +MCP supports OAuth 2.1 for securing server-client communication. Servers act as Resource Servers (RS) that validate tokens, while clients obtain tokens from Authorization Servers (AS). |
4 | 4 |
|
5 | | - This page is currently being written. Check back soon for complete documentation. |
| 5 | +## Server-Side Authentication |
| 6 | + |
| 7 | +Authentication can be used by servers that want to expose tools accessing protected resources. |
| 8 | + |
| 9 | +`mcp.server.auth` implements OAuth 2.1 resource server functionality, where MCP servers act as Resource Servers (RS) that validate tokens issued by separate Authorization Servers (AS). This follows the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) and implements RFC 9728 (Protected Resource Metadata) for AS discovery. |
| 10 | + |
| 11 | +MCP servers can use authentication by providing an implementation of the `TokenVerifier` protocol: |
| 12 | + |
| 13 | +<!-- snippet-source examples/snippets/servers/oauth_server.py --> |
| 14 | +```python |
| 15 | +""" |
| 16 | +Run from the repository root: |
| 17 | + uv run examples/snippets/servers/oauth_server.py |
| 18 | +""" |
| 19 | + |
| 20 | +from pydantic import AnyHttpUrl |
| 21 | + |
| 22 | +from mcp.server.auth.provider import AccessToken, TokenVerifier |
| 23 | +from mcp.server.auth.settings import AuthSettings |
| 24 | +from mcp.server.fastmcp import FastMCP |
| 25 | + |
| 26 | + |
| 27 | +class SimpleTokenVerifier(TokenVerifier): |
| 28 | + """Simple token verifier for demonstration.""" |
| 29 | + |
| 30 | + async def verify_token(self, token: str) -> AccessToken | None: |
| 31 | + pass # This is where you would implement actual token validation |
| 32 | + |
| 33 | + |
| 34 | +# Create FastMCP instance as a Resource Server |
| 35 | +mcp = FastMCP( |
| 36 | + "Weather Service", |
| 37 | + json_response=True, |
| 38 | + # Token verifier for authentication |
| 39 | + token_verifier=SimpleTokenVerifier(), |
| 40 | + # Auth settings for RFC 9728 Protected Resource Metadata |
| 41 | + auth=AuthSettings( |
| 42 | + issuer_url=AnyHttpUrl("https://auth.example.com"), # Authorization Server URL |
| 43 | + resource_server_url=AnyHttpUrl("http://localhost:3001"), # This server's URL |
| 44 | + required_scopes=["user"], |
| 45 | + ), |
| 46 | +) |
| 47 | + |
| 48 | + |
| 49 | +@mcp.tool() |
| 50 | +async def get_weather(city: str = "London") -> dict[str, str]: |
| 51 | + """Get weather data for a city""" |
| 52 | + return { |
| 53 | + "city": city, |
| 54 | + "temperature": "22", |
| 55 | + "condition": "Partly cloudy", |
| 56 | + "humidity": "65%", |
| 57 | + } |
| 58 | + |
| 59 | + |
| 60 | +if __name__ == "__main__": |
| 61 | + mcp.run(transport="streamable-http") |
| 62 | +``` |
| 63 | + |
| 64 | +_Full example: [examples/snippets/servers/oauth_server.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/oauth_server.py)_ |
| 65 | +<!-- /snippet-source --> |
| 66 | + |
| 67 | +For a complete example with separate Authorization Server and Resource Server implementations, see [`examples/servers/simple-auth/`](examples/servers/simple-auth/). |
| 68 | + |
| 69 | +**Architecture:** |
| 70 | + |
| 71 | +- **Authorization Server (AS)**: Handles OAuth flows, user authentication, and token issuance |
| 72 | +- **Resource Server (RS)**: Your MCP server that validates tokens and serves protected resources |
| 73 | +- **Client**: Discovers AS through RFC 9728, obtains tokens, and uses them with the MCP server |
| 74 | + |
| 75 | +See [TokenVerifier](src/mcp/server/auth/provider.py) for more details on implementing token validation. |
| 76 | + |
| 77 | +## Client-Side Authentication |
| 78 | + |
| 79 | +The SDK includes [authorization support](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for connecting to protected MCP servers: |
| 80 | + |
| 81 | +<!-- snippet-source examples/snippets/clients/oauth_client.py --> |
| 82 | +```python |
| 83 | +""" |
| 84 | +Before running, specify running MCP RS server URL. |
| 85 | +To spin up RS server locally, see |
| 86 | + examples/servers/simple-auth/README.md |
| 87 | +
|
| 88 | +cd to the `examples/snippets` directory and run: |
| 89 | + uv run oauth-client |
| 90 | +""" |
| 91 | + |
| 92 | +import asyncio |
| 93 | +from urllib.parse import parse_qs, urlparse |
| 94 | + |
| 95 | +import httpx |
| 96 | +from pydantic import AnyUrl |
| 97 | + |
| 98 | +from mcp import ClientSession |
| 99 | +from mcp.client.auth import OAuthClientProvider, TokenStorage |
| 100 | +from mcp.client.streamable_http import streamable_http_client |
| 101 | +from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken |
| 102 | + |
| 103 | + |
| 104 | +class InMemoryTokenStorage(TokenStorage): |
| 105 | + """Demo In-memory token storage implementation.""" |
| 106 | + |
| 107 | + def __init__(self): |
| 108 | + self.tokens: OAuthToken | None = None |
| 109 | + self.client_info: OAuthClientInformationFull | None = None |
| 110 | + |
| 111 | + async def get_tokens(self) -> OAuthToken | None: |
| 112 | + """Get stored tokens.""" |
| 113 | + return self.tokens |
| 114 | + |
| 115 | + async def set_tokens(self, tokens: OAuthToken) -> None: |
| 116 | + """Store tokens.""" |
| 117 | + self.tokens = tokens |
| 118 | + |
| 119 | + async def get_client_info(self) -> OAuthClientInformationFull | None: |
| 120 | + """Get stored client information.""" |
| 121 | + return self.client_info |
| 122 | + |
| 123 | + async def set_client_info(self, client_info: OAuthClientInformationFull) -> None: |
| 124 | + """Store client information.""" |
| 125 | + self.client_info = client_info |
| 126 | + |
| 127 | + |
| 128 | +async def handle_redirect(auth_url: str) -> None: |
| 129 | + print(f"Visit: {auth_url}") |
| 130 | + |
| 131 | + |
| 132 | +async def handle_callback() -> tuple[str, str | None]: |
| 133 | + callback_url = input("Paste callback URL: ") |
| 134 | + params = parse_qs(urlparse(callback_url).query) |
| 135 | + return params["code"][0], params.get("state", [None])[0] |
| 136 | + |
| 137 | + |
| 138 | +async def main(): |
| 139 | + """Run the OAuth client example.""" |
| 140 | + oauth_auth = OAuthClientProvider( |
| 141 | + server_url="http://localhost:8001", |
| 142 | + client_metadata=OAuthClientMetadata( |
| 143 | + client_name="Example MCP Client", |
| 144 | + redirect_uris=[AnyUrl("http://localhost:3000/callback")], |
| 145 | + grant_types=["authorization_code", "refresh_token"], |
| 146 | + response_types=["code"], |
| 147 | + scope="user", |
| 148 | + ), |
| 149 | + storage=InMemoryTokenStorage(), |
| 150 | + redirect_handler=handle_redirect, |
| 151 | + callback_handler=handle_callback, |
| 152 | + ) |
| 153 | + |
| 154 | + async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client: |
| 155 | + async with streamable_http_client("http://localhost:8001/mcp", http_client=custom_client) as (read, write, _): |
| 156 | + async with ClientSession(read, write) as session: |
| 157 | + await session.initialize() |
| 158 | + |
| 159 | + tools = await session.list_tools() |
| 160 | + print(f"Available tools: {[tool.name for tool in tools.tools]}") |
| 161 | + |
| 162 | + resources = await session.list_resources() |
| 163 | + print(f"Available resources: {[r.uri for r in resources.resources]}") |
| 164 | + |
| 165 | + |
| 166 | +def run(): |
| 167 | + asyncio.run(main()) |
| 168 | + |
| 169 | + |
| 170 | +if __name__ == "__main__": |
| 171 | + run() |
| 172 | +``` |
| 173 | + |
| 174 | +_Full example: [examples/snippets/clients/oauth_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/oauth_client.py)_ |
| 175 | +<!-- /snippet-source --> |
| 176 | + |
| 177 | +For a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/). |
0 commit comments