Skip to content

Server OAuth metadata hardcodes token_endpoint_auth_methods_supported, breaking public client flows #2260

@namabile

Description

@namabile

Initial Checks

Description

build_metadata() in mcp/server/auth/routes.py hardcodes token_endpoint_auth_methods_supported to ["client_secret_post", "client_secret_basic"]:

https://github.com/modelcontextprotocol/python-sdk/blob/main/src/mcp/server/auth/routes.py#L168

This omits "none", which is a valid value defined in RFC 7591 Section 2 for public clients:

The client is a public client as defined in OAuth 2.0, Section 2.1, and does not have a client secret.

Spec Analysis

The MCP authorization spec (2025-06-18) requires support for public clients:

Authorization servers MUST implement OAuth 2.1 with appropriate security measures for both confidential and public clients.

And explicitly recommends dynamic client registration:

MCP clients and authorization servers SHOULD support the OAuth 2.0 Dynamic Client Registration Protocol (RFC7591) to allow MCP clients to obtain OAuth client IDs without user interaction.

The best practices section further recommends:

We strongly recommend that local clients implement OAuth 2.1 as a public client: 1. Utilizing code challenges (PKCE) for authorization requests...

OAuth 2.1 (draft-ietf-oauth-v2-1-13, Section 2.1) defines public clients:

Clients without credentials are called "public clients."

RFC 7591 (Section 2) defines token_endpoint_auth_method: "none" as the way to register public clients:

"none": The client is a public client as defined in OAuth 2.0, Section 2.1, and does not have a client secret.

RFC 7591 (Section 3.2.1) makes client_secret optional in registration responses, allowing it to be omitted for public clients.

RFC 8414 (Section 2) defines token_endpoint_auth_methods_supported as using values from token_endpoint_auth_method in RFC 7591 — which explicitly includes "none".

The Problem

The SDK's registration handler (register.py:54-60) already correctly supports public clients:

if client_metadata.token_endpoint_auth_method != "none":
    client_secret = secrets.token_hex(32)

But build_metadata() doesn't advertise "none" as a supported method. This creates a contradictory situation: the server accepts public client registrations but tells clients it doesn't support them.

Impact: Real-World Breakage

MCP clients (Claude Code, Cursor, etc.) that follow the spec:

  1. Discover the authorization server metadata via /.well-known/oauth-authorization-server
  2. See only ["client_secret_post", "client_secret_basic"] supported
  3. Register a dynamic client requesting token_endpoint_auth_method: "none" (public client, per MCP best practices)
  4. Registration succeeds — no client_secret returned (correct per RFC 7591)
  5. Token exchange fails — the client has no client_secret to send, but the metadata says one is required

Claude Code shows: "Existing OAuth client information is required when exchanging an authorization code"

The browser OAuth flow completes successfully, the authorization code is received, but the token exchange never happens because the client cannot reconcile the metadata (secrets required) with the registration response (no secret issued).

Suggested Fix

Include "none" in the default token_endpoint_auth_methods_supported list, since the registration handler already supports it:

token_endpoint_auth_methods_supported=["client_secret_post", "client_secret_basic", "none"],

The same change should apply to revocation_endpoint_auth_methods_supported for consistency.

A PR is available: #2261

Related Issues

Python & MCP Python SDK

Python: 3.13
MCP SDK: 1.26.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions