diff --git a/mcp/client.go b/mcp/client.go index f81d0c14..8d2b37aa 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -549,8 +549,11 @@ func injectRequestMeta[T any, P interface { res := cs.state.InitializeResult if params == nil { params = new(T) + } else { + copy := *params + params = © } - m := params.GetMeta() + m := maps.Clone(params.GetMeta()) if m == nil { m = map[string]any{} } @@ -1380,6 +1383,8 @@ func (cs *ClientSession) CallTool(ctx context.Context, params *CallToolParams) ( } if params.Arguments == nil { // Avoid sending nil over the wire. + copy := *params + params = © params.Arguments = map[string]any{} } if tool := cs.lookupTool(params.Name); tool != nil { diff --git a/mcp/client_test.go b/mcp/client_test.go index bc6dc86e..12bf52e7 100644 --- a/mcp/client_test.go +++ b/mcp/client_test.go @@ -12,7 +12,9 @@ import ( "errors" "fmt" "log/slog" + "maps" "slices" + "sync" "sync/atomic" "testing" @@ -1396,3 +1398,116 @@ func TestListRootsCapabilityDisabled(t *testing.T) { }) } } + +func TestInjectRequestMetaPreservesParams(t *testing.T) { + cs := &ClientSession{ + client: NewClient(testImpl, nil), + state: clientSessionState{InitializeResult: &InitializeResult{ + ProtocolVersion: protocolVersion20260728, + }}, + } + for _, meta := range []Meta{ + nil, + {}, + {"custom": "value"}, + {MetaKeyProtocolVersion: "override", MetaKeyClientInfo: "client", MetaKeyClientCapabilities: "capabilities"}, + } { + params := &GetPromptParams{Name: "prompt", Meta: meta} + wantMeta := maps.Clone(meta) + got := injectRequestMeta(cs, params) + if diff := cmp.Diff(wantMeta, params.Meta); diff != "" { + t.Errorf("caller metadata changed (-want +got):\n%s", diff) + } + if got == params { + t.Error("injectRequestMeta returned caller-owned params") + } + if got.Name != params.Name { + t.Error("prompt name changed") + } + for _, key := range []string{MetaKeyProtocolVersion, MetaKeyClientInfo, MetaKeyClientCapabilities} { + if _, ok := got.Meta[key]; !ok { + t.Errorf("missing metadata key %q", key) + } + if value, ok := wantMeta[key]; ok && !cmp.Equal(value, got.Meta[key]) { + t.Errorf("explicit metadata key %q was overwritten", key) + } + } + got.Meta["custom"] = "changed" + if diff := cmp.Diff(wantMeta, params.Meta); diff != "" { + t.Errorf("metadata map is shared (-want +got):\n%s", diff) + } + } + if got := injectRequestMeta(cs, (*GetPromptParams)(nil)); got == nil || got.Meta == nil { + t.Error("nil params did not receive metadata") + } +} + +func TestCallToolPreservesSharedParams(t *testing.T) { + for _, version := range []string{protocolVersion20251125, protocolVersion20260728} { + t.Run(version, func(t *testing.T) { + ctx := t.Context() + s := NewServer(testImpl, nil) + s.AddTool(&Tool{Name: "ping", InputSchema: map[string]any{"type": "object"}}, + func(_ context.Context, req *CallToolRequest) (*CallToolResult, error) { + name := "legacy" + if info, ok := req.Params.Meta[MetaKeyClientInfo].(map[string]any); ok { + name, _ = info["name"].(string) + } + return &CallToolResult{Content: []Content{&TextContent{Text: name}}}, nil + }) + var clients []*ClientSession + for _, name := range []string{"client-a", "client-b"} { + ct, st := NewInMemoryTransports() + ss, err := s.Connect(ctx, st, nil) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = ss.Close() }) + cs, err := NewClient(&Implementation{Name: name, Version: "1"}, nil).Connect(ctx, ct, + &ClientSessionOptions{ProtocolVersion: version}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = cs.Close() }) + clients = append(clients, cs) + } + call := func(cs *ClientSession, params *CallToolParams) { + t.Helper() + result, err := cs.CallTool(ctx, params) + if err != nil { + t.Error(err) + return + } + want := "legacy" + if version == protocolVersion20260728 { + want = cs.client.impl.Name + } + if got := result.Content[0].(*TextContent).Text; got != want { + t.Errorf("server received client name %q, want %q", got, want) + } + } + for _, concurrent := range []bool{false, true} { + for _, meta := range []Meta{nil, {"custom": "value"}} { + params := &CallToolParams{Name: "ping", Meta: meta} + want := &CallToolParams{Name: "ping", Meta: maps.Clone(meta)} + if concurrent { + var wg sync.WaitGroup + for range 4 { + for _, cs := range clients { + wg.Go(func() { call(cs, params) }) + } + } + wg.Wait() + } else { + for _, cs := range clients { + call(cs, params) + } + } + if diff := cmp.Diff(want, params); diff != "" { + t.Errorf("caller params changed (-want +got):\n%s", diff) + } + } + } + }) + } +}