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:
Max Isbey
2026-03-24 13:46:08 +00:00
parent 92c693bb73
commit c85501ac65
11 changed files with 827 additions and 10 deletions
@@ -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()
+1
View File
@@ -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"