modelcontextprotocol / modelcontextprotocol/typescript-sdk

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

Open Beginner friendly
#1,946 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

auth bug needs repro
Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 15h
Merged PRs (30d)
4

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/client/streamableHttp.ts at the 401 handler in StreamableHTTPClientTransport.send, comparing the current behavior with the fallback described in the issue and related PR #1045. Reproduce with a non-Bearer challenge and a path-suffixed well-known URI; the work is done when discovery falls back to those URIs and the OAuth flow can proceed.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, authentication
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.