Repository navigation
fix(shared): stop concurrent int/string request ids from colliding in _in_flight - #3815
1828703450qqcom-ux wants to merge 1 commit into
Conversation
… _in_flight JSONRPCDispatcher._in_flight keyed inbound requests by coerce_request_id(req.id), which folds "7" and 7 into one dict slot. Two concurrent requests whose ids differ only in type blind-overwrote each other, and a notifications/cancelled naming one cancelled whichever was stored — the other ran to completion and could no longer be cancelled (issue modelcontextprotocol#3807). Store in-flight requests under the verbatim id so distinct wire representations stay distinct. Cancellation looks up the exact id first and falls back to coerced/stringified forms so a peer that echoes a different type between request and cancel still cancels (existing correlation tests pass unchanged).
|
This PR has been closed automatically. This repo only keeps pull requests open when they come from a maintainer, or from a contributor a maintainer has assigned to the linked issue, and you aren't currently assigned to #3807. If a maintainer assigns you to #3807, this PR reopens on its own and there's nothing more you need to do here. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. What does help is engaging on the issue itself by confirming the repro, explaining why it matters for your use case, or describing the approach you'd take. You're welcome to keep pushing commits here (just avoid force-pushing, since GitHub can't reopen a rewritten branch), but that on its own won't get the PR reviewed or the issue assigned, and realistically most auto-closed PRs stay closed. There's no need to open a new PR either way. CONTRIBUTING.md has the full reasoning, but in short:
Maintainers: reopen, remove |
fix(shared): stop concurrent int/string request ids from colliding in _in_flight
Fixes #3807
Summary
JSONRPCDispatcher._in_flightkeyed inbound requests bycoerce_request_id(req.id), which folds"7"and7into one dict slot.Two concurrent requests whose ids differ only in JSON-RPC type
blind-overwrote each other, and a
notifications/cancellednaming onecancelled whichever was stored — the other ran to completion and could no
longer be cancelled.
representations stay distinct. Cancellation looks up the exact id first
and falls back to coerced/stringified forms so a peer that echoes a
different type between request and cancel still cancels the request
(the existing
test_cancelled_correlates_across_string_and_int_request_id_formspasses unchanged in both parametrizations).
Reproduction
Unmodified SDK, stdio, two concurrent requests with ids
"9007199254740991"(string) and
9007199254740991(int), plus anotifications/cancelledforthe string id (see the reproduction script linked in #3807):
main: the cancel hits the int-id request (they share the coercedslot); the string-id request answers, the int-id request never does.
request answers normally.
Fix
src/mcp/shared/jsonrpc_dispatcher.py— three hunks:_dispatch_request):self._in_flight[req.id] = ...—verbatim id, no coercion.
_dispatch_notification): exact-id lookup first; if missed,try the coerced id and its stringified form so cross-type echo still
correlates.
_handle_request): pop by verbatim id, matching the store.The
pending(outbound correlation) table is untouched: its coercion iscorrect and needed for response matching.
Validation
New regression test
test_peer_cancel_does_not_cross_request_id_types:main(af4c6f4)"9007199254740991"and9007199254740991, cancel the string oneThe regression test fails on unmodified
main(the int-id request gets noresponse) and passes after. Full
tests/shared/test_jsonrpc_dispatcher.py:80 passed, 0 failed — zero regressions, including both parametrizations
of the cross-type correlation test.
Scope
src/mcp/shared/jsonrpc_dispatcher.pyandtests/shared/test_jsonrpc_dispatcher.py.pendingtable,_allocate_id, the directdispatcher's collision detection, or any handler/cancellation semantics
beyond the id-keying described above.