Skip to content

Commit 5bc9e07

Browse files
committed
Fold only the canonical decimal form in coerce_request_id
coerce_request_id() coerced any string int() accepts back to an integer, so non-canonical numeric strings that are distinct ids on the JSON-RPC wire -- "007", "+7", "1_000", " 7 ", non-ASCII digit strings -- collapsed onto the same integer correlation key as the value they happen to parse to. That conflates ids the peer sent as genuinely different values in the shared _pending / _in_flight / progress-token correlation domain. Fold only when the string equals str(int(s)), the canonical form a JSON serializer emits for an int, preserving the intended "peer stringified an int id" correlation while leaving every other string a distinct id.
1 parent d060b36 commit 5bc9e07

2 files changed

Lines changed: 30 additions & 2 deletions

File tree

src/mcp/shared/dispatcher.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,20 @@ def coerce_request_id(request_id: RequestId) -> RequestId:
5858
5959
This is the collision/correlation domain dispatchers share: "7" and 7 are one
6060
id for correlation purposes, even where the wire carries the verbatim value.
61+
62+
Only the canonical decimal form a JSON serializer emits for an int (``str(n)``)
63+
folds. Non-canonical numeric strings — ``"007"``, ``"+7"``, ``"1_000"``,
64+
``" 7 "``, non-ASCII digit strings — are distinct ids on the JSON-RPC wire, not
65+
the same id as the integer they happen to parse to, so they pass through
66+
unchanged rather than collapsing wire-distinct ids into one correlation key.
6167
"""
6268
if isinstance(request_id, str):
6369
try:
64-
return int(request_id)
70+
value = int(request_id)
6571
except ValueError:
66-
pass
72+
return request_id
73+
if str(value) == request_id:
74+
return value
6775
return request_id
6876

6977

tests/shared/test_jsonrpc_dispatcher.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2011,6 +2011,26 @@ def test_coerce_request_id_passes_through_non_numeric_string_and_int():
20112011
assert coerce_request_id(42) == 42
20122012

20132013

2014+
def test_coerce_request_id_folds_only_the_canonical_decimal_form():
2015+
"""Only a string equal to `str(int)` folds; non-canonical numeric strings stay distinct wire ids.
2016+
2017+
"007", "+7", "1_000", " 7 " and non-ASCII digit strings are distinct JSON-RPC
2018+
ids, not the same id as the integer, so folding them would let the correlation
2019+
domain conflate ids the peer sent as genuinely different values.
2020+
"""
2021+
# Canonical decimal (including negative) still folds, preserving the intended
2022+
# "peer stringified an int id" correlation.
2023+
assert coerce_request_id("7") == 7
2024+
assert coerce_request_id("-3") == -3
2025+
assert coerce_request_id("0") == 0
2026+
# Non-canonical numeric strings are left as-is: they are not str(int) of any int.
2027+
for wire_id in ("007", "+7", " 7 ", "1_000", "\t42\n", "٧", "7.0"):
2028+
assert coerce_request_id(wire_id) == wire_id, wire_id
2029+
# A non-canonical numeric string does not collide with the integer it parses to.
2030+
assert coerce_request_id("1_000") != coerce_request_id(1000)
2031+
assert coerce_request_id("+7") != coerce_request_id(7)
2032+
2033+
20142034
@pytest.mark.anyio
20152035
async def test_jsonrpc_error_response_with_null_id_is_dropped():
20162036
"""Parse-error responses (id=null) have no waiter; they're dropped and the read loop stays healthy."""

0 commit comments

Comments
 (0)