Skip to content

OAuth discovery does not fall back to well-known URI when 401 carries a non-Bearer WWW-Authenticate (e.g. Negotiate) #1946

@caravin

Description

@caravin

Describe the bug

StreamableHTTPClientTransport (TypeScript SDK 1.29.0) does not fall back to well-known protected-resource metadata discovery when a 401 response carries a non-Bearer WWW-Authenticate challenge such as Negotiate. The 401 is bubbled to the caller and the OAuth flow never starts, even though valid metadata is hosted at the spec-defined well-known URI.

The MCP authorization spec (Protected Resource Metadata Discovery Requirements) states:

MCP clients MUST support both discovery mechanisms and use the resource metadata URL from the parsed WWW-Authenticate headers when present; otherwise, they MUST fall back to constructing and requesting the well-known URIs in the order listed above.

"Otherwise" should include the case where WWW-Authenticate is present but does not advertise Bearer ... resource_metadata=.... Today the fallback only fires when the header is missing entirely (per PR #1045 / SEP-985).

To Reproduce

Steps to reproduce the behavior:

  1. Stand up an MCP server that returns 401 with a non-Bearer challenge for unauthenticated requests, and hosts valid protected-resource metadata at the path-suffixed well-known URI:

    $ curl -i -X POST https://server/api/v1/help_python_skill/mcp \
        -H 'Content-Type: application/json' \
        -H 'Accept: application/json, text/event-stream' \
        -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
    HTTP/1.1 401 Unauthorized
    www-authenticate: Negotiate
    content-type: text/plain
    
    Authentication required.
    
    $ curl -i 'https://server/.well-known/oauth-protected-resource/api/v1/help_python_skill/mcp'
    HTTP/1.1 200 OK
    content-type: application/json
    
    {"resource":"https://server/api/v1/help_python_skill/mcp",
     "authorization_servers":["https://server"],
     "scopes_supported":[".default"],
     "bearer_methods_supported":["header"]}
    
  2. Connect to it from MCP Inspector (uses TS SDK 1.29.0):

    npx @modelcontextprotocol/inspector
    
  3. Enter the server URL and pick streamable-http transport. Click Connect.

  4. Observe the connection fails with a bare 401; no request is ever made to either well-known URL.

Expected behavior
On any 401 from the MCP endpoint, after parsing WWW-Authenticate:

  1. If a Bearer challenge is present → use its resource_metadata URL.
  2. Otherwise (header missing OR present with a non-Bearer scheme) → fall back to:
    • <origin>/.well-known/oauth-protected-resource<resource_path>
    • then <origin>/.well-known/oauth-protected-resource

Step 2 currently only fires when the header is missing entirely; it should also fire when the header advertises a non-Bearer scheme.

Logs
Inspector logs from a failing connection attempt:

[MCP] New StreamableHttp connection request
[MCP] Query parameters: {"url":"https://server/api/v1/help_python_skill/mcp","transportType":"streamable-http"}
[MCP] Created StreamableHttp client transport
[MCP] Client <-> Proxy  sessionId: 86af1624-e651-42ae-90a3-1fb0dade5bc5
[MCP] Error from MCP server: StreamableHTTPError: Streamable HTTP error: Error POSTing to endpoint: Authentication required.
[MCP]     at StreamableHTTPClientTransport.send (.../@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js:364:23)
[MCP]     at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
[MCP]   code: 401
[MCP] }

Additional context

  • Affected file: src/client/streamableHttp.ts — the 401 handler in StreamableHTTPClientTransport.send (compiled at dist/esm/client/streamableHttp.js:364).
  • Downgrading to @modelcontextprotocol/sdk@1.17.5 makes the connection work — discovery proceeds and OAuth completes — confirming this is a regression introduced in a later version's stricter handling of the 401 response.
  • Workaround: pin @modelcontextprotocol/sdk to 1.17.5 via npm overrides in the consuming package.
  • Real-world impact: any MCP server that (a) supports OAuth via the well-known URI mechanism and (b) sits behind middleware that emits a non-Bearer challenge (Kerberos/SPNEGO is common in enterprise environments) cannot be connected to from any client built on this SDK — including MCP Inspector.
  • Environment:
    • @modelcontextprotocol/sdk: 1.29.0 (regression), 1.17.5 (works)
    • @modelcontextprotocol/inspector: 0.16.6 and 0.21.1 (both reproduce — same underlying SDK)
    • Node: v20.14.0
  • Related: PR #1045 (SEP-985 fallback — handles only missing-header case), Issue #822, Issue #758.

Metadata

Metadata

Assignees

No one assigned

    Labels

    authIssues and PRs related to Authentication / OAuthbugSomething isn't workingneeds reproneeds additional information to be able to reproduce bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions