go-sdk/internal/json/json.go
Luca Candela 3785c500b5
mcp: preserve structured content number precision (#1239)
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>
2026-09-16 09:22:16 +02:00

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
}