Skip to content

Commit 85d8a90

Browse files
committed
Fix concurrent writes bug in intent capture
1 parent 44f4456 commit 85d8a90

File tree

2 files changed

+42
-1
lines changed

2 files changed

+42
-1
lines changed

contrib/mark3labs/mcp-go/intent_capture.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package mcpgo
77

88
import (
99
"context"
10+
"maps"
1011
"slices"
1112

1213
instrmcp "github.com/DataDog/dd-trace-go/v2/instrumentation/mcp"
@@ -35,6 +36,9 @@ func injectTelemetryListToolsHook(ctx context.Context, id any, message *mcp.List
3536
return
3637
}
3738

39+
// The server reuses tools across requests. Slices and nested objects are cloned to avoid concurrent writes.
40+
result.Tools = slices.Clone(result.Tools)
41+
3842
for i := range result.Tools {
3943
t := &result.Tools[i]
4044

@@ -48,14 +52,16 @@ func injectTelemetryListToolsHook(ctx context.Context, id any, message *mcp.List
4852
}
4953
if t.InputSchema.Properties == nil {
5054
t.InputSchema.Properties = map[string]any{}
55+
} else {
56+
t.InputSchema.Properties = maps.Clone(t.InputSchema.Properties)
5157
}
5258

5359
// Insert/overwrite the telemetry property
5460
t.InputSchema.Properties[instrmcp.TelemetryKey] = telemetrySchema()
5561

5662
// Mark telemetry as required (idempotent)
5763
if !slices.Contains(t.InputSchema.Required, instrmcp.TelemetryKey) {
58-
t.InputSchema.Required = append(t.InputSchema.Required, instrmcp.TelemetryKey)
64+
t.InputSchema.Required = append(slices.Clone(t.InputSchema.Required), instrmcp.TelemetryKey)
5965
}
6066
}
6167
}

contrib/mark3labs/mcp-go/intent_capture_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,41 @@ func TestIntentCapture(t *testing.T) {
9191
assert.Equal(t, "test intent description", toolSpan.Meta["intent"])
9292
}
9393

94+
func TestIntentCaptureConcurrentListTools(t *testing.T) {
95+
tt := testTracer(t)
96+
defer tt.Stop()
97+
98+
srv := server.NewMCPServer("test-server", "1.0.0", WithMCPServerTracing(&TracingConfig{IntentCaptureEnabled: true}))
99+
100+
calcTool := mcp.NewTool("calculator",
101+
mcp.WithDescription("A simple calculator"),
102+
mcp.WithString("operation", mcp.Required(), mcp.Description("The operation to perform")),
103+
mcp.WithNumber("x", mcp.Required(), mcp.Description("First number")),
104+
mcp.WithNumber("y", mcp.Required(), mcp.Description("Second number")))
105+
106+
srv.AddTool(calcTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
107+
return mcp.NewToolResultText(`{"result":8}`), nil
108+
})
109+
110+
ctx := context.Background()
111+
112+
const numGoroutines = 10
113+
done := make(chan struct{})
114+
115+
for i := 0; i < numGoroutines; i++ {
116+
go func() {
117+
defer func() { done <- struct{}{} }()
118+
for j := 0; j < 100; j++ {
119+
srv.HandleMessage(ctx, []byte(`{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}`))
120+
}
121+
}()
122+
}
123+
124+
for i := 0; i < numGoroutines; i++ {
125+
<-done
126+
}
127+
}
128+
94129
func mustMarshal(v interface{}) []byte {
95130
b, _ := json.Marshal(v)
96131
return b

0 commit comments

Comments
 (0)