Skip to content

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

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

Activity

  1. added a commit that references this issue on Mar 9, 2026
    a78316c
  2. namabile commented on Mar 9, 2026

    @namabile
    Author

    Cross-reference: TypeScript SDK client behavior

    For additional context, the TypeScript SDK's client (packages/client/src/client/auth.ts) has a selectClientAuthMethod() function that handles this more gracefully:

    // If server metadata omits token_endpoint_auth_methods_supported, RFC 8414 §2 says the
    // default is client_secret_basic. RFC 6749 §2.3.1 also requires servers to support HTTP
    // Basic authentication for clients with a secret, making it the safest default.
    if (supportedMethods.length === 0) {
        return hasClientSecret ? 'client_secret_basic' : 'none';
    }

    When the metadata is empty, it defaults to "none" for public clients. However, when the metadata explicitly lists only ["client_secret_post", "client_secret_basic"] (as the Python SDK's build_metadata() does), the TS client sees the server doesn't support "none" and fails.

    This confirms the fix should be on the server side (metadata should include "none") rather than only on the client side. The server's registration handler already accepts token_endpoint_auth_method: "none" — the metadata should reflect this.

  3. namabile commented on Mar 9, 2026

    @namabile
    Author

    Official MCP example server uses "none"

    The official modelcontextprotocol/example-remote-server (source) explicitly sets:

    token_endpoint_auth_methods_supported: ['none'],

    This confirms that "none" is the intended auth method for MCP servers accepting public client registrations. The Python SDK's build_metadata() is the outlier here — it hardcodes ["client_secret_post", "client_secret_basic"] while the official example uses ["none"].

    Additional references

    • modelcontextprotocol/modelcontextprotocol#276 — "Private vs Public clients in the Authorization / OAuth 2.1 spec" — discusses how CLI/desktop clients are public clients and client_secret is redundant when PKCE is used.

    • MCP Authorization Spec (2025-06-18) Best Practices: "We strongly recommend that local clients implement OAuth 2.1 as a public client."

    • anthropics/claude-code#26675 — Open issue about Claude Code's OAuth flow for MCP servers (related but distinct — about DCR being required).

    • OpenAI Developer Community — ChatGPT registers with token_endpoint_auth_method: "none" but expects a client_secret — same class of bug, demonstrating this affects multiple MCP clients.

  4. added a commit that references this issue on Mar 9, 2026
    6839133
  5. aminabedi-bentley commented on May 21, 2026

    @aminabedi-bentley

    Hi maintainers,
    Flagging that this issue was closed as completed on 2026-03-09, but the linked fix #2261 is still open and unmerged (created the same day, ~2 months ago, currently REVIEW_REQUIRED).

    I just verified against the latest released mcp==1.27.1 (2026-05-08) and main: build_metadata() in src/mcp/server/auth/routes.py still hardcodes:

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

    with no "none". Downstream, fastmcp==3.3.1 (2026-05-15) calls this build_metadata() directly, so it inherits the same bug, ironically while registering its own proxy DCR clients with token_endpoint_auth_method="none" internally.

    The practical impact is unchanged from the original report: public MCP clients (VS Code, Claude Code, ChatGPT, Cursor) that follow the MCP authorization spec's "implement OAuth 2.1 as a public client" guidance can't complete DCR against a Python-SDK-based server without downstream workarounds (e.g. an ASGI middleware that rewrites the metadata response).

    Could this be reopened, or #2261 prioritized for review? Happy to help test if useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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