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
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
"""Minimal bearer-token authentication example.
|
||||
|
||||
Demonstrates the simplest possible MCP client authentication: a bearer token
|
||||
from an environment variable. `BearerAuth` is an `httpx.Auth` implementation
|
||||
that calls `token()` before every request and optionally `on_unauthorized()`
|
||||
on 401 before retrying once.
|
||||
|
||||
For full OAuth flows (authorization code, PKCE, dynamic client registration),
|
||||
see `oauth_client.py` and use `OAuthClientProvider` instead — both plug into
|
||||
the same `auth` parameter.
|
||||
|
||||
Run against any MCP server that accepts bearer tokens:
|
||||
|
||||
MCP_TOKEN=your-token MCP_SERVER_URL=http://localhost:8001/mcp uv run bearer-auth-client
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from mcp.client import Client
|
||||
from mcp.client.auth import BearerAuth
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
server_url = os.environ.get("MCP_SERVER_URL", "http://localhost:8001/mcp")
|
||||
token = os.environ.get("MCP_TOKEN")
|
||||
|
||||
if not token:
|
||||
raise SystemExit("Set MCP_TOKEN to your bearer token")
|
||||
|
||||
# token() is called before every request. With no on_unauthorized handler,
|
||||
# a 401 raises UnauthorizedError immediately — no retry.
|
||||
auth = BearerAuth(token)
|
||||
|
||||
async with Client(server_url, auth=auth) as client:
|
||||
tools = await client.list_tools()
|
||||
print(f"Available tools: {[t.name for t in tools.tools]}")
|
||||
|
||||
|
||||
def run() -> None:
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -21,4 +21,5 @@ completion-client = "clients.completion_client:main"
|
||||
direct-execution-server = "servers.direct_execution:main"
|
||||
display-utilities-client = "clients.display_utilities:main"
|
||||
oauth-client = "clients.oauth_client:run"
|
||||
bearer-auth-client = "clients.bearer_auth_client:run"
|
||||
elicitation-client = "clients.url_elicitation_client:run"
|
||||
|
||||
Reference in New Issue
Block a user