Skip to content

fix(shared): stop concurrent int/string request ids from colliding in _in_flight - #3815

Closed
1828703450qqcom-ux wants to merge 1 commit into
modelcontextprotocol:mainfrom
1828703450qqcom-ux:fix/in-flight-id-type-collision
Closed

1828703450qqcom-ux wants to merge 1 commit into
modelcontextprotocol:mainfrom
1828703450qqcom-ux:fix/in-flight-id-type-collision

Conversation

@1828703450qqcom-ux

Copy link
Copy Markdown

fix(shared): stop concurrent int/string request ids from colliding in _in_flight

Fixes #3807

Summary

  • 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 JSON-RPC 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.
  • 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 the request
    (the existing test_cancelled_correlates_across_string_and_int_request_id_forms
    passes unchanged in both parametrizations).

Reproduction

Unmodified SDK, stdio, two concurrent requests with ids "9007199254740991"
(string) and 9007199254740991 (int), plus a notifications/cancelled for
the string id (see the reproduction script linked in #3807):

  • on main: the cancel hits the int-id request (they share the coerced
    slot); the string-id request answers, the int-id request never does.
  • after the fix: the cancel hits only the string-id request; the int-id
    request answers normally.

Fix

src/mcp/shared/jsonrpc_dispatcher.py — three hunks:

  1. Store (_dispatch_request): self._in_flight[req.id] = ... —
    verbatim id, no coercion.
  2. Cancel (_dispatch_notification): exact-id lookup first; if missed,
    try the coerced id and its stringified form so cross-type echo still
    correlates.
  3. Delete (_handle_request): pop by verbatim id, matching the store.

The pending (outbound correlation) table is untouched: its coercion is
correct and needed for response matching.

Validation

New regression test test_peer_cancel_does_not_cross_request_id_types:

case on main (af4c6f4) after
concurrent "9007199254740991" and 9007199254740991, cancel the string one int-id request cancelled (wrong); string-id answered string-id cancelled; int-id answers
cross-type echo: cancel with int for a string-id request passes passes (coerced fallback)
cross-type echo: cancel with string for an int-id request passes passes (stringified fallback)

The regression test fails on unmodified main (the int-id request gets no
response) 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

  • 2 files: src/mcp/shared/jsonrpc_dispatcher.py and
    tests/shared/test_jsonrpc_dispatcher.py.
  • No change to the outbound pending table, _allocate_id, the direct
    dispatcher's collision detection, or any handler/cancellation semantics
    beyond the id-keying described above.

… _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).
@github-actions github-actions Bot added the missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md) label Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

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:

  • We're a small team with very little capacity to review community PRs right now.
  • Many recent PRs are AI-generated with little human review, and reviewing one carefully still costs a maintainer as much time as it ever did. A well-described issue is usually more useful to us than the code.

Maintainers: reopen, remove missing-issue-link, or add bypass-issue-check to override.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

notifications/cancelled for request 7 cancels a concurrent request "7" (_in_flight keyed by coerce_request_id)

1 participant