Summary
The streamable-HTTP server transport routes responses to their originating POST via a map keyed by JSON-RPC request id with no duplicate-in-flight guard. Two concurrent POSTs on one session carrying the same request id will cross-wire: the mapping for the first request is silently overwritten, so its response is delivered to (or dropped on) the wrong HTTP response stream.
This is a code-level finding on main, filed as the TypeScript sibling of modelcontextprotocol/python-sdk#3060, where the identical pattern was reproduced end-to-end and observed in production: with 12 concurrent tools/call POSTs all using id: 1 on one session, 1 request received a different request's entire response envelope and 11 hung indefinitely; a unique-id control burst was 12/12 clean. The python issue includes the full reproducer and the production incident details.
Why this matters in practice
Duplicate in-flight ids violate the spec, but a major production client — claude.ai's custom-connector MCP client sends every request with id: 1, and pools one transport session across a user's conversations — so any stateful server with slow tools and concurrent calls will hit this. The failure mode is silent delivery of one request's data to another request, which callers cannot detect without embedding sentinels.
Code path (packages/server/src/server/streamableHttp.ts, main)
- POST handling registers each incoming request as
this._requestToStreamMapping.set(message.id, streamId) (both the JSON-response branch and the SSE branch, ~L801 and ~L858). There is no check whether message.id is already mapped to a live stream — a concurrent duplicate silently overwrites the previous entry.
send() routes a response by requestId = message.id through _requestToStreamMapping to a stream — i.e., to whichever POST registered last.
- The transport does guard stream-id conflicts for GET resumption (
'Conflict: Stream already has an active connection', ~L530), but there is no analogous guard for duplicate request ids on POST.
Suggested fix
On POST, if message.id already exists in _requestToStreamMapping (with a live stream), reject with JSON-RPC -32600 rather than overwriting — or queue the duplicate until the first completes. Either prevents cross-request data delivery while surfacing the client's protocol violation.
The client-side id-reuse behavior is also being reported to Anthropic (claude.ai).
Summary
The streamable-HTTP server transport routes responses to their originating POST via a map keyed by JSON-RPC request id with no duplicate-in-flight guard. Two concurrent POSTs on one session carrying the same request id will cross-wire: the mapping for the first request is silently overwritten, so its response is delivered to (or dropped on) the wrong HTTP response stream.
This is a code-level finding on
main, filed as the TypeScript sibling of modelcontextprotocol/python-sdk#3060, where the identical pattern was reproduced end-to-end and observed in production: with 12 concurrenttools/callPOSTs all usingid: 1on one session, 1 request received a different request's entire response envelope and 11 hung indefinitely; a unique-id control burst was 12/12 clean. The python issue includes the full reproducer and the production incident details.Why this matters in practice
Duplicate in-flight ids violate the spec, but a major production client — claude.ai's custom-connector MCP client sends every request with
id: 1, and pools one transport session across a user's conversations — so any stateful server with slow tools and concurrent calls will hit this. The failure mode is silent delivery of one request's data to another request, which callers cannot detect without embedding sentinels.Code path (
packages/server/src/server/streamableHttp.ts, main)this._requestToStreamMapping.set(message.id, streamId)(both the JSON-response branch and the SSE branch, ~L801 and ~L858). There is no check whethermessage.idis already mapped to a live stream — a concurrent duplicate silently overwrites the previous entry.send()routes a response byrequestId = message.idthrough_requestToStreamMappingto a stream — i.e., to whichever POST registered last.'Conflict: Stream already has an active connection', ~L530), but there is no analogous guard for duplicate request ids on POST.Suggested fix
On POST, if
message.idalready exists in_requestToStreamMapping(with a live stream), reject with JSON-RPC-32600rather than overwriting — or queue the duplicate until the first completes. Either prevents cross-request data delivery while surfacing the client's protocol violation.The client-side id-reuse behavior is also being reported to Anthropic (claude.ai).