Skip to content

Let a token verifier gate the server without AuthSettings - #3292

Draft
maxisbey wants to merge 1 commit into
mainfrom
token-verifier-without-auth-settings
Draft

Let a token verifier gate the server without AuthSettings#3292
maxisbey wants to merge 1 commit into
mainfrom
token-verifier-without-auth-settings

Conversation

@maxisbey

Copy link
Copy Markdown
Contributor

MCPServer(token_verifier=...) no longer requires auth=AuthSettings(...). A verifier on its own is now a plain bearer gate; AuthSettings is 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 mode issuer_url is 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 no auth= was accepted but built an app that 401'd every request, valid token included, because AuthenticationMiddleware(BearerAuthBackend) was installed under if auth: while RequireAuthMiddleware was installed under if token_verifier:. MCPServer refused the same shape with a ValueError, so the two layers disagreed about one state.

What changes:

  • lowlevel.Server.streamable_http_app and MCPServer.sse_app: the authentication backend + auth-context middleware are installed whenever a verifier is present. auth layers on required scopes, the metadata route, and the resource_metadata pointer in the 401. Every previously-valid combination produces the same routes (same order), middleware and responses as before.
  • MCPServer.__init__: a bare token_verifier= is accepted. Still refused: auth= with nothing to gate with, auth_server_provider= without auth= (it needs the issuer), and both a provider and a verifier.
  • docs/run/authorization.md: new "Just a pre-shared token" section with a runnable docs_src example (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:

  • Split the resource-server fields out of 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.
  • Deduplicate the auth wiring between Server.streamable_http_app and MCPServer.sse_app.
  • Change how a TokenVerifier that raises is surfaced (still a 500 from Starlette's AuthenticationMiddleware).
  • At the low level, auth_server_provider without auth is still silently ignored, as before.

How Has This Been Tested?

  • New docs tests for the pre-shared-token example (route list, exact 401 challenge, no metadata route, Client round trip with a static header), an interaction test for the low-level verifier-only shape, two sse_app tests (verifier-only, and verifier + settings), and constructor tests for the refused shapes. Three # pragma: no cover markers on the touched wiring/validation come off as a result; the embedded-AS-over-SSE branch keeps its pre-existing one.
  • Driven end to end under uvicorn with curl and the SDK client over both streamable HTTP and SSE: no/wrong token → 401 without resource_metadata, metadata paths → 404, valid token → initialize 200 and whoami returns the verifier's client_id; non-ASCII and malformed Authorization headers → 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 ValueError now succeeds; everything that worked before behaves identically. Two error messages are reworded (... without auth settings now names only auth_server_provider; ... when auth is enabled... with auth settings).

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

The spec makes authorization OPTIONAL and only SHOULD for HTTP transports, and basic allows 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.

`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.
@github-actions

Copy link
Copy Markdown
Contributor

📚 Documentation preview

Preview https://pr-3292.mcp-python-docs.pages.dev
Deployment https://378427fe.mcp-python-docs.pages.dev
Commit 196b959
Triggered by @maxisbey
Updated 2026-08-11 21:29:06 UTC

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.

Authentication in High Level MCPServer

1 participant