Let a token verifier gate the server without AuthSettings - #3292
Draft
maxisbey wants to merge 1 commit into
Draft
Conversation
`MCPServer(token_verifier=...)` no longer needs `auth=AuthSettings(...)`. On its own a verifier is now a plain bearer gate: requests without a token it accepts get a 401 whose `WWW-Authenticate` carries no `resource_metadata`, no protected-resource metadata route is published, and `get_access_token()` works as before. `AuthSettings` keeps its job of describing that gate to OAuth clients (required scopes, RFC 9728 metadata, the discovery pointer in the 401), so it is what you add when a real authorization server issues the tokens. Previously the constructor refused a verifier without settings, which forced anyone with a pre-shared token to invent an issuer URL, and the low-level `Server.streamable_http_app(token_verifier=...)` accepted the same shape but answered every request 401, valid token included, because the authentication backend was only installed when settings were given. Both wiring sites (and `MCPServer.sse_app`) now install the backend whenever a verifier is present. The authorization docs gain a "Just a pre-shared token" section with a runnable example, and the constructor still refuses the two shapes that cannot work: settings with nothing to gate with, and an embedded authorization-server provider without settings for its issuer.
Contributor
📚 Documentation preview
|
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.
MCPServer(token_verifier=...)no longer requiresauth=AuthSettings(...). A verifier on its own is now a plain bearer gate;AuthSettingsis what you add to describe that gate to OAuth clients.Motivation and Context
Closes #3283 (see also #431, #702).
Someone with a pre-shared token and no authorization server anywhere had to write
AuthSettings(issuer_url=<something made up>, resource_server_url=...)just to get past the constructor, and the made-up issuer then got advertised in the RFC 9728 metadata document, which sends OAuth-capable clients off to discover an AS that doesn't exist. In resource-server-only modeissuer_urlis never contacted; it's only echoed into that document. The TypeScript and Go SDKs both treat "verifier, metadata optional" as the primitive (requireBearerAuth({verifier}),RequireBearerToken(verifier, nil)); this brings the Python high-level API in line.There was also a low-level inconsistency behind it:
Server.streamable_http_app(token_verifier=V)with noauth=was accepted but built an app that 401'd every request, valid token included, becauseAuthenticationMiddleware(BearerAuthBackend)was installed underif auth:whileRequireAuthMiddlewarewas installed underif token_verifier:.MCPServerrefused the same shape with aValueError, so the two layers disagreed about one state.What changes:
lowlevel.Server.streamable_http_appandMCPServer.sse_app: the authentication backend + auth-context middleware are installed whenever a verifier is present.authlayers on required scopes, the metadata route, and theresource_metadatapointer in the 401. Every previously-valid combination produces the same routes (same order), middleware and responses as before.MCPServer.__init__: a baretoken_verifier=is accepted. Still refused:auth=with nothing to gate with,auth_server_provider=withoutauth=(it needs the issuer), and both a provider and a verifier.docs/run/authorization.md: new "Just a pre-shared token" section with a runnabledocs_srcexample (constant-time compare, token from the environment, fails closed when unset), and the "always travel together" wording is corrected.What this deliberately doesn't do:
AuthSettings(it still mixes embedded-AS config with RS discovery config). That's the principled follow-up and touches shipped 2.x surface; this change is a strict subset of it.Server.streamable_http_appandMCPServer.sse_app.TokenVerifierthat raises is surfaced (still a 500 from Starlette'sAuthenticationMiddleware).auth_server_providerwithoutauthis still silently ignored, as before.How Has This Been Tested?
Clientround trip with a static header), an interaction test for the low-level verifier-only shape, twosse_apptests (verifier-only, and verifier + settings), and constructor tests for the refused shapes. Three# pragma: no covermarkers on the touched wiring/validation come off as a result; the embedded-AS-over-SSE branch keeps its pre-existing one.resource_metadata, metadata paths → 404, valid token →initialize200 andwhoamireturns the verifier'sclient_id; non-ASCII and malformedAuthorizationheaders → 401../scripts/test(100% coverage, strict-no-cover), ruff, pyright, and the strict docs build pass locally.Breaking Changes
None. A constructor call that used to raise
ValueErrornow succeeds; everything that worked before behaves identically. Two error messages are reworded (... without auth settingsnow names onlyauth_server_provider;... when auth is enabled→... with auth settings).Types of changes
Checklist
Additional context
The spec makes authorization OPTIONAL and only SHOULD for HTTP transports, and
basicallows custom authentication strategies, so a pre-shared bearer with no metadata is outside the OAuth profile rather than in violation of it. The docs section says so in practical terms: with nothing to discover, the client has to arrive already holding the token.