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
6 changes: 6 additions & 0 deletions internal/jsonrpc2/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math"

internaljson "github.com/modelcontextprotocol/go-sdk/internal/json"
)
Expand All @@ -32,6 +33,11 @@ func MakeID(v any) (ID, error) {
case nil:
return ID{}, nil
case float64:
// JSON-RPC request IDs are integers; reject fractional or out-of-range
// values instead of silently truncating them.
if v != math.Trunc(v) || v >= 9.223372036854775808e18 || v < -9.223372036854775808e18 {
return ID{}, fmt.Errorf("%w: request id must be an integer, got %v", ErrParse, v)
}
return Int64ID(int64(v)), nil
case string:
return StringID(v), nil
Expand Down
48 changes: 48 additions & 0 deletions internal/jsonrpc2/wire_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,54 @@ func TestDecodeResponseUnchanged(t *testing.T) {
}
}

func TestMakeIDRejectsNonInteger(t *testing.T) {
for _, test := range []struct {
name string
id any
}{
{name: "fractional", id: 1.9},
{name: "half", id: 2.5},
{name: "above int64 range", id: 9.3e18},
{name: "below int64 range", id: -9.3e18},
} {
t.Run(test.name, func(t *testing.T) {
if _, err := jsonrpc2.MakeID(test.id); err == nil {
t.Errorf("MakeID(%v) = nil error, want ErrParse", test.id)
}
})
}
}

func TestMakeIDAcceptsInteger(t *testing.T) {
for _, test := range []struct {
name string
id any
want int64
}{
{name: "zero", id: float64(0), want: 0},
{name: "positive", id: float64(42), want: 42},
{name: "negative", id: float64(-7), want: -7},
{name: "max exact float64 integer", id: float64(1 << 53), want: 1 << 53},
} {
t.Run(test.name, func(t *testing.T) {
id, err := jsonrpc2.MakeID(test.id)
if err != nil {
t.Fatalf("MakeID(%v) = error: %v", test.id, err)
}
if got := id.Raw(); got != test.want {
t.Errorf("MakeID(%v).Raw() = %v, want %v", test.id, got, test.want)
}
})
}
}

func TestDecodeMessageRejectsFractionalID(t *testing.T) {
encoded := []byte(`{"jsonrpc":"2.0","id":1.9,"method":"ping"}`)
if _, err := jsonrpc2.DecodeMessage(encoded); err == nil {
t.Fatal("DecodeMessage with fractional id = nil error, want error")
}
}

// Messages with an id but no "method" key are responses, not malformed requests.
func TestDecodeIDOnlyMessageIsResponse(t *testing.T) {
encoded := []byte(`{"jsonrpc":"2.0","id":5}`)
Expand Down