Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions internal/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ func Unmarshal(data []byte, v any) error {
return NewDecoder(bytes.NewReader(data)).Decode(v)
}

func UnmarshalUseNumber(data []byte, v any) error {
if err := checkMaxDepth(data, defaultMaxDepth); err != nil {
return err
}
dec := NewDecoder(bytes.NewReader(data))
dec.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.
Expand Down
12 changes: 10 additions & 2 deletions mcp/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@ type CallToolResult struct {
// result of the tool call. Per SEP-2106, it may marshal to any valid JSON
// value (object, array, or primitive) conforming to the tool's
// [Tool.OutputSchema].
// Numbers received from the wire are represented as [json.Number] to preserve
// their exact values.
//
// When using a [ToolHandlerFor] with structured output, you should not
// populate this field. It will be automatically populated with the typed Out
Expand Down Expand Up @@ -391,12 +393,18 @@ 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"`
StructuredContent json.RawMessage `json:"structuredContent"`
ResultType resultType `json:"resultType"`
}
if err := internaljson.Unmarshal(data, &wire); err != nil {
return err
}
if len(wire.StructuredContent) > 0 {
if err := internaljson.UnmarshalUseNumber(wire.StructuredContent, &wire.res.StructuredContent); err != nil {
return err
}
}
var err error
if wire.res.Content, err = contentsFromWire(wire.Content, nil); err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion mcp/protocol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1285,7 +1285,7 @@ func TestContentUnmarshal(t *testing.T) {
Meta: Meta{"m": true},
Content: content,
IsError: true,
StructuredContent: 3.0,
StructuredContent: json.Number("3"),
}
var gotf CallToolResult
roundtrip(ctrf, &gotf)
Expand Down
50 changes: 48 additions & 2 deletions mcp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ func TestAddToolNonObjectOutputSchema(t *testing.T) {
name: "primitive number (map-based schema)",
outputSchema: map[string]any{"type": "number"},
content: 42.0,
want: 42.0,
want: json.Number("42"),
},
{
name: "primitive string (RawMessage schema)",
Expand Down Expand Up @@ -724,6 +724,52 @@ func TestAddToolNonObjectOutputSchema(t *testing.T) {
}
}

func TestCallToolStructuredContentPreservesLargeInteger(t *testing.T) {
const want int64 = 9007199254740993

server := NewServer(testImpl, nil)
server.AddTool(&Tool{
Name: "large-integer",
InputSchema: &jsonschema.Schema{Type: "object"},
}, func(context.Context, *CallToolRequest) (*CallToolResult, error) {
return &CallToolResult{StructuredContent: map[string]any{"id": want}}, nil
})

clientTransport, serverTransport := NewInMemoryTransports()
serverSession, err := server.Connect(context.Background(), serverTransport, nil)
if err != nil {
t.Fatal(err)
}
defer serverSession.Close()

client := NewClient(testImpl, nil)
clientSession, err := client.Connect(context.Background(), clientTransport, nil)
if err != nil {
t.Fatal(err)
}
defer clientSession.Close()

result, err := clientSession.CallTool(context.Background(), &CallToolParams{Name: "large-integer"})
if err != nil {
t.Fatal(err)
}
structured, ok := result.StructuredContent.(map[string]any)
if !ok {
t.Fatalf("StructuredContent type = %T, want map[string]any", result.StructuredContent)
}
number, ok := structured["id"].(json.Number)
if !ok {
t.Fatalf("id type = %T, want json.Number", structured["id"])
}
got, err := number.Int64()
if err != nil {
t.Fatal(err)
}
if got != want {
t.Errorf("id = %d, want %d", got, want)
}
}

// TestAddToolGenericNonObjectOutput verifies SEP-2106 for the generic
// AddTool[In, Out] helper: Out may be a slice or primitive whose inferred
// JSON Schema has a non-object root.
Expand Down Expand Up @@ -816,7 +862,7 @@ func TestAddToolGenericNonObjectOutput(t *testing.T) {
if res.IsError {
t.Fatalf("unexpected tool error: %v", res.Content)
}
if diff := cmp.Diff(float64(42), res.StructuredContent); diff != "" {
if diff := cmp.Diff(json.Number("42"), res.StructuredContent); diff != "" {
t.Errorf("structured content mismatch (-want +got):\n%s", diff)
}
})
Expand Down
10 changes: 8 additions & 2 deletions mcp/streamable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,14 @@ func TestStreamableConcurrentHandling(t *testing.T) {
t.Errorf("CallTool failed: %v", err)
return
}
if got := int(res.StructuredContent.(map[string]any)["Count"].(float64)); got != i {
t.Errorf("got count %d, want %d", got, i)
number, ok := res.StructuredContent.(map[string]any)["Count"].(json.Number)
if !ok {
t.Errorf("count type = %T, want json.Number", res.StructuredContent.(map[string]any)["Count"])
continue
}
got, err := number.Int64()
if err != nil || got != int64(i) {
t.Errorf("got count %d, %v; want %d", got, err, i)
}
}
})
Expand Down