Repository navigation
mcp: preserve structured content number precision - #1239
Merged
guglielmo-san merged 3 commits intoSep 16, 2026
Merged
guglielmo-san merged 3 commits into
guglielmo-san merged 3 commits into
Conversation
guglielmo-san
approved these changes
Sep 16, 2026
Contributor
|
Thank you for the contribution! |
yehia-khalil
added a commit
to yehia-khalil/go-sdk
that referenced
this pull request
Sep 26, 2026
modelcontextprotocol#1239 fixed the CLIENT side of this: CallToolResult.StructuredContent is typed `any`, so a Go client decoded wire numbers into float64. This is the SERVER side, which that change did not reach — the bytes are already wrong before they are sent, so every client is affected, including clients that are not written in Go and never touch CallToolResult at all. applySchema decodes a tool's arguments and its result into `any` and re-marshals whenever defaults may have been applied — which is every object-rooted schema, so in practice always. The decoder had no UseNumber(), so every JSON number became a float64 and the re-marshal wrote the lossy value back out. An integer that is exact in JSON and exact in int64 but not in float64 therefore arrived correct and left wrong, silently: in: {"id":9007199254740993} out: {"id":9007199254740992} Snowflake ids, Discord/Twitter-style ids and any BIGINT primary key allocated above 2^53 are all in range, on both the argument path (server.go:360) and the result path (server.go:422). UseNumber() alone does not fix it: json.Number is a string type, and jsonschema reports it as `has type "string", want "integer"`. So the decoded tree is narrowed first — json.Number becomes int64 when it is an integer that fits and float64 otherwise. Both validate correctly against "integer" and "number", and both re-marshal to the text they were decoded from. This reuses modelcontextprotocol#1239's internal/json.UnmarshalUseNumber rather than adding a second decoder helper. That function also runs checkMaxDepth, which an earlier revision of this change did not — reusing it closes that gap as well as avoiding the duplicate. A JSON integer outside int64's range still falls back to float64; that is documented on narrowNumbers. Representing it exactly would need a big.Int, which jsonschema does not accept. Tests cover the argument path, the result path, nesting inside arrays and objects (a database row set is an array of arrays), and that a float is still a float afterwards. Verification: - gofmt -l . - go build ./... - go vet ./... - go test ./... (14 packages, 0 failures)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CallToolResult currently decodes structuredContent through an any field, which converts JSON numbers to float64 and silently rounds integers outside the IEEE-754 safe range.
This change decodes structuredContent separately with UseNumber, preserving wire numbers as json.Number while retaining the existing decoded object, array, and primitive shapes. The field documentation now describes that representation. Users who depend on the previous float64 behavior can temporarily restore it with MCPGODEBUG=structuredcontentfloat64=1.
An end-to-end in-memory client/server regression covers 9007199254740993 from a low-level Server.AddTool result. Additional coverage verifies the compatibility option restores the legacy rounded float64 value.
Verification:
Fixes #1236