Skip to content

Commit 6e4c4b7

Browse files
committed
feat(contrib/modelcontextprotocol/go-sdk): support per-request intent capture predicate
1 parent ccb716c commit 6e4c4b7

4 files changed

Lines changed: 114 additions & 23 deletions

File tree

contrib/modelcontextprotocol/go-sdk/intent_capture.go

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,37 @@ func telemetrySchema() map[string]any {
2929
}
3030
}
3131

32-
// intentCaptureReceivingMiddleware is an mcp.Server receiving middleware
33-
// adding intent information to the tool call span.
34-
// Intent capture works by injecting an additional required parameter on tools that the
35-
// client agent will fill in to explain context about the task.
36-
// The middleware records this intent on the span, and then removes it from the arguments before the tool is called.
37-
func intentCaptureReceivingMiddleware(next mcp.MethodHandler) mcp.MethodHandler {
38-
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
39-
switch method {
40-
case "tools/list":
41-
// The additional parameter is added to the tool arguments returned by tools/list.
42-
res, err := next(ctx, method, req)
43-
if toolListRes, ok := res.(*mcp.ListToolsResult); ok {
44-
injectToolsListResponse(toolListRes)
32+
// intentCaptureReceivingMiddlewareFor returns an mcp.Server receiving
33+
// middleware that adds intent information to the tool call span, gated
34+
// per-request by the supplied predicate. When the predicate returns false,
35+
// the request passes through untouched.
36+
//
37+
// Intent capture works by injecting an additional required parameter on
38+
// tools that the client agent will fill in to explain context about the
39+
// task. The middleware records this intent on the span, and then removes
40+
// it from the arguments before the tool is called.
41+
func intentCaptureReceivingMiddlewareFor(enabled func(context.Context) bool) func(mcp.MethodHandler) mcp.MethodHandler {
42+
return func(next mcp.MethodHandler) mcp.MethodHandler {
43+
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
44+
if !enabled(ctx) {
45+
return next(ctx, method, req)
4546
}
46-
return res, err
47-
case "tools/call":
48-
// The intent is recorded and the argument is removed.
49-
if toolReq, ok := req.(*mcp.CallToolRequest); ok {
50-
return processToolCallIntent(next, ctx, method, toolReq)
47+
switch method {
48+
case "tools/list":
49+
// The additional parameter is added to the tool arguments returned by tools/list.
50+
res, err := next(ctx, method, req)
51+
if toolListRes, ok := res.(*mcp.ListToolsResult); ok {
52+
injectToolsListResponse(toolListRes)
53+
}
54+
return res, err
55+
case "tools/call":
56+
// The intent is recorded and the argument is removed.
57+
if toolReq, ok := req.(*mcp.CallToolRequest); ok {
58+
return processToolCallIntent(next, ctx, method, toolReq)
59+
}
5160
}
61+
return next(ctx, method, req)
5262
}
53-
return next(ctx, method, req)
5463
}
5564
}
5665

contrib/modelcontextprotocol/go-sdk/intent_capture_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,73 @@ import (
1919
"github.com/DataDog/dd-trace-go/v2/instrumentation/testutils/testtracer"
2020
)
2121

22+
func TestIntentCapturePredicate(t *testing.T) {
23+
// The predicate must gate per-request: when it returns false, no schema
24+
// injection and the telemetry argument reaches the handler.
25+
tt := testTracer(t)
26+
defer tt.Stop()
27+
ctx := context.Background()
28+
29+
var enabled bool
30+
server := mcp.NewServer(&mcp.Implementation{Name: "test-server", Version: "1.0.0"}, nil)
31+
AddTracing(server, WithIntentCapturePredicate(func(context.Context) bool {
32+
return enabled
33+
}))
34+
35+
var receivedArgs map[string]any
36+
server.AddTool(&mcp.Tool{
37+
Name: "tool",
38+
Description: "tool",
39+
InputSchema: &jsonschema.Schema{Type: "object", Properties: map[string]*jsonschema.Schema{"q": {Type: "string"}}},
40+
}, func(_ context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
41+
_ = json.Unmarshal(req.Params.Arguments, &receivedArgs)
42+
return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: "ok"}}}, nil
43+
})
44+
45+
client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "1.0.0"}, nil)
46+
clientTransport, serverTransport := mcp.NewInMemoryTransports()
47+
serverSession, err := server.Connect(ctx, serverTransport, nil)
48+
require.NoError(t, err)
49+
defer serverSession.Close()
50+
clientSession, err := client.Connect(ctx, clientTransport, nil)
51+
require.NoError(t, err)
52+
defer clientSession.Close()
53+
54+
// Predicate false: schema not injected, telemetry argument passed through.
55+
enabled = false
56+
listResult, err := clientSession.ListTools(ctx, &mcp.ListToolsParams{})
57+
require.NoError(t, err)
58+
require.Len(t, listResult.Tools, 1)
59+
if schema, ok := listResult.Tools[0].InputSchema.(map[string]any); ok {
60+
if props, _ := schema["properties"].(map[string]any); props != nil {
61+
assert.NotContains(t, props, "telemetry")
62+
}
63+
}
64+
65+
_, err = clientSession.CallTool(ctx, &mcp.CallToolParams{
66+
Name: "tool",
67+
Arguments: map[string]any{"q": "x", "telemetry": map[string]any{"intent": "ignored"}},
68+
})
69+
require.NoError(t, err)
70+
assert.Contains(t, receivedArgs, "telemetry", "telemetry should pass through when predicate is false")
71+
72+
// Predicate true: schema injected, telemetry stripped.
73+
enabled = true
74+
listResult, err = clientSession.ListTools(ctx, &mcp.ListToolsParams{})
75+
require.NoError(t, err)
76+
schema, ok := listResult.Tools[0].InputSchema.(map[string]any)
77+
require.True(t, ok)
78+
assert.Contains(t, schema["properties"].(map[string]any), "telemetry")
79+
80+
receivedArgs = nil
81+
_, err = clientSession.CallTool(ctx, &mcp.CallToolParams{
82+
Name: "tool",
83+
Arguments: map[string]any{"q": "x", "telemetry": map[string]any{"intent": "captured"}},
84+
})
85+
require.NoError(t, err)
86+
assert.NotContains(t, receivedArgs, "telemetry")
87+
}
88+
2289
func TestIntentCapturePreservesUnknownSchemaKeywords(t *testing.T) {
2390
// *jsonschema.Schema doesn't model additionalProperties/oneOf; the map-based
2491
// injection must pass those through verbatim instead of dropping them.

contrib/modelcontextprotocol/go-sdk/option.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,29 @@
55

66
package gosdk
77

8+
import "context"
9+
810
type config struct {
9-
intentCaptureEnabled bool
11+
intentCapturePredicate func(context.Context) bool
1012
}
1113

1214
type Option func(*config)
1315

16+
// WithIntentCapture enables intent capture for all tool calls.
17+
//
18+
// For per-request control (e.g. driven by a feature flag), use
19+
// WithIntentCapturePredicate instead.
1420
func WithIntentCapture() Option {
21+
return WithIntentCapturePredicate(func(context.Context) bool { return true })
22+
}
23+
24+
// WithIntentCapturePredicate enables intent capture and gates it per-request
25+
// using the supplied predicate. The predicate is consulted on every tools/list
26+
// and tools/call: when it returns false, the request behaves as if intent
27+
// capture were disabled (no schema injection, no telemetry stripping, no
28+
// intent annotation).
29+
func WithIntentCapturePredicate(predicate func(context.Context) bool) Option {
1530
return func(cfg *config) {
16-
cfg.intentCaptureEnabled = true
31+
cfg.intentCapturePredicate = predicate
1732
}
1833
}

contrib/modelcontextprotocol/go-sdk/tracing.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ func AddTracing(server *mcp.Server, opts ...Option) {
2727
middlewares := []mcp.Middleware{tracingMiddleware}
2828

2929
// Intent capture is added after tracing so that the intent can be annotated on the existing span.
30-
if cfg.intentCaptureEnabled {
31-
middlewares = append(middlewares, intentCaptureReceivingMiddleware)
30+
if cfg.intentCapturePredicate != nil {
31+
middlewares = append(middlewares, intentCaptureReceivingMiddlewareFor(cfg.intentCapturePredicate))
3232
}
3333

3434
server.AddReceivingMiddleware(middlewares...)

0 commit comments

Comments
 (0)