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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions