modelcontextprotocol / modelcontextprotocol/typescript-sdk
streamable HTTP server: no duplicate-in-flight request-id guard — concurrent POSTs with the same JSON-RPC id cross-wire responses
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 13.4k
- Forks
- 2.2k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 4
Description
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 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.- 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).
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
Read packages/server/src/server/streamableHttp.ts, especially POST registration around ~L801 and ~L858 and the send() routing through _requestToStreamMapping. Trace how a duplicate in-flight id is handled, then ensure it cannot overwrite the existing mapping; done means concurrent duplicate-id POSTs no longer cross-wire or leave responses hanging, with the chosen JSON-RPC error or queue behavior applied consistently.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100