MCP client transport loses precision on integers > 2^53 (breaks any tool returning int64 IDs)
MCP client transport loses precision on integer values > 2^53 in tool results
Summary
When an MCP server returns a tool result containing an integer larger than 2^53 (9007199254740991), the value that reaches the model is corrupted. This silently breaks any MCP tool whose backend API returns 64-bit integer IDs — a very common case (Twitter/X Snowflake IDs, Telegram chat IDs, Snowflake DB keys, Stripe IDs, and now Yandex.Direct "combinatorial" ad IDs, all exceed 2^53).
The corruption happens on the client side of the MCP transport (inside Claude Code), not on the server side. The MCP server emits a correct JSON-RPC response with the exact integer; by the time the tool result is surfaced to the model, the integer has been rounded.
Expected behavior
JSON-RPC (and the MCP spec) treats integer as arbitrary precision — it is explicitly not a JavaScript Number. An MCP client must preserve integer values exactly on the server↔model round-trip, the same way json.loads / Pydantic model_dump_json do on the Python server side. A long/int64 returned by a tool should arrive at the model unchanged.
Actual behavior
Large integers are mangled. Concrete example observed with a Yandex.Direct ads.get MCP tool:
- Server returns
"Id": 1915883588174806058 - Model receives
1915883588174806000
The downstream consequence: the model then calls ads.update({id: 1915883588174806000, ...}), the backend rejects it with "object not found", and every read→mutate cycle on 64-bit IDs is broken.
Layer-by-layer measurement (proves the loss is client-side)
Tracing one ID through every hop between the backend API and the model:
| Layer | Value | Exact? |
|---|---|---|
| Backend HTTP API response body | 1915883588174806058 | ✅ |
| Local CLI wrapper stdout (json.dumps of the API response) | 1915883588174806058 | ✅ |
| MCP server run_json (json.loads(stdout)) | 1915883588174806058 | ✅ |
| MCP server final JSON-RPC serialization (pydantic CallToolResult.model_dump_json, structuredContent: dict[str, Any]) | 1915883588174806058 | ✅ (verified by constructing an equivalent CallToolResult and dumping it — int preserved) |
| Tool result as received by the model (client transport) | 1915883588174806000 | ❌ |
Every layer the server author controls preserves the integer. The degradation appears only after the correct JSON-RPC message leaves the server and before it is presented to the model — i.e. inside the MCP client transport.
Not classic IEEE-754 double rounding
Worth flagging because it narrows the suspect: a plain float() cast of the value produces 1915883588174806016 (standard double rounding). The value actually received is 1915883588174806000 — the three least significant decimal digits are zeroed. That signature points away from a naive float(x) and toward something like formatting the value as a decimal float with reduced precision, or a Number/parseInt/atof-style parse on a JS-side deserialization path. Either way it is in the client, not the server.
Reproducible without any specific backend
The bug is not specific to Yandex.Direct. Any minimal MCP server that returns {"id": 1915883588174806058} (or any integer > 2^53) from a tool, attached to Claude Code, will reproduce it: the model will see a rounded value. A one-line tool returning a hardcoded large integer is enough to confirm.
Why this matters
MCP servers are thin transports over real-world APIs, and real-world APIs routinely use 64-bit integer identifiers. Right now every such server is silently broken for read→mutate workflows, and there is nothing the server author can do to fix it without degrading their own contract (the only "fix" available to a server is to stringify all IDs, which violates the JSON-RPC integer type and breaks clients that do handle big ints correctly). The correct fix is in the client transport, once, for all servers.
Suggested fix
Preserve integer precision end-to-end in the MCP client transport when deserializing JSON-RPC tool results — i.e. parse JSON with a bigint-aware path (Python json.loads already does this; on the JS side, a reviver or a bigint-capable JSON parser) rather than routing numeric values through number/IEEE-754 double. At minimum, values exceeding 2^53 should either be kept exact or surfaced as an explicit error, rather than silently rounded.
4 Comments
I have reproduced this issue
When I wrote this I thought this was like a scream in some emptiness.
Reproduced on v2.1.233 (macOS).
Minimal repro, no real backend needed:
get_ad) answerstools/callwith the JSON-RPC bytes written verbatim (not through a JSNumber):``
json
`{"result":{"content":[{"type":"text","text":"{\"id\": 1915883588174806058}"}],"structuredContent":{"id":1915883588174806058}}}
tools/callPiping a
request into the server directly confirms it emits1915883588174806058` exactly..mcp.jsonand run:claude -p "Call the get_ad tool once and reply with ONLY the exact id value you received" --output-format stream-json --verbose --allowedTools mcp__bigint__get_adObserved: the tool result content is
{"id":1915883588174806000}and the model answers1915883588174806000— the last three digits are zeroed exactly as described in the issue.Expected:
1915883588174806058reaches the model unchanged.Extra data point: if the server returns only text
content(nostructuredContent), the value survives intact. The loss only occurs when the server includesstructuredContent: that JSON object is parsed into a JavaScript number and re-serialized, and the re-serialized JSON replaces the server's text content, so even the exact text block gets dropped. A server can work around it today by omittingstructuredContentor stringifying big IDs.Assessment: This looks like a genuine bug. The MCP spec says
contentshould carry a faithful rendering ofstructuredContent, and the server does send the exact value there, but the client parsesstructuredContentas a JavaScript number (losing precision above 2^53), re-serializes it, and drops the exact text block in favor of the lossy copy. This is not a recent regression — the client has preferred the re-serializedstructuredContentsince that field was first supported. A fix would either keep the server's own text block when both are present, or parse the JSON-RPC frames with a big-integer-safe parser.🤖 Generated with Claude Code
Confirmed — reproduced on 2.1.237 (Linux) with a minimal repro: a hand-rolled stdio MCP server whose tool result contains the exact integer
1915883588174806058on the wire (verified byte-for-byte, in both the text content andstructuredContent). Asking Claude to repeat theiddigit-for-digit returns1915883588174806000— the same corrupted value you observed.One note on the "not classic IEEE-754" analysis: it is standard double rounding after all. Parsing yields the nearest double (…806016), but JavaScript prints doubles using the shortest representation that round-trips, which renders that double as
…806000— Python's repr of the same double shows…016, which is why the signatures differ. That confirms your core diagnosis: the JSON-RPC response is being routed through 64-bit floats on the client side, and integers above 2^53 are silently corrupted before they reach the model.We agree this is a client-side bug worth fixing (real-world APIs routinely use int64 IDs, and read→mutate cycles break silently). Marking as reproduced.
🤖 Generated with Claude Code