From 5475987e27d0ea182f100136756a11220d685873 Mon Sep 17 00:00:00 2001 From: King Star Date: Sun, 6 Sep 2026 22:23:56 +0800 Subject: [PATCH 1/3] mcp: preserve JSON number precision in tool argument schemas applySchema decoded tool arguments into a map[string]any, which represents every JSON number as a float64, and re-marshalled the value when schema defaults were applied. Integers outside the IEEE-754 safe range were silently rounded before the typed handler decoded them: a tool taking an int64 ID received 9007199254740992 for an argument of 9007199254740993. Decode with UseNumber so the re-marshalled JSON reproduces each number's original literal text. UnmarshalUseNumber applies the same nesting-depth check as Unmarshal. Validation cannot run on that value directly. A json.Number has reflect.Kind String, so jsonschema's type check reports it as a JSON string and rejects it against "type": "integer". Validate a copy whose numbers are converted back to float64, which is exactly the representation this code has always validated, so validation semantics are unchanged. A number too large for a float64 is still rejected, as it was when decoding produced the error. This covers the output schema path as well, so an object-rooted structured result keeps its precision on the wire. Fixes #1201 --- internal/json/json.go | 16 +++++++ mcp/server_test.go | 43 +++++++++++++++++ mcp/tool.go | 57 +++++++++++++++++++++-- mcp/tool_test.go | 104 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 217 insertions(+), 3 deletions(-) diff --git a/internal/json/json.go b/internal/json/json.go index b3fa039b6..8031101d1 100644 --- a/internal/json/json.go +++ b/internal/json/json.go @@ -37,6 +37,12 @@ func (d *Decoder) Decode(v any) error { return d.dec.Decode(v) } +// UseNumber causes the decoder to unmarshal a number into a [json.Number] +// rather than a float64, preserving the number's original literal text. +func (d *Decoder) UseNumber() { + d.dec.UseNumber() +} + func Unmarshal(data []byte, v any) error { if err := checkMaxDepth(data, defaultMaxDepth); err != nil { return err @@ -44,6 +50,16 @@ func Unmarshal(data []byte, v any) error { return NewDecoder(bytes.NewReader(data)).Decode(v) } +// UnmarshalUseNumber is [Unmarshal] with [Decoder.UseNumber] set. +func UnmarshalUseNumber(data []byte, v any) error { + if err := checkMaxDepth(data, defaultMaxDepth); err != nil { + return err + } + dec := NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + return dec.Decode(v) +} + // checkMaxDepth scans data once and reports [errMaxDepthExceeded] if the // nesting of JSON objects and arrays exceeds maxDepth. It is a lightweight pass which // tracks '{' and '[' against '}' and ']' while skipping over the contents of strings. diff --git a/mcp/server_test.go b/mcp/server_test.go index 11e75e232..e94da50fd 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -1932,3 +1932,46 @@ func TestServerSupportedProtocolVersions_NewProtocol(t *testing.T) { t.Errorf("UnsupportedProtocolVersionData.Supported mismatch (-want +got):\n%s", diff) } } + +// TestToolArgumentIntegerPrecision exercises the typed AddTool path end to end +// with an integer outside the IEEE-754 safe range (issue #1201). +func TestToolArgumentIntegerPrecision(t *testing.T) { + ctx := context.Background() + + type input struct { + ID int64 `json:"id"` + } + const want int64 = 9007199254740993 // 2^53 + 1 + + var got int64 + server := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil) + AddTool(server, &Tool{Name: "echo_id"}, + func(_ context.Context, _ *CallToolRequest, in input) (*CallToolResult, input, error) { + got = in.ID + return nil, in, nil + }) + + cTransport, sTransport := NewInMemoryTransports() + ss, err := server.Connect(ctx, sTransport, nil) + if err != nil { + t.Fatal(err) + } + defer ss.Close() + + client := NewClient(&Implementation{Name: "testClient", Version: "v1.0.0"}, nil) + cs, err := client.Connect(ctx, cTransport, nil) + if err != nil { + t.Fatal(err) + } + defer cs.Close() + + if _, err := cs.CallTool(ctx, &CallToolParams{ + Name: "echo_id", + Arguments: map[string]any{"id": want}, + }); err != nil { + t.Fatal(err) + } + if got != want { + t.Errorf("handler received id %d, want %d", got, want) + } +} diff --git a/mcp/tool.go b/mcp/tool.go index 11ad100ee..ec56ad8f5 100644 --- a/mcp/tool.go +++ b/mcp/tool.go @@ -91,18 +91,22 @@ func applySchema(data json.RawMessage, resolved *jsonschema.Resolved, forOutput return data, nil } + // Decode numbers as json.Number so that any re-marshalling below + // reproduces their original literal text. Decoding into any/map[string]any + // otherwise represents every JSON number as a float64, which silently + // rounds integers outside the IEEE-754 safe range (issue #1201). var unmarshaled any if !forOutput { v := make(map[string]any) if len(data) > 0 { - if err := internaljson.Unmarshal(data, &v); err != nil { + if err := internaljson.UnmarshalUseNumber(data, &v); err != nil { return nil, fmt.Errorf("unmarshaling arguments: %w", err) } } unmarshaled = v } else { if len(data) > 0 { - if err := internaljson.Unmarshal(data, &unmarshaled); err != nil { + if err := internaljson.UnmarshalUseNumber(data, &unmarshaled); err != nil { return nil, fmt.Errorf("unmarshaling output: %w", err) } } @@ -126,7 +130,15 @@ func applySchema(data json.RawMessage, resolved *jsonschema.Resolved, forOutput appliedDefaults = true } - if err := resolved.Validate(&unmarshaled); err != nil { + // Validate a float64 copy: a json.Number has reflect.Kind String, so + // jsonschema reports it as a JSON string and rejects it against + // "type": "integer". Converting keeps validation on exactly the + // representation it has always seen. + forValidation, err := jsonNumbersAsFloat(unmarshaled) + if err != nil { + return nil, err + } + if err := resolved.Validate(&forValidation); err != nil { return nil, err } @@ -141,6 +153,45 @@ func applySchema(data json.RawMessage, resolved *jsonschema.Resolved, forOutput return out, nil } +// jsonNumbersAsFloat returns a copy of v with every [json.Number] replaced by +// the float64 it denotes, leaving all other values alone. +// +// It reports an error for a number that cannot be represented as a float64, +// matching the error that decoding it without [json.Decoder.UseNumber] would +// have produced. +func jsonNumbersAsFloat(v any) (any, error) { + switch v := v.(type) { + case map[string]any: + m := make(map[string]any, len(v)) + for key, elem := range v { + c, err := jsonNumbersAsFloat(elem) + if err != nil { + return nil, err + } + m[key] = c + } + return m, nil + case []any: + s := make([]any, len(v)) + for i, elem := range v { + c, err := jsonNumbersAsFloat(elem) + if err != nil { + return nil, err + } + s[i] = c + } + return s, nil + case json.Number: + f, err := v.Float64() + if err != nil { + return nil, fmt.Errorf("number %s cannot be unmarshaled into a float64", v) + } + return f, nil + default: + return v, nil + } +} + // isObjectJSON reports whether data is a JSON object (i.e., starts with '{' // after any leading whitespace). Returns false for arrays, primitives, null, // or empty input. diff --git a/mcp/tool_test.go b/mcp/tool_test.go index 7a82c96dd..b3abf84bd 100644 --- a/mcp/tool_test.go +++ b/mcp/tool_test.go @@ -272,3 +272,107 @@ func TestValidateToolName(t *testing.T) { }) } + +// TestApplySchemaNumberPrecision verifies that applySchema preserves JSON +// integers outside the IEEE-754 safe range, which a float64 round-trip would +// silently round (issue #1201). +func TestApplySchemaNumberPrecision(t *testing.T) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "id": {Type: "integer"}, + "nested": {Type: "object", Properties: map[string]*jsonschema.Schema{"id": {Type: "integer"}}}, + "ids": {Type: "array", Items: &jsonschema.Schema{Type: "integer"}}, + "ratio": {Type: "number"}, + "x": {Type: "integer", Default: json.RawMessage("3")}, + }, + } + resolved, err := schema.Resolve(&jsonschema.ResolveOptions{ValidateDefaults: true}) + if err != nil { + t.Fatal(err) + } + + // A default on the schema forces the re-marshalling path, which is where + // the rounding was observable. + for _, tt := range []struct { + name string + data string + want string + }{ + {"large int64", `{"id":9007199254740993}`, `{"id":9007199254740993,"x":3}`}, + {"max int64", `{"id":9223372036854775807}`, `{"id":9223372036854775807,"x":3}`}, + {"min int64", `{"id":-9223372036854775808}`, `{"id":-9223372036854775808,"x":3}`}, + {"nested", `{"nested":{"id":9007199254740993}}`, `{"nested":{"id":9007199254740993},"x":3}`}, + {"array", `{"ids":[9007199254740993]}`, `{"ids":[9007199254740993],"x":3}`}, + {"high-precision float", `{"ratio":0.1234567890123456789}`, `{"ratio":0.1234567890123456789,"x":3}`}, + } { + t.Run(tt.name, func(t *testing.T) { + got, err := applySchema(json.RawMessage(tt.data), resolved, false) + if err != nil { + t.Fatalf("applySchema(%s) failed: %v", tt.data, err) + } + if string(got) != tt.want { + t.Errorf("applySchema(%s) = %s, want %s", tt.data, got, tt.want) + } + }) + } +} + +// TestApplySchemaOutputNumberPrecision covers the output path, where an +// object-rooted schema with a default also re-marshals the value. +func TestApplySchemaOutputNumberPrecision(t *testing.T) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "id": {Type: "integer"}, + "x": {Type: "integer", Default: json.RawMessage("3")}, + }, + } + resolved, err := schema.Resolve(&jsonschema.ResolveOptions{ValidateDefaults: true}) + if err != nil { + t.Fatal(err) + } + + const data = `{"id":9007199254740993}` + const want = `{"id":9007199254740993,"x":3}` + got, err := applySchema(json.RawMessage(data), resolved, true) + if err != nil { + t.Fatalf("applySchema(%s) failed: %v", data, err) + } + if string(got) != want { + t.Errorf("applySchema(%s) = %s, want %s", data, got, want) + } +} + +// TestApplySchemaNumberErrors verifies that numbers which cannot be +// represented as a float64 are still rejected, and that a value of the wrong +// JSON type is still reported as such rather than as a string. +func TestApplySchemaNumberErrors(t *testing.T) { + schema := &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{"id": {Type: "integer"}}, + } + resolved, err := schema.Resolve(nil) + if err != nil { + t.Fatal(err) + } + + for _, tt := range []struct { + name string + data string + want string + }{ + {"unrepresentable", `{"id":1e9999999}`, "float64"}, + {"wrong type", `{"id":"nope"}`, `want "integer"`}, + } { + t.Run(tt.name, func(t *testing.T) { + _, err := applySchema(json.RawMessage(tt.data), resolved, false) + if err == nil { + t.Fatalf("applySchema(%s) succeeded, want error", tt.data) + } + if !strings.Contains(err.Error(), tt.want) { + t.Errorf("applySchema(%s) error = %v, want it to contain %q", tt.data, err, tt.want) + } + }) + } +} From 485e2842f0ec593c5f6fb581897541fa28a31617 Mon Sep 17 00:00:00 2001 From: King Star Date: Wed, 9 Sep 2026 05:28:02 +0800 Subject: [PATCH 2/3] mcp: preserve structured content number precision --- mcp/protocol.go | 56 ++++++++++++++++++++++++++++++++++++-- mcp/protocol_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++ mcp/server_test.go | 38 ++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 2 deletions(-) diff --git a/mcp/protocol.go b/mcp/protocol.go index 63d701514..34a05e44b 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -8,6 +8,8 @@ import ( "encoding/json" "fmt" "maps" + "math" + "math/big" internaljson "github.com/modelcontextprotocol/go-sdk/internal/json" "github.com/modelcontextprotocol/go-sdk/internal/mcpgodebug" @@ -391,12 +393,20 @@ func (x *CallToolResult) UnmarshalJSON(data []byte) error { type res CallToolResult // avoid recursion var wire struct { res - Content []*wireContent `json:"content"` - ResultType resultType `json:"resultType"` + Content []*wireContent `json:"content"` + ResultType resultType `json:"resultType"` + StructuredContent json.RawMessage `json:"structuredContent"` } if err := internaljson.Unmarshal(data, &wire); err != nil { return err } + if len(wire.StructuredContent) > 0 { + var structured any + if err := internaljson.UnmarshalUseNumber(wire.StructuredContent, &structured); err != nil { + return err + } + wire.res.StructuredContent = preserveJSONNumbers(structured) + } var err error if wire.res.Content, err = contentsFromWire(wire.Content, nil); err != nil { return err @@ -406,6 +416,48 @@ func (x *CallToolResult) UnmarshalJSON(data []byte) error { return nil } +// preserveJSONNumbers keeps the existing float64 representation when it can +// round-trip a JSON number without changing its value. Numbers that would +// lose information remain json.Number so callers can handle them exactly. +func preserveJSONNumbers(value any) any { + switch value := value.(type) { + case map[string]any: + for key, elem := range value { + value[key] = preserveJSONNumbers(elem) + } + case []any: + for i, elem := range value { + value[i] = preserveJSONNumbers(elem) + } + case json.Number: + if number, ok := losslessFloat64(value); ok { + return number + } + } + return value +} + +func losslessFloat64(value json.Number) (float64, bool) { + number, err := value.Float64() + if err != nil || math.IsInf(number, 0) || math.IsNaN(number) { + return 0, false + } + + original, ok := new(big.Rat).SetString(value.String()) + if !ok { + return 0, false + } + encoded, err := json.Marshal(number) + if err != nil { + return 0, false + } + roundTripped, ok := new(big.Rat).SetString(string(encoded)) + if !ok || original.Cmp(roundTripped) != 0 { + return 0, false + } + return number, true +} + func (x *CallToolParams) isParams() {} func (x *CallToolParams) isNil() bool { return x == nil } func (x *CallToolParams) GetProgressToken() any { return getProgressToken(x) } diff --git a/mcp/protocol_test.go b/mcp/protocol_test.go index fe496dec8..dee8548ed 100644 --- a/mcp/protocol_test.go +++ b/mcp/protocol_test.go @@ -1217,6 +1217,70 @@ func TestInputRequestMapJSON(t *testing.T) { }) } +func TestCallToolResultPreservesStructuredContentNumberPrecision(t *testing.T) { + const input = `{"content":[],"structuredContent":{"id":9007199254740993,"safe":42,"fraction":0.1,"ids":[9007199254740993,42]}}` + + var result CallToolResult + if err := json.Unmarshal([]byte(input), &result); err != nil { + t.Fatal(err) + } + + structured, ok := result.StructuredContent.(map[string]any) + if !ok { + t.Fatalf("structured content has type %T, want map[string]any", result.StructuredContent) + } + if got, ok := structured["id"].(json.Number); !ok || got.String() != "9007199254740993" { + t.Fatalf("structured content id = %#v (%T), want exact json.Number", structured["id"], structured["id"]) + } + if got, ok := structured["safe"].(float64); !ok || got != 42 { + t.Fatalf("structured content safe = %#v (%T), want float64(42)", structured["safe"], structured["safe"]) + } + if got, ok := structured["fraction"].(float64); !ok || got != 0.1 { + t.Fatalf("structured content fraction = %#v (%T), want float64(0.1)", structured["fraction"], structured["fraction"]) + } + ids, ok := structured["ids"].([]any) + if !ok { + t.Fatalf("structured content ids has type %T, want []any", structured["ids"]) + } + if got, ok := ids[0].(json.Number); !ok || got.String() != "9007199254740993" { + t.Fatalf("structured content ids[0] = %#v (%T), want exact json.Number", ids[0], ids[0]) + } + if got, ok := ids[1].(float64); !ok || got != 42 { + t.Fatalf("structured content ids[1] = %#v (%T), want float64(42)", ids[1], ids[1]) + } + + encoded, err := json.Marshal(&result) + if err != nil { + t.Fatal(err) + } + var wire struct { + StructuredContent json.RawMessage `json:"structuredContent"` + } + if err := json.Unmarshal(encoded, &wire); err != nil { + t.Fatal(err) + } + var got map[string]json.RawMessage + if err := json.Unmarshal(wire.StructuredContent, &got); err != nil { + t.Fatal(err) + } + if string(got["id"]) != "9007199254740993" { + t.Errorf("round-trip structured content id = %s, want 9007199254740993", got["id"]) + } +} + +func TestCallToolResultPreservesRootStructuredContentNumberPrecision(t *testing.T) { + const input = `{"content":[],"structuredContent":9007199254740993}` + + var result CallToolResult + if err := json.Unmarshal([]byte(input), &result); err != nil { + t.Fatal(err) + } + got, ok := result.StructuredContent.(json.Number) + if !ok || got.String() != "9007199254740993" { + t.Fatalf("structured content = %#v (%T), want exact json.Number", result.StructuredContent, result.StructuredContent) + } +} + func TestInputResponseMapJSON(t *testing.T) { tests := []struct { name string diff --git a/mcp/server_test.go b/mcp/server_test.go index e94da50fd..d5e03f423 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -822,6 +822,44 @@ func TestAddToolGenericNonObjectOutput(t *testing.T) { }) } +func TestCallToolStructuredContentPreservesLargeInteger(t *testing.T) { + ctx := context.Background() + server := NewServer(testImpl, nil) + server.AddTool(&Tool{ + Name: "large_integer", + InputSchema: &jsonschema.Schema{Type: "object"}, + }, func(context.Context, *CallToolRequest) (*CallToolResult, error) { + return &CallToolResult{ + Content: []Content{&TextContent{Text: "ok"}}, + StructuredContent: json.RawMessage(`{"id":9007199254740993}`), + }, nil + }) + + clientTransport, serverTransport := NewInMemoryTransports() + if _, err := server.Connect(ctx, serverTransport, nil); err != nil { + t.Fatal(err) + } + client := NewClient(testImpl, nil) + clientSession, err := client.Connect(ctx, clientTransport, nil) + if err != nil { + t.Fatal(err) + } + defer clientSession.Close() + + result, err := clientSession.CallTool(ctx, &CallToolParams{Name: "large_integer"}) + if err != nil { + t.Fatal(err) + } + structured, ok := result.StructuredContent.(map[string]any) + if !ok { + t.Fatalf("structured content has type %T, want map[string]any", result.StructuredContent) + } + got, ok := structured["id"].(json.Number) + if !ok || got.String() != "9007199254740993" { + t.Fatalf("structured content id = %#v (%T), want exact json.Number", structured["id"], structured["id"]) + } +} + // TestAddToolInputSchemaComposition verifies SEP-2106 (input side): composition // keywords such as oneOf are allowed on the input schema alongside // type:"object". From fd4681dd320c08e0e3fcc86db77fbf2b9183777c Mon Sep 17 00:00:00 2001 From: King Star Date: Sat, 12 Sep 2026 22:26:02 +0800 Subject: [PATCH 3/3] Revert "mcp: preserve structured content number precision" This reverts commit 485e2842f0ec593c5f6fb581897541fa28a31617. --- mcp/protocol.go | 56 ++------------------------------------ mcp/protocol_test.go | 64 -------------------------------------------- mcp/server_test.go | 38 -------------------------- 3 files changed, 2 insertions(+), 156 deletions(-) diff --git a/mcp/protocol.go b/mcp/protocol.go index 34a05e44b..63d701514 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -8,8 +8,6 @@ import ( "encoding/json" "fmt" "maps" - "math" - "math/big" internaljson "github.com/modelcontextprotocol/go-sdk/internal/json" "github.com/modelcontextprotocol/go-sdk/internal/mcpgodebug" @@ -393,20 +391,12 @@ func (x *CallToolResult) UnmarshalJSON(data []byte) error { type res CallToolResult // avoid recursion var wire struct { res - Content []*wireContent `json:"content"` - ResultType resultType `json:"resultType"` - StructuredContent json.RawMessage `json:"structuredContent"` + Content []*wireContent `json:"content"` + ResultType resultType `json:"resultType"` } if err := internaljson.Unmarshal(data, &wire); err != nil { return err } - if len(wire.StructuredContent) > 0 { - var structured any - if err := internaljson.UnmarshalUseNumber(wire.StructuredContent, &structured); err != nil { - return err - } - wire.res.StructuredContent = preserveJSONNumbers(structured) - } var err error if wire.res.Content, err = contentsFromWire(wire.Content, nil); err != nil { return err @@ -416,48 +406,6 @@ func (x *CallToolResult) UnmarshalJSON(data []byte) error { return nil } -// preserveJSONNumbers keeps the existing float64 representation when it can -// round-trip a JSON number without changing its value. Numbers that would -// lose information remain json.Number so callers can handle them exactly. -func preserveJSONNumbers(value any) any { - switch value := value.(type) { - case map[string]any: - for key, elem := range value { - value[key] = preserveJSONNumbers(elem) - } - case []any: - for i, elem := range value { - value[i] = preserveJSONNumbers(elem) - } - case json.Number: - if number, ok := losslessFloat64(value); ok { - return number - } - } - return value -} - -func losslessFloat64(value json.Number) (float64, bool) { - number, err := value.Float64() - if err != nil || math.IsInf(number, 0) || math.IsNaN(number) { - return 0, false - } - - original, ok := new(big.Rat).SetString(value.String()) - if !ok { - return 0, false - } - encoded, err := json.Marshal(number) - if err != nil { - return 0, false - } - roundTripped, ok := new(big.Rat).SetString(string(encoded)) - if !ok || original.Cmp(roundTripped) != 0 { - return 0, false - } - return number, true -} - func (x *CallToolParams) isParams() {} func (x *CallToolParams) isNil() bool { return x == nil } func (x *CallToolParams) GetProgressToken() any { return getProgressToken(x) } diff --git a/mcp/protocol_test.go b/mcp/protocol_test.go index dee8548ed..fe496dec8 100644 --- a/mcp/protocol_test.go +++ b/mcp/protocol_test.go @@ -1217,70 +1217,6 @@ func TestInputRequestMapJSON(t *testing.T) { }) } -func TestCallToolResultPreservesStructuredContentNumberPrecision(t *testing.T) { - const input = `{"content":[],"structuredContent":{"id":9007199254740993,"safe":42,"fraction":0.1,"ids":[9007199254740993,42]}}` - - var result CallToolResult - if err := json.Unmarshal([]byte(input), &result); err != nil { - t.Fatal(err) - } - - structured, ok := result.StructuredContent.(map[string]any) - if !ok { - t.Fatalf("structured content has type %T, want map[string]any", result.StructuredContent) - } - if got, ok := structured["id"].(json.Number); !ok || got.String() != "9007199254740993" { - t.Fatalf("structured content id = %#v (%T), want exact json.Number", structured["id"], structured["id"]) - } - if got, ok := structured["safe"].(float64); !ok || got != 42 { - t.Fatalf("structured content safe = %#v (%T), want float64(42)", structured["safe"], structured["safe"]) - } - if got, ok := structured["fraction"].(float64); !ok || got != 0.1 { - t.Fatalf("structured content fraction = %#v (%T), want float64(0.1)", structured["fraction"], structured["fraction"]) - } - ids, ok := structured["ids"].([]any) - if !ok { - t.Fatalf("structured content ids has type %T, want []any", structured["ids"]) - } - if got, ok := ids[0].(json.Number); !ok || got.String() != "9007199254740993" { - t.Fatalf("structured content ids[0] = %#v (%T), want exact json.Number", ids[0], ids[0]) - } - if got, ok := ids[1].(float64); !ok || got != 42 { - t.Fatalf("structured content ids[1] = %#v (%T), want float64(42)", ids[1], ids[1]) - } - - encoded, err := json.Marshal(&result) - if err != nil { - t.Fatal(err) - } - var wire struct { - StructuredContent json.RawMessage `json:"structuredContent"` - } - if err := json.Unmarshal(encoded, &wire); err != nil { - t.Fatal(err) - } - var got map[string]json.RawMessage - if err := json.Unmarshal(wire.StructuredContent, &got); err != nil { - t.Fatal(err) - } - if string(got["id"]) != "9007199254740993" { - t.Errorf("round-trip structured content id = %s, want 9007199254740993", got["id"]) - } -} - -func TestCallToolResultPreservesRootStructuredContentNumberPrecision(t *testing.T) { - const input = `{"content":[],"structuredContent":9007199254740993}` - - var result CallToolResult - if err := json.Unmarshal([]byte(input), &result); err != nil { - t.Fatal(err) - } - got, ok := result.StructuredContent.(json.Number) - if !ok || got.String() != "9007199254740993" { - t.Fatalf("structured content = %#v (%T), want exact json.Number", result.StructuredContent, result.StructuredContent) - } -} - func TestInputResponseMapJSON(t *testing.T) { tests := []struct { name string diff --git a/mcp/server_test.go b/mcp/server_test.go index d5e03f423..e94da50fd 100644 --- a/mcp/server_test.go +++ b/mcp/server_test.go @@ -822,44 +822,6 @@ func TestAddToolGenericNonObjectOutput(t *testing.T) { }) } -func TestCallToolStructuredContentPreservesLargeInteger(t *testing.T) { - ctx := context.Background() - server := NewServer(testImpl, nil) - server.AddTool(&Tool{ - Name: "large_integer", - InputSchema: &jsonschema.Schema{Type: "object"}, - }, func(context.Context, *CallToolRequest) (*CallToolResult, error) { - return &CallToolResult{ - Content: []Content{&TextContent{Text: "ok"}}, - StructuredContent: json.RawMessage(`{"id":9007199254740993}`), - }, nil - }) - - clientTransport, serverTransport := NewInMemoryTransports() - if _, err := server.Connect(ctx, serverTransport, nil); err != nil { - t.Fatal(err) - } - client := NewClient(testImpl, nil) - clientSession, err := client.Connect(ctx, clientTransport, nil) - if err != nil { - t.Fatal(err) - } - defer clientSession.Close() - - result, err := clientSession.CallTool(ctx, &CallToolParams{Name: "large_integer"}) - if err != nil { - t.Fatal(err) - } - structured, ok := result.StructuredContent.(map[string]any) - if !ok { - t.Fatalf("structured content has type %T, want map[string]any", result.StructuredContent) - } - got, ok := structured["id"].(json.Number) - if !ok || got.String() != "9007199254740993" { - t.Fatalf("structured content id = %#v (%T), want exact json.Number", structured["id"], structured["id"]) - } -} - // TestAddToolInputSchemaComposition verifies SEP-2106 (input side): composition // keywords such as oneOf are allowed on the input schema alongside // type:"object".