Adds BearerAuth, a lightweight httpx.Auth implementation with a two-method
contract (token() + optional on_unauthorized()). This covers the many deployments
that don't fit the OAuth authorization-code flow: gateway/proxy patterns, service
accounts with pre-provisioned tokens, enterprise SSO where tokens come from a
separate pipeline.
For simple cases, it's a one-liner:
auth = BearerAuth("my-api-key")
async with Client(url, auth=auth) as client: ...
For token rotation, pass a callable (sync or async):
auth = BearerAuth(lambda: os.environ.get("MCP_TOKEN"))
For custom 401 handling, pass or override on_unauthorized(). The handler receives
the 401 response (body pre-read, WWW-Authenticate available), refreshes
credentials, and the request retries once. Retry state is naturally per-operation
via httpx's generator-per-request pattern — no shared counter to reset or leak.
OAuthClientProvider is unchanged. Both are httpx.Auth subclasses and plug into
the same auth parameter — no adapter or type guard needed.
Also adds:
- auth= convenience parameter on streamable_http_client() and Client (mutually
exclusive with http_client=, raises ValueError if both given)
- UnauthorizedError exception for unrecoverable 401s
- sync_auth_flow override that raises a clear error instead of silently no-oping
- docs/authorization.md with bearer-token and OAuth sections
- examples/snippets/clients/bearer_auth_client.py
- 21 tests covering generator-driven unit tests and httpx wire-level integration
4.7 KiB
Authorization
MCP HTTP transports authenticate via httpx.Auth. The SDK provides two
implementations that plug into the same auth parameter:
BearerAuth— a minimal two-method provider for API keys, gateway-managed tokens, service accounts, or any scenario where the token comes from an external pipeline.OAuthClientProvider— full OAuth 2.1 authorization-code flow with PKCE, Protected Resource Metadata discovery (RFC 9728), dynamic client registration, and automatic token refresh.
Both are httpx.Auth subclasses. Pass either to Client(url, auth=...),
streamable_http_client(url, auth=...), or directly to
httpx.AsyncClient(auth=...).
Bearer tokens
For a static token (API key, pre-provisioned credential):
from mcp.client import Client
from mcp.client.auth import BearerAuth
async with Client("https://api.example.com/mcp", auth=BearerAuth("my-api-key")) as client:
tools = await client.list_tools()
For a dynamic token (environment variable, cache, external service), pass a callable — sync or async:
import os
from mcp.client.auth import BearerAuth
auth = BearerAuth(lambda: os.environ.get("MCP_TOKEN"))
token() is called before every request, so the callable can return a freshly
rotated value each time. Keep it fast — return a cached value and refresh in the
background rather than blocking on network calls.
Handling 401
By default, BearerAuth raises UnauthorizedError immediately on 401. To
refresh credentials and retry once, pass an on_unauthorized handler:
from mcp.client.auth import BearerAuth, UnauthorizedContext
token_cache = TokenCache()
async def refresh(ctx: UnauthorizedContext) -> None:
# ctx.response.headers["WWW-Authenticate"] has scope/resource_metadata hints
await token_cache.invalidate()
auth = BearerAuth(token_cache.get, on_unauthorized=refresh)
After on_unauthorized returns, token() is called again and the request is
retried once. If the retry also gets 401, UnauthorizedError is raised. Retry
state is scoped per-request — a failed retry on one request does not block
retries on subsequent requests.
To abort without retrying (for example, when interactive user action is required), raise from the handler:
async def signal_host(ctx: UnauthorizedContext) -> None:
ui.show_reauth_prompt()
raise UnauthorizedError("User action required before retry")
Subclassing
For more complex providers, subclass BearerAuth and override token() and
on_unauthorized():
from mcp.client.auth import BearerAuth, UnauthorizedContext
class MyAuth(BearerAuth):
async def token(self) -> str | None:
return await self._store.get_access_token()
async def on_unauthorized(self, context: UnauthorizedContext) -> None:
await self._store.refresh()
OAuth 2.1
For the full OAuth authorization-code flow with PKCE — including Protected
Resource Metadata discovery, authorization server metadata discovery, dynamic
client registration, and automatic token refresh — use OAuthClientProvider:
import httpx
from mcp.client.auth import OAuthClientProvider, TokenStorage
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientMetadata
auth = OAuthClientProvider(
server_url="https://api.example.com",
client_metadata=OAuthClientMetadata(
client_name="My MCP Client",
redirect_uris=["http://localhost:3000/callback"],
grant_types=["authorization_code", "refresh_token"],
response_types=["code"],
),
storage=my_token_storage,
redirect_handler=open_browser,
callback_handler=wait_for_callback,
)
async with streamable_http_client("https://api.example.com/mcp", auth=auth) as (read, write):
...
See examples/snippets/clients/oauth_client.py for a complete working example.
Non-interactive grants
For machine-to-machine authentication without a browser redirect, use the
extensions in mcp.client.auth.extensions:
ClientCredentialsOAuthProvider—client_credentialsgrant with client ID and secretPrivateKeyJWTOAuthProvider—client_credentialswithprivate_key_jwtclient authentication (RFC 7523)
Custom httpx.Auth
Any httpx.Auth implementation works. To combine authentication with custom
HTTP settings (headers, timeouts, proxies), configure an httpx.AsyncClient
directly:
import httpx
from mcp.client.streamable_http import streamable_http_client
http_client = httpx.AsyncClient(
auth=my_auth,
headers={"X-Custom": "value"},
timeout=httpx.Timeout(60.0),
)
async with http_client:
async with streamable_http_client(url, http_client=http_client) as (read, write):
...