Skip to content

Commit c85501a

Browse files
committed
feat(auth): add BearerAuth for minimal bearer-token authentication
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
1 parent 92c693b commit c85501a

File tree

11 files changed

+827
-10
lines changed

11 files changed

+827
-10
lines changed

docs/authorization.md

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,150 @@
11
# Authorization
22

3-
!!! warning "Under Construction"
3+
MCP HTTP transports authenticate via `httpx.Auth`. The SDK provides two
4+
implementations that plug into the same `auth` parameter:
45

5-
This page is currently being written. Check back soon for complete documentation.
6+
- **`BearerAuth`** — a minimal two-method provider for API keys, gateway-managed
7+
tokens, service accounts, or any scenario where the token comes from an
8+
external pipeline.
9+
- **`OAuthClientProvider`** — full OAuth 2.1 authorization-code flow with PKCE,
10+
Protected Resource Metadata discovery (RFC 9728), dynamic client registration,
11+
and automatic token refresh.
12+
13+
Both are `httpx.Auth` subclasses. Pass either to `Client(url, auth=...)`,
14+
`streamable_http_client(url, auth=...)`, or directly to
15+
`httpx.AsyncClient(auth=...)`.
16+
17+
## Bearer tokens
18+
19+
For a static token (API key, pre-provisioned credential):
20+
21+
```python
22+
from mcp.client import Client
23+
from mcp.client.auth import BearerAuth
24+
25+
async with Client("https://api.example.com/mcp", auth=BearerAuth("my-api-key")) as client:
26+
tools = await client.list_tools()
27+
```
28+
29+
For a dynamic token (environment variable, cache, external service), pass a
30+
callable — sync or async:
31+
32+
```python
33+
import os
34+
from mcp.client.auth import BearerAuth
35+
36+
auth = BearerAuth(lambda: os.environ.get("MCP_TOKEN"))
37+
```
38+
39+
`token()` is called before every request, so the callable can return a freshly
40+
rotated value each time. Keep it fast — return a cached value and refresh in the
41+
background rather than blocking on network calls.
42+
43+
### Handling 401
44+
45+
By default, `BearerAuth` raises `UnauthorizedError` immediately on 401. To
46+
refresh credentials and retry once, pass an `on_unauthorized` handler:
47+
48+
```python
49+
from mcp.client.auth import BearerAuth, UnauthorizedContext
50+
51+
token_cache = TokenCache()
52+
53+
async def refresh(ctx: UnauthorizedContext) -> None:
54+
# ctx.response.headers["WWW-Authenticate"] has scope/resource_metadata hints
55+
await token_cache.invalidate()
56+
57+
auth = BearerAuth(token_cache.get, on_unauthorized=refresh)
58+
```
59+
60+
After `on_unauthorized` returns, `token()` is called again and the request is
61+
retried once. If the retry also gets 401, `UnauthorizedError` is raised. Retry
62+
state is scoped per-request — a failed retry on one request does not block
63+
retries on subsequent requests.
64+
65+
To abort without retrying (for example, when interactive user action is
66+
required), raise from the handler:
67+
68+
```python
69+
async def signal_host(ctx: UnauthorizedContext) -> None:
70+
ui.show_reauth_prompt()
71+
raise UnauthorizedError("User action required before retry")
72+
```
73+
74+
### Subclassing
75+
76+
For more complex providers, subclass `BearerAuth` and override `token()` and
77+
`on_unauthorized()`:
78+
79+
```python
80+
from mcp.client.auth import BearerAuth, UnauthorizedContext
81+
82+
class MyAuth(BearerAuth):
83+
async def token(self) -> str | None:
84+
return await self._store.get_access_token()
85+
86+
async def on_unauthorized(self, context: UnauthorizedContext) -> None:
87+
await self._store.refresh()
88+
```
89+
90+
## OAuth 2.1
91+
92+
For the full OAuth authorization-code flow with PKCE — including Protected
93+
Resource Metadata discovery, authorization server metadata discovery, dynamic
94+
client registration, and automatic token refresh — use `OAuthClientProvider`:
95+
96+
```python
97+
import httpx
98+
from mcp.client.auth import OAuthClientProvider, TokenStorage
99+
from mcp.client.streamable_http import streamable_http_client
100+
from mcp.shared.auth import OAuthClientMetadata
101+
102+
auth = OAuthClientProvider(
103+
server_url="https://api.example.com",
104+
client_metadata=OAuthClientMetadata(
105+
client_name="My MCP Client",
106+
redirect_uris=["http://localhost:3000/callback"],
107+
grant_types=["authorization_code", "refresh_token"],
108+
response_types=["code"],
109+
),
110+
storage=my_token_storage,
111+
redirect_handler=open_browser,
112+
callback_handler=wait_for_callback,
113+
)
114+
115+
async with streamable_http_client("https://api.example.com/mcp", auth=auth) as (read, write):
116+
...
117+
```
118+
119+
See `examples/snippets/clients/oauth_client.py` for a complete working example.
120+
121+
### Non-interactive grants
122+
123+
For machine-to-machine authentication without a browser redirect, use the
124+
extensions in `mcp.client.auth.extensions`:
125+
126+
- `ClientCredentialsOAuthProvider``client_credentials` grant with client ID
127+
and secret
128+
- `PrivateKeyJWTOAuthProvider``client_credentials` with `private_key_jwt`
129+
client authentication (RFC 7523)
130+
131+
## Custom `httpx.Auth`
132+
133+
Any `httpx.Auth` implementation works. To combine authentication with custom
134+
HTTP settings (headers, timeouts, proxies), configure an `httpx.AsyncClient`
135+
directly:
136+
137+
```python
138+
import httpx
139+
from mcp.client.streamable_http import streamable_http_client
140+
141+
http_client = httpx.AsyncClient(
142+
auth=my_auth,
143+
headers={"X-Custom": "value"},
144+
timeout=httpx.Timeout(60.0),
145+
)
146+
147+
async with http_client:
148+
async with streamable_http_client(url, http_client=http_client) as (read, write):
149+
...
150+
```
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Minimal bearer-token authentication example.
2+
3+
Demonstrates the simplest possible MCP client authentication: a bearer token
4+
from an environment variable. `BearerAuth` is an `httpx.Auth` implementation
5+
that calls `token()` before every request and optionally `on_unauthorized()`
6+
on 401 before retrying once.
7+
8+
For full OAuth flows (authorization code, PKCE, dynamic client registration),
9+
see `oauth_client.py` and use `OAuthClientProvider` instead — both plug into
10+
the same `auth` parameter.
11+
12+
Run against any MCP server that accepts bearer tokens:
13+
14+
MCP_TOKEN=your-token MCP_SERVER_URL=http://localhost:8001/mcp uv run bearer-auth-client
15+
"""
16+
17+
import asyncio
18+
import os
19+
20+
from mcp.client import Client
21+
from mcp.client.auth import BearerAuth
22+
23+
24+
async def main() -> None:
25+
server_url = os.environ.get("MCP_SERVER_URL", "http://localhost:8001/mcp")
26+
token = os.environ.get("MCP_TOKEN")
27+
28+
if not token:
29+
raise SystemExit("Set MCP_TOKEN to your bearer token")
30+
31+
# token() is called before every request. With no on_unauthorized handler,
32+
# a 401 raises UnauthorizedError immediately — no retry.
33+
auth = BearerAuth(token)
34+
35+
async with Client(server_url, auth=auth) as client:
36+
tools = await client.list_tools()
37+
print(f"Available tools: {[t.name for t in tools.tools]}")
38+
39+
40+
def run() -> None:
41+
asyncio.run(main())
42+
43+
44+
if __name__ == "__main__":
45+
run()

examples/snippets/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ completion-client = "clients.completion_client:main"
2121
direct-execution-server = "servers.direct_execution:main"
2222
display-utilities-client = "clients.display_utilities:main"
2323
oauth-client = "clients.oauth_client:run"
24+
bearer-auth-client = "clients.bearer_auth_client:run"
2425
elicitation-client = "clients.url_elicitation_client:run"

src/mcp/client/auth/__init__.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,35 @@
1-
"""OAuth2 Authentication implementation for HTTPX.
1+
"""Client-side authentication for MCP HTTP transports.
22
3-
Implements authorization code flow with PKCE and automatic token refresh.
3+
Two `httpx.Auth` implementations are provided:
4+
5+
- `BearerAuth` — minimal two-method provider (`token()` + optional
6+
`on_unauthorized()`) for API keys, gateway-managed tokens, service accounts,
7+
or any scenario where the token comes from an external pipeline.
8+
- `OAuthClientProvider` — full OAuth 2.1 authorization-code flow with PKCE,
9+
Protected Resource Metadata discovery (RFC 9728), dynamic client registration,
10+
and automatic token refresh.
11+
12+
Both are `httpx.Auth` subclasses and plug into the same `auth` parameter.
413
"""
514

6-
from mcp.client.auth.exceptions import OAuthFlowError, OAuthRegistrationError, OAuthTokenError
15+
from mcp.client.auth.bearer import BearerAuth, TokenSource, UnauthorizedContext, UnauthorizedHandler
16+
from mcp.client.auth.exceptions import OAuthFlowError, OAuthRegistrationError, OAuthTokenError, UnauthorizedError
717
from mcp.client.auth.oauth2 import (
818
OAuthClientProvider,
919
PKCEParameters,
1020
TokenStorage,
1121
)
1222

1323
__all__ = [
24+
"BearerAuth",
1425
"OAuthClientProvider",
1526
"OAuthFlowError",
1627
"OAuthRegistrationError",
1728
"OAuthTokenError",
1829
"PKCEParameters",
30+
"TokenSource",
1931
"TokenStorage",
32+
"UnauthorizedContext",
33+
"UnauthorizedError",
34+
"UnauthorizedHandler",
2035
]

0 commit comments

Comments
 (0)