Failure sequence
- Connect to a legacy MCP server negotiated at
2025-03-26, where receive-side JSON-RPC batches are allowed.
- Odin sends a post-handshake request such as
tools/list with ID 7.
- The server returns one legal batch frame containing two response objects with ID
7, for example one result with marker: \"first\" and a second with marker: \"second\".
- On stdio, the first response resolves the pending future and the second is logged as late/unknown. On HTTP,
_match_response() selects the first matching object and the remaining matching response is dispatched without triggering a protocol failure. The request completes using marker: \"first\" instead of rejecting the duplicate response sequence.
The same defect also applies to duplicate matching response events in an HTTP SSE stream: the callback overwrites the previously collected response and accepts one after the stream closes.
A server can therefore provide contradictory results for one request without invalidating discovery or a tool call. During discovery, this can make the published catalog depend on response ordering instead of rejecting the malformed exchange.
Sites
src/tools/mcp/client.py:392-420 resolves a stdio future once and drops another response with the same pending ID.
src/tools/mcp/client.py:611-616 returns the first matching HTTP response without checking cardinality.
src/tools/mcp/client.py:975-1007 overwrites streamed matches and routes extra JSON-body matches without detecting duplicates.
Handshake traffic already rejects duplicate matching responses at src/tools/mcp/client.py:508-516; post-handshake requests should enforce the same one-response-per-request invariant.
Expected
If more than one response matches a post-handshake request ID, fail that request with MCPProtocolError and do not publish or execute from either result.
Failure sequence
2025-03-26, where receive-side JSON-RPC batches are allowed.tools/listwith ID7.7, for example one result withmarker: \"first\"and a second withmarker: \"second\"._match_response()selects the first matching object and the remaining matching response is dispatched without triggering a protocol failure. The request completes usingmarker: \"first\"instead of rejecting the duplicate response sequence.The same defect also applies to duplicate matching response events in an HTTP SSE stream: the callback overwrites the previously collected response and accepts one after the stream closes.
A server can therefore provide contradictory results for one request without invalidating discovery or a tool call. During discovery, this can make the published catalog depend on response ordering instead of rejecting the malformed exchange.
Sites
src/tools/mcp/client.py:392-420resolves a stdio future once and drops another response with the same pending ID.src/tools/mcp/client.py:611-616returns the first matching HTTP response without checking cardinality.src/tools/mcp/client.py:975-1007overwrites streamed matches and routes extra JSON-body matches without detecting duplicates.Handshake traffic already rejects duplicate matching responses at
src/tools/mcp/client.py:508-516; post-handshake requests should enforce the same one-response-per-request invariant.Expected
If more than one response matches a post-handshake request ID, fail that request with
MCPProtocolErrorand do not publish or execute from either result.