feat(auth): add BearerAuth for minimal bearer-token authentication#2336
Draft
feat(auth): add BearerAuth for minimal bearer-token authentication#2336
Conversation
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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
BearerAuth— a lightweighthttpx.Authimplementation with a two-method contract (token()+ optionalon_unauthorized()). Python-SDK counterpart to typescript-sdk#1710.Motivation and Context
OAuthClientProviderassumes 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 unusedOAuthClientProvidermethods or manually setAuthorizationheaders onhttpx.AsyncClient.BearerAuthcovers 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 inOAuthClientProvider.Non-breaking. Both
BearerAuthandOAuthClientProviderarehttpx.Authsubclasses and plug into the sameauthparameter. No adapter or type guard needed —httpx.Authis already the extension point (per #1240 guidance).OAuthClientProvideris unchanged.What changed
src/mcp/client/auth/bearer.py— new module:BearerAuth(httpx.Auth)— accepts a static string, sync callable, or async callable fortoken; optionalon_unauthorizedhandlerUnauthorizedContext— the 401 response (body pre-read) and the rejected requestTokenSource,UnauthorizedHandler— type aliasessrc/mcp/client/auth/exceptions.py:UnauthorizedError— raised when 401 cannot be recoveredTransports:
streamable_http_client()gainsauth=convenience parameter (mutually exclusive withhttp_client=)Clientdataclass gainsauth=field that flows to the transport401 handling: call
on_unauthorized(), retry once, give up. Retry state is naturally per-operation via httpx's generator-per-request pattern — a freshasync_auth_flowgenerator per request means no shared retry counter to reset or leak across concurrent requests. Response body is read withawait response.aread()only on 401 (not every response) so handlers can inspectresponse.texteven underclient.stream().Sync guard:
sync_auth_flowraises a clearRuntimeErrorinstead of silently no-oping withhttpx.Client.Docs:
docs/authorization.md— complete rewrite with bearer-token and OAuth sectionsexamples/snippets/clients/bearer_auth_client.py— env-var token patternHow Has This Been Tested?
21 tests in
tests/client/auth/test_bearer.py:httpx.MockTransport(header on wire, 401→retry→200, streaming-mode body access)Clientandstreamable_http_clientauth-parameter passthroughAll 1163 tests passing, 100% coverage,
strict-no-coverclean, ruff + pyright clean.Breaking Changes
None.
OAuthClientProvideris unchanged. Theauth=parameter is additive.Types of changes
Checklist
Additional context
Out of scope (noted for follow-up):
on_unauthorized()independently (thundering herd). Deduplicating via an in-flight-promise pattern would be a behavior change — same scoping as the TS PR.OAuthClientProvideralso lacks async_auth_flowguard (pre-existing, silently no-ops with sync clients). Could be hoisted to a shared base in a follow-up.AI Disclaimer