Skip to content

feat(auth): add BearerAuth for minimal bearer-token authentication#2336

Draft
maxisbey wants to merge 1 commit intomainfrom
feat/bearer-auth-provider
Draft

feat(auth): add BearerAuth for minimal bearer-token authentication#2336
maxisbey wants to merge 1 commit intomainfrom
feat/bearer-auth-provider

Conversation

@maxisbey
Copy link
Contributor

Adds BearerAuth — a lightweight httpx.Auth implementation with a two-method contract (token() + optional on_unauthorized()). Python-SDK counterpart to typescript-sdk#1710.

from mcp.client import Client
from mcp.client.auth import BearerAuth

async with Client(url, auth=BearerAuth("my-api-key")) as client:
    tools = await client.list_tools()

Motivation and Context

OAuthClientProvider assumes an interactive browser-redirect flow. Many deployments don't fit: gateway/proxy patterns, service accounts with pre-provisioned tokens, enterprise SSO where tokens come from a separate pipeline. Today those users either stub out unused OAuthClientProvider methods or manually set Authorization headers on httpx.AsyncClient.

BearerAuth covers the transport's actual needs with two methods: "give me a token" and "the token was rejected, do something." Everything OAuth-specific (discovery, PKCE, registration, refresh) stays in OAuthClientProvider.

Non-breaking. Both BearerAuth and OAuthClientProvider are httpx.Auth subclasses and plug into the same auth parameter. No adapter or type guard needed — httpx.Auth is already the extension point (per #1240 guidance). OAuthClientProvider is unchanged.

What changed

src/mcp/client/auth/bearer.py — new module:

  • BearerAuth(httpx.Auth) — accepts a static string, sync callable, or async callable for token; optional on_unauthorized handler
  • UnauthorizedContext — the 401 response (body pre-read) and the rejected request
  • TokenSource, UnauthorizedHandler — type aliases

src/mcp/client/auth/exceptions.py:

  • UnauthorizedError — raised when 401 cannot be recovered

Transports:

  • streamable_http_client() gains auth= convenience parameter (mutually exclusive with http_client=)
  • Client dataclass gains auth= field that flows to the transport

401 handling: call on_unauthorized(), retry once, give up. Retry state is naturally per-operation via httpx's generator-per-request pattern — a fresh async_auth_flow generator per request means no shared retry counter to reset or leak across concurrent requests. Response body is read with await response.aread() only on 401 (not every response) so handlers can inspect response.text even under client.stream().

Sync guard: sync_auth_flow raises a clear RuntimeError instead of silently no-oping with httpx.Client.

Docs:

  • docs/authorization.md — complete rewrite with bearer-token and OAuth sections
  • examples/snippets/clients/bearer_auth_client.py — env-var token pattern

How Has This Been Tested?

21 tests in tests/client/auth/test_bearer.py:

  • Generator-driven unit tests (token resolution, 401 retry, handler exception propagation, per-operation retry isolation, stale-header clearing)
  • Wire-level integration via httpx.MockTransport (header on wire, 401→retry→200, streaming-mode body access)
  • Client and streamable_http_client auth-parameter passthrough

All 1163 tests passing, 100% coverage, strict-no-cover clean, ruff + pyright clean.

Breaking Changes

None. OAuthClientProvider is unchanged. The auth= parameter is additive.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Out of scope (noted for follow-up):

  • Concurrent 401s each call on_unauthorized() independently (thundering herd). Deduplicating via an in-flight-promise pattern would be a behavior change — same scoping as the TS PR.
  • OAuthClientProvider also lacks a sync_auth_flow guard (pre-existing, silently no-ops with sync clients). Could be hoisted to a shared base in a follow-up.

AI Disclaimer

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant