Skip to content

Commit 8d9b740

Browse files
feat(contrib/modelcontextprotocol/go-sdk): skip intent capture for UI-only tools (#4949)
This PR stack ports functionality added in the clone of this contrib in dd-source back to dd-trace-go (Rapid clone at `domains/api_platform/libs/go/mcp/tracing`). ## Summary Skips telemetry/intent injection for tools whose `_meta.ui.visibility` excludes `"model"`. UI-only tools cannot be invoked by the model, so a required `telemetry` parameter would be useless noise. `_meta.ui.visibility` is part of the MCP Apps extension (see https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/SEP-1865.md). Absent visibility list ⇒ model-callable (spec default). ## What this enables in dd-source Same behavior already shipping in the Rapid clone — UI-only tools registered through dd-source's MCP server are silently skipped today; this PR brings that to the public contrib. Source PR: DataDog/dd-source#420911 ## Test plan - [x] `TestIntentCaptureSkipsUIOnlyTools` — three tools (no meta, ui-only, dual-visibility): only the UI-only one is skipped. - [x] All existing intent-capture tests still pass. Stacked on #4948. --- Parallel port in the mark3labs/mcp-go contrib: #4942 Co-authored-by: jboolean <julian.boilen@datadoghq.com>
2 parents 0f3b417 + 90174ac commit 8d9b740

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

contrib/modelcontextprotocol/go-sdk/intent_capture.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ func intentCaptureReceivingMiddleware(next mcp.MethodHandler) mcp.MethodHandler
5656

5757
func injectToolsListResponse(res *mcp.ListToolsResult) {
5858
for i := range res.Tools {
59+
// UI-only tools cannot be invoked by the model, so injecting a
60+
// required telemetry parameter would be useless noise.
61+
if !instrmcp.IsModelCallable(res.Tools[i].Meta) {
62+
continue
63+
}
5964
// Round-trip the input schema through map[string]any so unknown JSON
6065
// Schema keywords (additionalProperties, oneOf, patternProperties, etc.)
6166
// that *jsonschema.Schema does not model pass through verbatim.

contrib/modelcontextprotocol/go-sdk/intent_capture_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,63 @@ func TestIntentCapturePreservesUnknownSchemaKeywords(t *testing.T) {
6767
assert.NotNil(t, schema["oneOf"])
6868
}
6969

70+
func TestIntentCaptureSkipsUIOnlyTools(t *testing.T) {
71+
// Tools whose _meta.ui.visibility omits "model" cannot be model-invoked, so
72+
// telemetry injection should be skipped for them.
73+
tt := testTracer(t)
74+
defer tt.Stop()
75+
ctx := context.Background()
76+
77+
server := mcp.NewServer(&mcp.Implementation{Name: "test-server", Version: "1.0.0"}, nil)
78+
AddTracing(server, WithIntentCapture())
79+
80+
addTool := func(name string, visibility []any) {
81+
tool := &mcp.Tool{
82+
Name: name,
83+
Description: name,
84+
InputSchema: &jsonschema.Schema{Type: "object", Properties: map[string]*jsonschema.Schema{"q": {Type: "string"}}},
85+
}
86+
if visibility != nil {
87+
tool.Meta = mcp.Meta{"ui": map[string]any{"visibility": visibility}}
88+
}
89+
server.AddTool(tool, func(_ context.Context, _ *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
90+
return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: "ok"}}}, nil
91+
})
92+
}
93+
addTool("plain", nil)
94+
addTool("ui_only", []any{"app"})
95+
addTool("dual", []any{"model", "app"})
96+
97+
client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "1.0.0"}, nil)
98+
clientTransport, serverTransport := mcp.NewInMemoryTransports()
99+
serverSession, err := server.Connect(ctx, serverTransport, nil)
100+
require.NoError(t, err)
101+
defer serverSession.Close()
102+
clientSession, err := client.Connect(ctx, clientTransport, nil)
103+
require.NoError(t, err)
104+
defer clientSession.Close()
105+
106+
listResult, err := clientSession.ListTools(ctx, &mcp.ListToolsParams{})
107+
require.NoError(t, err)
108+
props := func(name string) map[string]any {
109+
for _, tl := range listResult.Tools {
110+
if tl.Name != name {
111+
continue
112+
}
113+
schema, ok := tl.InputSchema.(map[string]any)
114+
if !ok {
115+
return nil
116+
}
117+
p, _ := schema["properties"].(map[string]any)
118+
return p
119+
}
120+
return nil
121+
}
122+
assert.Contains(t, props("plain"), "telemetry")
123+
assert.NotContains(t, props("ui_only"), "telemetry", "UI-only tool should not have telemetry injected")
124+
assert.Contains(t, props("dual"), "telemetry")
125+
}
126+
70127
func TestIntentCapture(t *testing.T) {
71128
tt := testTracer(t)
72129
defer tt.Stop()

0 commit comments

Comments
 (0)