From 2612452b7875686abd7ded496755fadb762d7561 Mon Sep 17 00:00:00 2001 From: Sergey Kosovsky Date: Tue, 13 Oct 2026 01:47:42 +0700 Subject: [PATCH 1/2] jsonrpc2: avoid streaming decoder allocation for method strings --- internal/jsonrpc2/messages.go | 4 +- internal/jsonrpc2/method_decode_test.go | 89 +++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 internal/jsonrpc2/method_decode_test.go diff --git a/internal/jsonrpc2/messages.go b/internal/jsonrpc2/messages.go index 8b9677061..611b0cc30 100644 --- a/internal/jsonrpc2/messages.go +++ b/internal/jsonrpc2/messages.go @@ -198,7 +198,9 @@ func DecodeMessage(data []byte) (Message, error) { if len(msg.Method) > 0 { // The "method" key was present. Decode its value (including ""). var method string - if err := internaljson.Unmarshal(msg.Method, &method); err != nil { + // The envelope is already depth-checked and field names matched exactly. + // A scalar needs no streaming decoder and its 32 KiB buffer. + if err := json.Unmarshal(msg.Method, &method); err != nil { return nil, fmt.Errorf("unmarshaling jsonrpc message: %w", err) } return &Request{ diff --git a/internal/jsonrpc2/method_decode_test.go b/internal/jsonrpc2/method_decode_test.go new file mode 100644 index 000000000..192bf409c --- /dev/null +++ b/internal/jsonrpc2/method_decode_test.go @@ -0,0 +1,89 @@ +// 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 jsonrpc2 + +import ( + "strings" + "testing" +) + +func TestDecodeMethodValue(t *testing.T) { + for _, tt := range []struct { + name, value, want string + invalid bool + }{ + {"plain", `"tools/call"`, "tools/call", false}, + {"empty", `""`, "", false}, + {"escaped", `"tools\u002fcall"`, "tools/call", false}, + {"unicode", `"\ud83d\ude00"`, "😀", false}, + {"null", `null`, "", false}, + {"number", `42`, "", true}, + {"boolean", `true`, "", true}, + {"object", `{}`, "", true}, + {"array", `[]`, "", true}, + } { + t.Run(tt.name, func(t *testing.T) { + // Arrange. + wire := []byte(`{"jsonrpc":"2.0","id":1,"method":` + tt.value + `}`) + // Act. + msg, err := DecodeMessage(wire) + // Assert. + if tt.invalid { + if err == nil { + t.Fatal("expected invalid method error") + } + return + } + if err != nil { + t.Fatal(err) + } + req, ok := msg.(*Request) + if !ok || req.Method != tt.want { + t.Fatalf("decoded message = %#v, want request method %q", msg, tt.want) + } + }) + } +} + +func TestDecodeMethodEnvelopeGuards(t *testing.T) { + t.Run("case sensitive field", func(t *testing.T) { + // Arrange. + wire := []byte(`{"jsonrpc":"2.0","id":1,"Method":"ping","result":{}}`) + // Act. + msg, err := DecodeMessage(wire) + // Assert. + if err != nil { + t.Fatal(err) + } + if _, ok := msg.(*Response); !ok { + t.Fatalf("decoded %T, want response", msg) + } + }) + t.Run("depth limit", func(t *testing.T) { + // Arrange. + wire := []byte(`{"jsonrpc":"2.0","id":1,"method":` + strings.Repeat("[", 1001) + `0` + strings.Repeat("]", 1001) + `}`) + // Act. + _, err := DecodeMessage(wire) + // Assert. + if err == nil || !strings.Contains(err.Error(), "maximum nesting depth") { + t.Fatalf("error = %v, want depth rejection", err) + } + }) +} + +func BenchmarkDecodeMethodRequest(b *testing.B) { + for _, method := range []string{`"tools/call"`, `"tools\u002fcall"`} { + b.Run(method, func(b *testing.B) { + wire := []byte(`{"jsonrpc":"2.0","id":1,"method":` + method + `,"params":{}}`) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := DecodeMessage(wire); err != nil { + b.Fatal(err) + } + } + }) + } +} From d8603e9c2a8f1e8b9e185f39851643081a5606d2 Mon Sep 17 00:00:00 2001 From: Sergey Kosovsky <132824114+skosovsky@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:00:30 +0700 Subject: [PATCH 2/2] test(jsonrpc2): remove Arrange/Act/Assert comments --- internal/jsonrpc2/method_decode_test.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/internal/jsonrpc2/method_decode_test.go b/internal/jsonrpc2/method_decode_test.go index 192bf409c..ca76f1271 100644 --- a/internal/jsonrpc2/method_decode_test.go +++ b/internal/jsonrpc2/method_decode_test.go @@ -25,11 +25,8 @@ func TestDecodeMethodValue(t *testing.T) { {"array", `[]`, "", true}, } { t.Run(tt.name, func(t *testing.T) { - // Arrange. wire := []byte(`{"jsonrpc":"2.0","id":1,"method":` + tt.value + `}`) - // Act. msg, err := DecodeMessage(wire) - // Assert. if tt.invalid { if err == nil { t.Fatal("expected invalid method error") @@ -49,11 +46,8 @@ func TestDecodeMethodValue(t *testing.T) { func TestDecodeMethodEnvelopeGuards(t *testing.T) { t.Run("case sensitive field", func(t *testing.T) { - // Arrange. wire := []byte(`{"jsonrpc":"2.0","id":1,"Method":"ping","result":{}}`) - // Act. msg, err := DecodeMessage(wire) - // Assert. if err != nil { t.Fatal(err) } @@ -62,11 +56,8 @@ func TestDecodeMethodEnvelopeGuards(t *testing.T) { } }) t.Run("depth limit", func(t *testing.T) { - // Arrange. wire := []byte(`{"jsonrpc":"2.0","id":1,"method":` + strings.Repeat("[", 1001) + `0` + strings.Repeat("]", 1001) + `}`) - // Act. _, err := DecodeMessage(wire) - // Assert. if err == nil || !strings.Contains(err.Error(), "maximum nesting depth") { t.Fatalf("error = %v, want depth rejection", err) }