Repository navigation
Conversation
_commonHeaders() in the Streamable HTTP and SSE clients spread the transport headers and requestInit.headers into one plain object. Keys that differed only in case (mcp-protocol-version vs MCP-Protocol-Version) both survived, and the Headers constructor joined them into "2025-11-25, 2025-03-26", which the server rejects with 400. Build a Headers from the transport values and set() the caller's headers on top. Header names now compare case-insensitively, and the caller's value still wins, as it already did with a lowercase name. Fixes modelcontextprotocol#2969
🦋 Changeset detectedLatest commit: 7009366 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
|
Checked the merge in plain Node (Headers semantics, same as the runtime this runs in): One behaviour difference I could reproduce: when new Headers([['x-a','1'],['X-A','2']]) // x-a: '1, 2'
const m = new Headers({'mcp-protocol-version':'2025-11-25'});
new Headers([['MCP-Protocol-Version','a'],['mcp-protocol-version','b']]).forEach((v,k)=>m.set(k,v));
// mcp-protocol-version: 'a, b'So a caller who passes duplicates in tuple form now sends a comma-joined value, which for |
|
Good catch @arhancanli, thank you. |
|
Read 0ca1f0b: routing both through One thing I could not tell from the diff: does |
|
@arhancanli Yes, the former: |
|
Read 7009366: both tests append |
Fixes #2969
The 400 comes from the client header merge, not the server.
_commonHeaders()insrc/client/streamableHttp.tsandsrc/client/sse.tsspreads the transport headers andrequestInit.headersinto one plain object, somcp-protocol-versionand a caller'sMCP-Protocol-Versionboth survive and theHeadersconstructor joins them.Before: with
requestInit.headers: { 'MCP-Protocol-Version': '2025-03-26' }and 2025-11-25 negotiated, the client sendsmcp-protocol-version: 2025-11-25, 2025-03-26and the server answers 400.After: it sends
2025-03-26, the same as a lowercasemcp-protocol-versionalready did.The fix builds a
Headersfrom the transport values andset()s the caller's headers on top, so names compare case-insensitively. I kept the v1.x precedence where the caller's value wins. main (#2475) puts transport headers last instead, but that also changes whichAuthorizationgets sent, which seemed too much for a v1.x patch. With this in, #2975's server-side change isn't needed for SDK clients.Tested: a new e2e requirement,
typescript:client-transport:http:custom-header-case, connects a real client to the SDK server with a mixed-caseMCP-Protocol-VersioninrequestInit. On v1.x it fails with the same 400 as the issue; with the change it passes. The SSE client gets a unit test for the same merge.npm test(1859),npm run test:e2e(1136),npm run typecheck,npm run lintandnpm run buildpass.