diff --git a/internal/jsonrpc2/messages.go b/internal/jsonrpc2/messages.go index 8b9677061..dc445445c 100644 --- a/internal/jsonrpc2/messages.go +++ b/internal/jsonrpc2/messages.go @@ -19,8 +19,10 @@ type ID struct { value any } -// MakeID coerces the given Go value to an ID. The value should be the -// default JSON marshaling of a Request identifier: nil, float64, or string. +// MakeID coerces the given Go value to an ID. The value should be the JSON +// decoding of a Request identifier: nil, float64, json.Number, or string. +// Decoding with json.Number keeps integer IDs above 2^53 exact; a json.Number +// must be an integer literal in the int64 range. // // Returns an error if the value type was not a valid Request ID type. // @@ -33,6 +35,15 @@ func MakeID(v any) (ID, error) { return ID{}, nil case float64: return Int64ID(int64(v)), nil + case json.Number: + if i, err := v.Int64(); err == nil { + return Int64ID(i), nil + } + // Fallback to maintain backward compatibility + if f, err := v.Float64(); err == nil { + return Int64ID(int64(f)), nil + } + return ID{}, fmt.Errorf("%w: invalid numeric ID %q", ErrParse, v) case string: return StringID(v), nil } @@ -185,7 +196,9 @@ type wireDecode struct { func DecodeMessage(data []byte) (Message, error) { msg := wireDecode{} - if err := internaljson.Unmarshal(data, &msg); err != nil { + // UseNumber keeps a numeric ID exact (see MakeID). The other fields are + // raw or typed, so it affects nothing else. + if err := internaljson.UnmarshalUseNumber(data, &msg); err != nil { return nil, fmt.Errorf("unmarshaling jsonrpc message: %w", err) } if msg.VersionTag != wireVersion { diff --git a/internal/jsonrpc2/wire_test.go b/internal/jsonrpc2/wire_test.go index ef4714405..9fdd49959 100644 --- a/internal/jsonrpc2/wire_test.go +++ b/internal/jsonrpc2/wire_test.go @@ -148,6 +148,48 @@ func TestDecodeIDOnlyMessageIsResponse(t *testing.T) { } } +// TestDecodeIntegerIDs checks that numeric IDs decode exactly, including +// integers above 2^53 that a float64 cannot hold, and that a numeric ID that +// is not an int64 integer literal is rejected. +func TestDecodeIntegerIDs(t *testing.T) { + for _, tc := range []struct { + wire string + want jsonrpc2.ID + }{ + {`9007199254740992`, jsonrpc2.Int64ID(9007199254740992)}, + {`9007199254740993`, jsonrpc2.Int64ID(9007199254740993)}, + {`9223372036854775807`, jsonrpc2.Int64ID(9223372036854775807)}, + {`-9223372036854775808`, jsonrpc2.Int64ID(-9223372036854775808)}, + {`"9007199254740993"`, jsonrpc2.StringID("9007199254740993")}, + } { + for _, encoded := range []string{ + `{"jsonrpc":"2.0","id":` + tc.wire + `,"method":"m"}`, + `{"jsonrpc":"2.0","id":` + tc.wire + `,"result":{}}`, + } { + msg, err := jsonrpc2.DecodeMessage([]byte(encoded)) + if err != nil { + t.Fatalf("DecodeMessage(%s): %v", encoded, err) + } + var got jsonrpc2.ID + switch msg := msg.(type) { + case *jsonrpc2.Request: + got = msg.ID + case *jsonrpc2.Response: + got = msg.ID + } + if got != tc.want { + t.Errorf("DecodeMessage(%s) ID = %v, want %v", encoded, got.Raw(), tc.want.Raw()) + } + // A response echoes the request ID, so it must encode back unchanged. + reencoded, err := jsonrpc2.EncodeMessage(msg) + if err != nil { + t.Fatal(err) + } + checkJSON(t, reencoded, []byte(encoded)) + } + } +} + // TestWireErrorIs checks the errors.Is behavior of WireError, including that // comparing against a typed-nil *WireError does not panic. func TestWireErrorIs(t *testing.T) { diff --git a/jsonrpc/jsonrpc.go b/jsonrpc/jsonrpc.go index a9ea78fa8..bbc2efe4d 100644 --- a/jsonrpc/jsonrpc.go +++ b/jsonrpc/jsonrpc.go @@ -21,8 +21,10 @@ type ( Error = jsonrpc2.WireError ) -// MakeID coerces the given Go value to an ID. The value should be the -// default JSON marshaling of a Request identifier: nil, float64, or string. +// MakeID coerces the given Go value to an ID. The value should be the JSON +// decoding of a Request identifier: nil, float64, json.Number, or string. +// Decoding with json.Number keeps integer IDs above 2^53 exact; a json.Number +// must be an integer literal in the int64 range. // // Returns an error if the value type was not a valid Request ID type. func MakeID(v any) (ID, error) { diff --git a/mcp/transport.go b/mcp/transport.go index d2423d9dd..e7414d699 100644 --- a/mcp/transport.go +++ b/mcp/transport.go @@ -260,7 +260,8 @@ type canceller struct { func (c *canceller) Preempt(ctx context.Context, req *jsonrpc.Request) (result any, err error) { if req.Method == notificationCancelled { var params CancelledParams - if err := internaljson.Unmarshal(req.Params, ¶ms); err != nil { + // UseNumber keeps a numeric requestId exact, as in jsonrpc2.DecodeMessage. + if err := internaljson.UnmarshalUseNumber(req.Params, ¶ms); err != nil { return nil, err } id, err := jsonrpc2.MakeID(params.RequestID)