Describe the bug
If two POSTs are concurrently in flight on the same Mcp-Session-Id with the same JSON-RPC request ID, the server delivers the first request's response to the second POST, while the first hangs until client timeout — its tool having executed. The second request is never executed and gets no error; the misrouted response carries the ID it was waiting on, so it is accepted silently.
Reusing an in-flight ID is a client-side spec violation, but the server reacts by corrupting the compliant request rather than rejecting the offending one. Observed in practice with multiple client processes sharing one session (which the SDK permits): 6–25% of parallel tools/call failed silently, 0% sequentially.
To Reproduce
- POST a slow
tools/call with request ID 1.
- While it is in flight, POST another
tools/call on the same session, also with ID 1.
- The slow tool's response is written to the second POST; the first POST hangs in
hangResponse.
Expected behavior
The second POST is rejected (e.g. HTTP 400 / JSON-RPC -32600) and the first request completes unaffected — matching the stdio transport, which already rejects in-flight duplicates (ioConn.addBatch).
Mechanism (at main, 88c58c3)
servePOST registers c.requestStreams[reqID] = stream.id with no duplicate check (mcp/streamable.go:1586), so the second POST overwrites the first request's response routing, and the response later routes by bare ID to the last-registered stream (:1697-1707). jsonrpc2 does detect the duplicate but zeroes req.ID, so it is handled as a notification and no error response is written (internal/jsonrpc2/conn.go:556). Sequential ID reuse is unaffected — entries are deleted when responses are delivered — so this requires two simultaneously in-flight requests.
Versions
Reproduced on v1.6.0 (f5f2015) and main (88c58c3); go version go1.25.0 linux/arm64.
I have a minimal fix (atomic check-and-register in servePOST, rejecting duplicates before any response data is written) with deterministic regression tests, and can open a PR if rejecting server-side is the agreed direction.
Describe the bug
If two POSTs are concurrently in flight on the same
Mcp-Session-Idwith the same JSON-RPC request ID, the server delivers the first request's response to the second POST, while the first hangs until client timeout — its tool having executed. The second request is never executed and gets no error; the misrouted response carries the ID it was waiting on, so it is accepted silently.Reusing an in-flight ID is a client-side spec violation, but the server reacts by corrupting the compliant request rather than rejecting the offending one. Observed in practice with multiple client processes sharing one session (which the SDK permits): 6–25% of parallel
tools/callfailed silently, 0% sequentially.To Reproduce
tools/callwith request ID 1.tools/callon the same session, also with ID 1.hangResponse.Expected behavior
The second POST is rejected (e.g. HTTP 400 / JSON-RPC
-32600) and the first request completes unaffected — matching the stdio transport, which already rejects in-flight duplicates (ioConn.addBatch).Mechanism (at main, 88c58c3)
servePOSTregistersc.requestStreams[reqID] = stream.idwith no duplicate check (mcp/streamable.go:1586), so the second POST overwrites the first request's response routing, and the response later routes by bare ID to the last-registered stream (:1697-1707). jsonrpc2 does detect the duplicate but zeroesreq.ID, so it is handled as a notification and no error response is written (internal/jsonrpc2/conn.go:556). Sequential ID reuse is unaffected — entries are deleted when responses are delivered — so this requires two simultaneously in-flight requests.Versions
Reproduced on
v1.6.0(f5f2015) andmain(88c58c3);go version go1.25.0 linux/arm64.I have a minimal fix (atomic check-and-register in
servePOST, rejecting duplicates before any response data is written) with deterministic regression tests, and can open a PR if rejecting server-side is the agreed direction.