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: - gofmt -l . - go test ./... - go vet ./... Fixes #1236 --------- Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
93 lines
2.1 KiB
Go
93 lines
2.1 KiB
Go
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
|
|
// Use of this source code is governed by the license
|
|
// that can be found in the LICENSE file.
|
|
|
|
// Package json provides internal JSON utilities.
|
|
|
|
package json
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/segmentio/encoding/json"
|
|
)
|
|
|
|
// defaultMaxDepth is the maximum JSON nesting depth accepted by [Unmarshal].
|
|
// Realistic MCP payloads nest only a handful of levels, so 1000 is far more
|
|
// than any legitimate message requires while keeping worst-case parse cost
|
|
// bounded.
|
|
const defaultMaxDepth = 1000
|
|
|
|
// errMaxDepthExceeded is returned by [Unmarshal] when the input nests deeper than [defaultMaxDepth].
|
|
var errMaxDepthExceeded = fmt.Errorf("json: exceeded maximum nesting depth of %d", defaultMaxDepth)
|
|
|
|
type Decoder struct {
|
|
dec *json.Decoder
|
|
}
|
|
|
|
func NewDecoder(r io.Reader) *Decoder {
|
|
dec := json.NewDecoder(r)
|
|
dec.DontMatchCaseInsensitiveStructFields()
|
|
return &Decoder{dec: dec}
|
|
}
|
|
|
|
func (d *Decoder) Decode(v any) error {
|
|
return d.dec.Decode(v)
|
|
}
|
|
|
|
func Unmarshal(data []byte, v any) error {
|
|
if err := checkMaxDepth(data, defaultMaxDepth); err != nil {
|
|
return err
|
|
}
|
|
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.
|
|
func checkMaxDepth(data []byte, maxDepth int) error {
|
|
var (
|
|
depth int
|
|
inString bool
|
|
escaped bool
|
|
)
|
|
for _, b := range data {
|
|
if inString {
|
|
switch {
|
|
case escaped:
|
|
escaped = false
|
|
case b == '\\':
|
|
escaped = true
|
|
case b == '"':
|
|
inString = false
|
|
}
|
|
continue
|
|
}
|
|
|
|
switch b {
|
|
case '"':
|
|
inString = true
|
|
case '{', '[':
|
|
depth++
|
|
if depth > maxDepth {
|
|
return errMaxDepthExceeded
|
|
}
|
|
case '}', ']':
|
|
if depth > 0 {
|
|
depth--
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|