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)
Nobody has claimed this yet.
- 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:
-
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"]} -
Connect to it from MCP Inspector (uses TS SDK 1.29.0):
npx @modelcontextprotocol/inspector -
Enter the server URL and pick
streamable-httptransport. Click Connect. -
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:
- If a
Bearerchallenge is present → use itsresource_metadataURL. - 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 inStreamableHTTPClientTransport.send(compiled atdist/esm/client/streamableHttp.js:364). - Downgrading to
@modelcontextprotocol/sdk@1.17.5makes 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/sdkto1.17.5via npmoverridesin 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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