Skip to content

Commit a2b4f0a

Browse files
committed
fix(contrib/mark3labs/mcp-go): preserve raw input schema fields on intent injection
Tools defined with NewToolWithRawSchema may declare JSON Schema keywords that mcp.ToolInputSchema doesn't model (additionalProperties, oneOf, allOf, patternProperties, pattern, enum, minimum, etc.). The previous intent- capture unmarshal-into-ToolInputSchema path silently dropped those fields, causing tools/list to advertise a more permissive schema than the tool author specified whenever intent capture was enabled. Inject telemetry into the raw JSON via a generic map[string]any so unknown top-level keywords pass through verbatim. The struct-based path is kept for tools that don't use RawInputSchema. Same bug existed in dd-source #413963 and is now fixed in dd-trace-go. Reported by chatgpt-codex-connector on PR #4939.
1 parent b88a735 commit a2b4f0a

2 files changed

Lines changed: 67 additions & 5 deletions

File tree

contrib/mark3labs/mcp-go/intent_capture.go

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,15 @@ func injectTelemetryListToolsHook(ctx context.Context, id any, message *mcp.List
4444
for i := range result.Tools {
4545
t := &result.Tools[i]
4646

47+
// mcp.ToolInputSchema only models type/properties/required/$defs;
48+
// for tools defined with NewToolWithRawSchema we mutate the raw
49+
// JSON via a generic map so keywords like additionalProperties,
50+
// oneOf, or patternProperties pass through verbatim.
4751
if t.RawInputSchema != nil {
48-
var schema mcp.ToolInputSchema
49-
if err := json.Unmarshal(t.RawInputSchema, &schema); err != nil {
50-
continue
52+
if newRaw, ok := injectTelemetryIntoRawSchema(t.RawInputSchema); ok {
53+
t.RawInputSchema = newRaw
5154
}
52-
t.InputSchema = schema
53-
t.RawInputSchema = nil
55+
continue
5456
}
5557

5658
if t.InputSchema.Type == "" {
@@ -72,6 +74,38 @@ func injectTelemetryListToolsHook(ctx context.Context, id any, message *mcp.List
7274
}
7375
}
7476

77+
// injectTelemetryIntoRawSchema mutates a raw JSON Schema document to add the
78+
// telemetry property and require it. Unknown top-level keywords pass through
79+
// verbatim. Returns false when the input can't be parsed as a JSON object.
80+
func injectTelemetryIntoRawSchema(raw json.RawMessage) (json.RawMessage, bool) {
81+
var schema map[string]any
82+
if json.Unmarshal(raw, &schema) != nil || schema == nil {
83+
return nil, false
84+
}
85+
if _, ok := schema["type"]; !ok {
86+
schema["type"] = "object"
87+
}
88+
props, _ := schema["properties"].(map[string]any)
89+
props = maps.Clone(props)
90+
if props == nil {
91+
props = map[string]any{}
92+
}
93+
props[instrmcp.TelemetryKey] = telemetrySchema()
94+
schema["properties"] = props
95+
96+
required, _ := schema["required"].([]any)
97+
if !slices.Contains(required, any(instrmcp.TelemetryKey)) {
98+
required = append(slices.Clone(required), instrmcp.TelemetryKey)
99+
}
100+
schema["required"] = required
101+
102+
out, err := json.Marshal(schema)
103+
if err != nil {
104+
return nil, false
105+
}
106+
return out, true
107+
}
108+
75109
// Removing tracing parameters from the tool call request so its not sent to the tool.
76110
// This must be registered after the tool handler middleware (mcp-go runs middleware in registration order).
77111
// This removes the telemetry parameter before user-defined middleware or tool handlers can see it.

contrib/mark3labs/mcp-go/intent_capture_test.go

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

95+
func TestIntentCaptureRawInputSchemaPreservesUnknownFields(t *testing.T) {
96+
// mcp.ToolInputSchema doesn't model additionalProperties/oneOf/etc;
97+
// intent capture must not silently strip those when injecting telemetry.
98+
tt := testTracer(t)
99+
defer tt.Stop()
100+
101+
srv := server.NewMCPServer("test-server", "1.0.0", WithMCPServerTracing(&TracingConfig{IntentCaptureEnabled: true}))
102+
srv.AddTool(mcp.NewToolWithRawSchema("raw_tool", "raw", json.RawMessage(`{
103+
"type": "object",
104+
"properties": {"app_id": {"type": "string"}},
105+
"required": ["app_id"],
106+
"additionalProperties": false,
107+
"oneOf": [{"required": ["app_id"]}]
108+
}`)), func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
109+
return mcp.NewToolResultText("ok"), nil
110+
})
111+
112+
listResp := srv.HandleMessage(context.Background(), []byte(`{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}`))
113+
var listResult map[string]interface{}
114+
require.NoError(t, json.Unmarshal(mustMarshal(listResp), &listResult))
115+
schema := listResult["result"].(map[string]interface{})["tools"].([]interface{})[0].(map[string]interface{})["inputSchema"].(map[string]interface{})
116+
117+
assert.Contains(t, schema["properties"].(map[string]interface{}), "telemetry")
118+
assert.Contains(t, schema["required"].([]interface{}), "telemetry")
119+
assert.Equal(t, false, schema["additionalProperties"])
120+
assert.NotNil(t, schema["oneOf"])
121+
}
122+
95123
func TestIntentCaptureRawInputSchema(t *testing.T) {
96124
tt := testTracer(t)
97125
defer tt.Stop()

0 commit comments

Comments
 (0)