Skip to content

Commit 6b87b68

Browse files
committed
Record intent on span
Fix issues with middleware order Document
1 parent 221a400 commit 6b87b68

File tree

6 files changed

+88
-57
lines changed

6 files changed

+88
-57
lines changed

contrib/mark3labs/mcp-go/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ func main() {
2020
// Do not use with `server.WithHooks(...)`, as this overwrites the tracing hooks.
2121
// To add custom hooks alongside tracing, pass them via TracingConfig.Hooks, e.g.:
2222
// mcpgotrace.WithMCPServerTracing(&mcpgotrace.TracingConfig{Hooks: customHooks})
23+
24+
// To enable the capturing of intents on tool calls: `mcpgotrace.WithMCPServerTracing(&mcpgotrace.TracingConfig{IntentCaptureEnabled: true})`
25+
// Note that intent captures modifies modifies the tools schemas with an additional parameter than is removed before the tool is called.
2326
srv := server.NewMCPServer("my-server", "1.0.0",
2427
mcpgotrace.WithMCPServerTracing(nil))
2528
}
@@ -29,4 +32,7 @@ func main() {
2932

3033
The integration automatically traces:
3134
- **Tool calls**: Creates LLMObs tool spans with input/output annotation for all tool invocations
32-
- **Session initialization**: Create LLMObs task spans for session initialization, including client information.
35+
- **Session initialization**: Create LLMObs task spans for session initialization, including client information.
36+
37+
The integration can optionally capture "intent" on MCP tool calls. When enabled, this adds a parameter to the schema of each tool to request that the client include an explaination.
38+
This can help provide context in natural language about why tools are being used.

contrib/mark3labs/mcp-go/intent_capture.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ func injectDdtraceListToolsHook(ctx context.Context, id any, message *mcp.ListTo
6464
}
6565

6666
// Removing tracing parameters from the tool call request so its not sent to the tool.
67-
// This must be registered before the tool handler middleware, so that the span is available.
68-
// This must be registered after any user-defined middleware so that it is not visible to them.
67+
// This must be registered after the tool handler middleware (mcp-go runs middleware in registration order).
68+
// This removes the ddtrace parameter before user-defined middleware or tool handlers can see it.
6969
var processAndRemoveDdtraceToolMiddleware = func(next server.ToolHandlerFunc) server.ToolHandlerFunc {
7070
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
7171
if m, ok := request.Params.Arguments.(map[string]any); ok && m != nil {
@@ -108,6 +108,5 @@ func processDdtrace(ctx context.Context, m map[string]any) {
108108
if !ok {
109109
return
110110
}
111-
// TODO: Add fields to toolSpan to annotate intent
112-
_ = toolSpan
111+
toolSpan.Annotate(llmobs.WithIntent(intent))
113112
}

contrib/mark3labs/mcp-go/intent_capture_test.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,17 @@ import (
1010
"encoding/json"
1111
"testing"
1212

13-
"github.com/DataDog/dd-trace-go/v2/ddtrace/mocktracer"
1413
"github.com/mark3labs/mcp-go/mcp"
1514
"github.com/mark3labs/mcp-go/server"
1615
"github.com/stretchr/testify/assert"
1716
"github.com/stretchr/testify/require"
1817
)
1918

2019
func TestIntentCapture(t *testing.T) {
21-
mt := mocktracer.Start()
22-
defer mt.Stop()
20+
tt := testTracer(t)
21+
defer tt.Stop()
2322

24-
srv := server.NewMCPServer("test-server", "1.0.0", WithTracing(&TracingConfig{IntentCaptureEnabled: true}))
23+
srv := server.NewMCPServer("test-server", "1.0.0", WithMCPServerTracing(&TracingConfig{IntentCaptureEnabled: true}))
2524

2625
var receivedArgs map[string]any
2726
calcTool := mcp.NewTool("calculator",
@@ -71,14 +70,24 @@ func TestIntentCapture(t *testing.T) {
7170
session.Initialize()
7271
ctx = srv.WithContext(ctx, session)
7372

74-
srv.HandleMessage(ctx, []byte(`{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"calculator","arguments":{"operation":"add","x":5,"y":3,"ddtrace":{"intent":"test"}}}}`))
73+
srv.HandleMessage(ctx, []byte(`{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"calculator","arguments":{"operation":"add","x":5,"y":3,"ddtrace":{"intent":"test intent description"}}}}`))
7574

7675
// Ensure ddtrace is removed in tool call
7776
require.NotNil(t, receivedArgs)
7877
assert.Equal(t, "add", receivedArgs["operation"])
7978
assert.Equal(t, float64(5), receivedArgs["x"])
8079
assert.Equal(t, float64(3), receivedArgs["y"])
8180
assert.NotContains(t, receivedArgs, "ddtrace")
81+
82+
// Verify intent was recorded on the LLMObs span
83+
spans := tt.WaitForLLMObsSpans(t, 1)
84+
require.Len(t, spans, 1)
85+
86+
toolSpan := spans[0]
87+
assert.Equal(t, "tool", toolSpan.Meta["span.kind"])
88+
assert.Equal(t, "calculator", toolSpan.Name)
89+
assert.Contains(t, toolSpan.Meta, "intent")
90+
assert.Equal(t, "test intent description", toolSpan.Meta["intent"])
8291
}
8392

8493
func mustMarshal(v interface{}) []byte {

contrib/mark3labs/mcp-go/option.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ type TracingConfig struct {
1616
// Hooks allows you to provide custom hooks that will be merged with Datadog tracing hooks.
1717
// If nil, only Datadog tracing hooks will be added and any custom hooks provided via server.WithHooks(...) will be removed.
1818
// If provided, your custom hooks will be executed alongside Datadog tracing hooks.
19-
Hooks *server.Hooks
19+
Hooks *server.Hooks
20+
// Enables intent capture for tool spans.
21+
// This will modify the tool schemas to include a parameter for the client to provide the intent.
2022
IntentCaptureEnabled bool
2123
}
2224

@@ -55,12 +57,14 @@ func WithMCPServerTracing(options *TracingConfig) server.ServerOption {
5557

5658
server.WithHooks(hooks)(s)
5759

60+
// Register toolHandlerMiddleware first so it runs first (creates the span)
61+
// Note: mcp-go middleware runs in registration order (first registered runs first)
62+
server.WithToolHandlerMiddleware(toolHandlerMiddleware)(s)
63+
5864
if options.IntentCaptureEnabled {
5965
hooks.AddAfterListTools(injectDdtraceListToolsHook)
66+
// Register intent capture middleware second so it runs second (after span is created)
6067
server.WithToolHandlerMiddleware(processAndRemoveDdtraceToolMiddleware)(s)
6168
}
62-
63-
// This must be after the intent capture middleware, so that span in available. Last registered middleware is run first.
64-
server.WithToolHandlerMiddleware(toolHandlerMiddleware)(s)
6569
}
6670
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016 Datadog, Inc.
5+
6+
package mcpgo
7+
8+
import (
9+
"testing"
10+
11+
"github.com/DataDog/dd-trace-go/v2/ddtrace/tracer"
12+
"github.com/DataDog/dd-trace-go/v2/instrumentation/testutils/testtracer"
13+
"github.com/mark3labs/mcp-go/mcp"
14+
)
15+
16+
// testTracer creates a testtracer with LLMObs enabled for integration tests
17+
func testTracer(t *testing.T, opts ...testtracer.Option) *testtracer.TestTracer {
18+
defaultOpts := []testtracer.Option{
19+
testtracer.WithTracerStartOpts(
20+
tracer.WithLLMObsEnabled(true),
21+
tracer.WithLLMObsMLApp("test-mcp-app"),
22+
tracer.WithLogStartup(false),
23+
),
24+
testtracer.WithAgentInfoResponse(testtracer.AgentInfo{
25+
Endpoints: []string{"/evp_proxy/v2/"},
26+
}),
27+
}
28+
allOpts := append(defaultOpts, opts...)
29+
tt := testtracer.Start(t, allOpts...)
30+
t.Cleanup(tt.Stop)
31+
return tt
32+
}
33+
34+
// mockSession is a simple mock implementation of server.ClientSession for testing
35+
type mockSession struct {
36+
id string
37+
initialized bool
38+
notificationCh chan mcp.JSONRPCNotification
39+
}
40+
41+
func (m *mockSession) SessionID() string {
42+
return m.id
43+
}
44+
45+
func (m *mockSession) Initialize() {
46+
m.initialized = true
47+
m.notificationCh = make(chan mcp.JSONRPCNotification, 10)
48+
}
49+
50+
func (m *mockSession) Initialized() bool {
51+
return m.initialized
52+
}
53+
54+
func (m *mockSession) NotificationChannel() chan<- mcp.JSONRPCNotification {
55+
return m.notificationCh
56+
}

contrib/mark3labs/mcp-go/tracing_test.go

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"testing"
1414

1515
"github.com/DataDog/dd-trace-go/v2/ddtrace/mocktracer"
16-
"github.com/DataDog/dd-trace-go/v2/ddtrace/tracer"
1716
"github.com/DataDog/dd-trace-go/v2/instrumentation/testutils/testtracer"
1817
"github.com/mark3labs/mcp-go/mcp"
1918
"github.com/mark3labs/mcp-go/server"
@@ -369,45 +368,3 @@ func TestWithMCPServerTracingWithCustomHooks(t *testing.T) {
369368
}
370369

371370
// Test helpers
372-
373-
// testTracer creates a testtracer with LLMObs enabled for integration tests
374-
func testTracer(t *testing.T, opts ...testtracer.Option) *testtracer.TestTracer {
375-
defaultOpts := []testtracer.Option{
376-
testtracer.WithTracerStartOpts(
377-
tracer.WithLLMObsEnabled(true),
378-
tracer.WithLLMObsMLApp("test-mcp-app"),
379-
tracer.WithLogStartup(false),
380-
),
381-
testtracer.WithAgentInfoResponse(testtracer.AgentInfo{
382-
Endpoints: []string{"/evp_proxy/v2/"},
383-
}),
384-
}
385-
allOpts := append(defaultOpts, opts...)
386-
tt := testtracer.Start(t, allOpts...)
387-
t.Cleanup(tt.Stop)
388-
return tt
389-
}
390-
391-
// mockSession is a simple mock implementation of server.ClientSession for testing
392-
type mockSession struct {
393-
id string
394-
initialized bool
395-
notificationCh chan mcp.JSONRPCNotification
396-
}
397-
398-
func (m *mockSession) SessionID() string {
399-
return m.id
400-
}
401-
402-
func (m *mockSession) Initialize() {
403-
m.initialized = true
404-
m.notificationCh = make(chan mcp.JSONRPCNotification, 10)
405-
}
406-
407-
func (m *mockSession) Initialized() bool {
408-
return m.initialized
409-
}
410-
411-
func (m *mockSession) NotificationChannel() chan<- mcp.JSONRPCNotification {
412-
return m.notificationCh
413-
}

0 commit comments

Comments
 (0)