Skip to content

Commit 919e66c

Browse files
fix(inventory): reuse encoded schema cache for typed tools
Key output encodings by their source definitions while preserving the SDK-registered schema guard. Keep header routing metadata available for legacy and unknown protocol clients. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 4b8a7ba commit 919e66c

3 files changed

Lines changed: 56 additions & 6 deletions

File tree

‎pkg/inventory/encoded_schemas.go‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import (
1111
)
1212

1313
type listedToolSchemas struct {
14-
source any
15-
registered *mcp.Tool
14+
source any
15+
outputSource any
16+
registered *mcp.Tool
1617
}
1718

1819
type encodedSchemaKey struct {
@@ -76,7 +77,11 @@ func encodedToolSchemasMiddleware(schemas map[string]listedToolSchemas) mcp.Midd
7677
if err != nil {
7778
return nil, fmt.Errorf("encode input schema for %q: %w", tool.Name, err)
7879
}
79-
toolCopy.OutputSchema, err = encodeListedSchema(definition.registered.OutputSchema, definition.registered.OutputSchema, tool.OutputSchema, false)
80+
outputSource := definition.outputSource
81+
if outputSource == nil {
82+
outputSource = definition.registered.OutputSchema
83+
}
84+
toolCopy.OutputSchema, err = encodeListedSchema(outputSource, definition.registered.OutputSchema, tool.OutputSchema, false)
8085
if err != nil {
8186
return nil, fmt.Errorf("encode output schema for %q: %w", tool.Name, err)
8287
}

‎pkg/inventory/registry.go‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,9 @@ func (r *Inventory) registerTools(ctx context.Context, s *mcp.Server, deps any,
279279
sourceSchema = registered.InputSchema
280280
}
281281
schemas[registered.Name] = listedToolSchemas{
282-
source: sourceSchema,
283-
registered: registered,
282+
source: sourceSchema,
283+
outputSource: tool.Tool.OutputSchema,
284+
registered: registered,
284285
}
285286
}
286287
s.AddReceivingMiddleware(encodedToolSchemasMiddleware(schemas))

‎pkg/inventory/typed_output_test.go‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,41 @@ func TestTypedToolRegistrationInfersSchemasAndValidates(t *testing.T) {
8484
assert.Equal(t, 1, handlerCalls, "SDK validation must reject invalid arguments before calling the handler")
8585
}
8686

87+
func TestTypedToolLegacySchemaRetainsHeaderRoutingMetadata(t *testing.T) {
88+
tool := NewServerToolWithContextHandler(
89+
mcp.Tool{Name: "legacy_routing_tool"},
90+
testToolsetMetadata("test"),
91+
func(_ context.Context, _ *mcp.CallToolRequest, input typedTestInput) (*mcp.CallToolResult, typedTestOutput, error) {
92+
return &mcp.CallToolResult{
93+
Content: []mcp.Content{&mcp.TextContent{Text: "legacy text result"}},
94+
}, typedTestOutput{Query: input.Query}, nil
95+
},
96+
)
97+
server := mcp.NewServer(&mcp.Implementation{Name: "test-server", Version: "v0.0.1"}, nil)
98+
tool.RegisterFunc(server, nil)
99+
session := connectTypedTestClient(t, server, "2025-11-25")
100+
101+
list, err := session.ListTools(context.Background(), nil)
102+
require.NoError(t, err)
103+
require.Len(t, list.Tools, 1)
104+
assert.Nil(t, list.Tools[0].OutputSchema)
105+
var inputSchema map[string]any
106+
require.NoError(t, json.Unmarshal([]byte(mustMarshalJSON(t, list.Tools[0].InputSchema)), &inputSchema))
107+
properties := inputSchema["properties"].(map[string]any)
108+
assert.Equal(t, "owner", properties["owner"].(map[string]any)["x-mcp-header"])
109+
assert.Equal(t, "repo", properties["repo"].(map[string]any)["x-mcp-header"])
110+
111+
result, err := session.CallTool(context.Background(), &mcp.CallToolParams{
112+
Name: "legacy_routing_tool",
113+
Arguments: map[string]any{"owner": "octo", "repo": "hello", "query": "is:open"},
114+
})
115+
require.NoError(t, err)
116+
require.False(t, result.IsError)
117+
assert.Nil(t, result.StructuredContent)
118+
require.Len(t, result.Content, 1)
119+
assert.Equal(t, "legacy text result", result.Content[0].(*mcp.TextContent).Text)
120+
}
121+
87122
func TestTypedToolInferredInputSchemaUsesStablePointer(t *testing.T) {
88123
type input struct {
89124
Owner string `json:"owner"`
@@ -1723,11 +1758,18 @@ func TestTypedToolSupportsExplicitRootUnionOutputSchema(t *testing.T) {
17231758
}
17241759

17251760
func TestTypedOutputProtocolGateUsesStatelessRequestVersion(t *testing.T) {
1761+
inputSchema := &jsonschema.Schema{
1762+
Type: "object",
1763+
Properties: map[string]*jsonschema.Schema{
1764+
"owner": {Type: "string", Extra: map[string]any{"x-mcp-header": "owner"}},
1765+
},
1766+
}
17261767
modernTool := &mcp.Tool{
17271768
Name: "typed_tool",
1769+
InputSchema: inputSchema,
17281770
OutputSchema: json.RawMessage(`{"type":"object"}`),
17291771
}
1730-
legacyTool := &mcp.Tool{Name: "typed_tool"}
1772+
legacyTool := &mcp.Tool{Name: "typed_tool", InputSchema: inputSchema}
17311773
registration := &typedToolRegistration{
17321774
name: "typed_tool",
17331775
modernTool: modernTool,
@@ -1765,6 +1807,8 @@ func TestTypedOutputProtocolGateUsesStatelessRequestVersion(t *testing.T) {
17651807
list := listResult.(*mcp.ListToolsResult)
17661808
require.Len(t, list.Tools, 1)
17671809
assert.Same(t, tc.wantTool, list.Tools[0])
1810+
assert.Contains(t, mustMarshalJSON(t, list.Tools[0].InputSchema), `"x-mcp-header":"owner"`,
1811+
"legacy and unknown-version variants must retain routing metadata")
17681812
})
17691813
}
17701814

0 commit comments

Comments
 (0)