Skip to content

Commit 18fb782

Browse files
SamMorrowDrumsCopilotgithub-actions[bot]
authored
feat(inventory): add typed MCP tool registration foundation (#3371)
* feat(inventory): add typed MCP tool registration foundation Use the MCP SDK's generic registration for concrete input/output types, with protocol-gated output schemas and structured results. Keep raw handlers compatible and apply shared middleware on both paths. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * chore: regenerate license files Auto-generated by license-check workflow * test: cover legacy typed CSV results Exercise the typed array and object paths through an initialized 2025-11-25 MCP session. Verify CSV text is returned once without structured output or the SDK JSON fallback. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * feat(inventory): normalize legacy typed arguments Add a raw JSON input normalizer that runs before SDK schema validation so migrated tools can retain legacy input coercions without widening their published schemas. Test case-folded issue states, numeric string IDs, validation failures and replacement registrations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * chore: regenerate license files Auto-generated by license-check workflow * test(inventory): preserve typed input optionality Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(inventory): stabilize typed schema compatibility Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * test(inventory): cover protocol isolation across clients Exercise simultaneous legacy and modern sessions against one typed tool, plus stateless HTTP requests with modern, legacy, and absent protocol headers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(inventory): harden typed schema registration contracts Cache immutable schema variants and enum overrides, preserve runtime input compatibility and request-era output behavior, and normalize dynamic scope challenges before dispatch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(inventory): cache warm schemas and guard typed preflight Avoid repeated canonical schema serialization and header annotation for cache-owned immutable pointers while retaining content-based lookup for caller-owned schemas. Run typed authorization and availability middleware before preflight and preserve tool-result error semantics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(inventory): preserve schema metadata isolation Advertise the SDK-compatible object schema for typed any input and deep-clone mutable JSON Schema metadata before caching or deriving runtime variants. Add legacy/modern registration and mutation isolation coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(inventory): detach numeric schema bounds and dedupe default traversal Two Copilot findings on 431b7a2: - CloneSchemas only deep-copies subschema nodes, so pointer-valued numeric validation keywords (Minimum, Maximum, MinLength, MinItems, etc.) remained aliased with the caller-owned schema after CloneSchema/CachedSchema. A caller mutating one of those pointers after caching could race with concurrent cached-schema serialization or silently change the cached validation bounds. Clone every *int/*float64 keyword explicitly alongside the existing collection/metadata cloning. - CloneSchemaWithoutDefaults's removeSchemaDefaults visited every map/pointer child twice: once via two literal child-collection loops, and again via schemaChildren, which already covers the same fields. This doubled traversal at every nested level, making default removal exponential for deeply nested object/array schemas. Traverse only the single comprehensive schemaChildren list. Add regression coverage across CloneSchema, CachedSchema, schema inference option cloning, and CloneSchemaWithoutDefaults: every *int/*float64 keyword is isolated from caller mutation (including under concurrent marshaling), and a 48-level deep chain is used for every schema-child field kind to confirm linear, non-exponential default removal. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(inventory): gate typed output on supported protocol range Select modern typed output for SDK-supported ISO-date protocol versions from 2026-07-28 onward. Missing, malformed, and unsupported future versions retain legacy behavior. Cover a later supported version through the selector list seam and verify the public path against the SDK supported set. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(inventory): unwrap pointer inputs before schema inference Match SDK input inference by unwrapping one pointer level before the cached input key and inference. Preserve nullable output pointer schemas. Cover both public constructors with actual modern and legacy discovery and calls, including structured null output. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * 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> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent bf0f67f commit 18fb782

37 files changed

Lines changed: 4059 additions & 171 deletions

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ For UI dependency upgrades, compare all four views (`get-me`, `issue-write`, `pr
6060
- Update readme documentation: `script/generate-docs`
6161
- If renaming a tool, add a deprecation alias (see [Tool Renaming Guide](docs/tool-renaming.md))
6262
- For toolset and icon configuration, see [Toolsets and Icons Guide](docs/toolsets-and-icons.md)
63+
- For typed tool registration and compatibility schemas, see [Typed Tool Schemas](docs/typed-tool-schemas.md)
6364
6. Push to your fork and [submit a pull request][pr] targeting the `main` branch
6465
7. Pat yourself on the back and wait for your pull request to be reviewed and merged.
6566

‎docs/typed-tool-schemas.md‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Typed tool schemas
2+
3+
Typed tool registrations use concrete Go input and output types with the MCP
4+
Go SDK's `mcp.AddTool` path. The SDK infers schemas when the tool definition
5+
does not provide them, validates arguments before the handler runs, and
6+
validates typed output. Keep business rules that JSON Schema cannot express
7+
in the handler or a preflight callback.
8+
9+
Input inference unwraps one pointer level, matching the SDK's object input
10+
contract. Output pointers retain nullable schemas and may return JSON null.
11+
12+
```go
13+
tool := github.NewToolWithSchemaOptions[workflowInput, workflowOutput](
14+
toolset,
15+
mcp.Tool{Name: "list_workflow_runs"},
16+
scopeAccess,
17+
inventory.TypedSchemaOptions{
18+
InputEnums: []inventory.SchemaEnum{
19+
{Path: "status", Values: github.WorkflowStatusValues()},
20+
},
21+
},
22+
listWorkflowRuns,
23+
)
24+
```
25+
26+
`SchemaEnum.Path` addresses inferred properties with dot-separated names and
27+
uses `items` to descend into array elements. `EnumSchema` creates a standalone
28+
string enum schema, while `WithEnum` returns a cloned explicit schema with an
29+
enum applied at a path. Inferred and explicit schema helpers cache immutable
30+
schemas; do not mutate the returned pointers.
31+
32+
When compatibility requires a broader runtime input contract than the one
33+
advertised to clients, provide `ValidationInputSchema`. The tool's declared
34+
`InputSchema` remains visible while the SDK validates calls against the
35+
runtime-only schema. Use `Preflight` for checks that need raw arguments or
36+
request dependencies before typed decoding; it may return a derived context
37+
for the handler. Input normalizers are only for compatibility transformations,
38+
not a replacement for schema validation.
39+
40+
The SDK applies defaults from the runtime input schema before decoding. If an
41+
omitted field must remain omitted, build the runtime schema with
42+
`inventory.CloneSchemaWithoutDefaults` before applying validation-only
43+
changes. Use `inventory.CloneSchema` when deriving other runtime-only schema
44+
variants so numeric bound pointers and nested metadata are detached as well as
45+
subschemas. Default removal visits each child once per parent. The advertised
46+
schema can retain its defaults; the constructor caches the runtime schema
47+
without mutating either caller-owned schema.
48+
49+
The output schema and `structuredContent` are exposed only for a negotiated,
50+
SDK-supported protocol version `2026-07-28` or later. Unknown versions are
51+
treated as legacy, including unsupported future dates; older, absent, and
52+
malformed versions also retain the legacy text result. Normal `Inventory.RegisterTools` and
53+
`ServerTool.RegisterFunc` registrations select behavior per request. Use
54+
`RegisterToolsForProtocolEra` or `RegisterFuncForProtocolEra` only when the
55+
protocol era is already known before server construction, such as a stateless
56+
request-scoped server.
57+
58+
If a handler intentionally returns non-JSON text or content blocks, set
59+
`PreserveHandlerContent` in `TypedSchemaOptions`, or call
60+
`inventory.PreserveToolHandlerContent(ctx)` from the handler middleware for
61+
request-dependent output such as CSV.

‎pkg/context/mcp_info.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ type MCPMethodInfo struct {
2424
ItemName string
2525
// RawArguments contains the unmaterialized tool arguments for tools/call requests.
2626
RawArguments json.RawMessage
27+
// NormalizedArguments contains canonical arguments prepared by HTTP scope
28+
// middleware so tool adapters do not rerun non-idempotent normalizers.
29+
NormalizedArguments json.RawMessage
30+
// ArgumentsNormalized distinguishes an intentionally empty normalized value
31+
// from a request that has not been normalized.
32+
ArgumentsNormalized bool
2733
// ProtocolVersion and ClientCapabilities describe the requesting MCP client
2834
// when stateless HTTP parsing makes them available before registration.
2935
ProtocolVersion string

‎pkg/github/actions.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,13 @@ Use this tool to list workflows in a repository, or list workflow runs, jobs, an
311311
"page": {
312312
Type: "number",
313313
Description: "Page number for pagination (default: 1)",
314-
Minimum: jsonschema.Ptr(1.0),
314+
Minimum: new(1.0),
315315
},
316316
"perPage": {
317317
Type: "number",
318318
Description: "Results per page for pagination (default: 30, max: 100)",
319-
Minimum: jsonschema.Ptr(1.0),
320-
Maximum: jsonschema.Ptr(100.0),
319+
Minimum: new(1.0),
320+
Maximum: new(100.0),
321321
},
322322
},
323323
Required: []string{"method", "owner", "repo"},
@@ -536,7 +536,7 @@ func ActionsRunTrigger(t translations.TranslationHelperFunc) inventory.ServerToo
536536
Annotations: &mcp.ToolAnnotations{
537537
Title: t("TOOL_ACTIONS_RUN_TRIGGER_USER_TITLE", "Trigger GitHub Actions workflow actions"),
538538
ReadOnlyHint: false,
539-
DestructiveHint: jsonschema.Ptr(true),
539+
DestructiveHint: new(true),
540540
},
541541
InputSchema: &jsonschema.Schema{
542542
Type: "object",

‎pkg/github/comment_minimize.go‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ var issueCommentVisibilityTarget = commentVisibilityTarget{
5757
"comment_id": {
5858
Type: "number",
5959
Description: "The numeric ID of the issue or pull request conversation comment",
60-
Minimum: jsonschema.Ptr(1.0),
60+
Minimum: new(1.0),
6161
},
6262
}
6363
},
@@ -84,7 +84,7 @@ var pullRequestReviewCommentVisibilityTarget = commentVisibilityTarget{
8484
"comment_id": {
8585
Type: "number",
8686
Description: "The numeric pull request review comment ID. Use the number from a #discussion_r... anchor, not the GraphQL thread node ID (PRRT_...).",
87-
Minimum: jsonschema.Ptr(1.0),
87+
Minimum: new(1.0),
8888
},
8989
}
9090
},
@@ -111,12 +111,12 @@ var pullRequestReviewVisibilityTarget = commentVisibilityTarget{
111111
"pullNumber": {
112112
Type: "number",
113113
Description: "The pull request number",
114-
Minimum: jsonschema.Ptr(1.0),
114+
Minimum: new(1.0),
115115
},
116116
"review_id": {
117117
Type: "number",
118118
Description: "The numeric ID of the pull request review",
119-
Minimum: jsonschema.Ptr(1.0),
119+
Minimum: new(1.0),
120120
},
121121
}
122122
},
@@ -175,8 +175,8 @@ func commentVisibilityTool(t translations.TranslationHelperFunc, target commentV
175175
Annotations: &mcp.ToolAnnotations{
176176
Title: t("TOOL_"+strings.ToUpper(name)+"_USER_TITLE", titleAction+" "+target.title),
177177
ReadOnlyHint: false,
178-
DestructiveHint: jsonschema.Ptr(false),
179-
OpenWorldHint: jsonschema.Ptr(true),
178+
DestructiveHint: new(false),
179+
OpenWorldHint: new(true),
180180
},
181181
InputSchema: &jsonschema.Schema{
182182
Type: "object",

‎pkg/github/copilot.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ func AssignCopilotToIssueWithIntent(t translations.TranslationHelperFunc) invent
563563
Type: "string",
564564
Description: "One concise sentence explaining what specifically about the issue led to choosing Copilot. " +
565565
"State the concrete signal (e.g. 'Well-scoped task with clear acceptance criteria').",
566-
MaxLength: jsonschema.Ptr(280),
566+
MaxLength: new(280),
567567
},
568568
"confidence": {
569569
Type: "string",

‎pkg/github/csv_output.go‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,14 @@ type csvOutputDocument struct {
4242
rows []map[string]string
4343
}
4444

45-
// withCSVOutput wraps the handler of every default-toolset list_* tool so that,
46-
// at request time, it checks the csv_output feature flag and converts the JSON
47-
// text response to CSV when enabled. The tool's schema, name, and scope are
48-
// unchanged — only the response payload format differs.
45+
// withCSVOutput adds dependency-aware middleware to every default-toolset
46+
// list_* tool so that it converts JSON text to CSV when the feature is enabled.
4947
func withCSVOutput(tools []inventory.ServerTool) []inventory.ServerTool {
5048
for i := range tools {
5149
if !isCSVOutputTool(tools[i]) {
5250
continue
5351
}
54-
tools[i].HandlerFunc = wrapHandlerWithCSVOutput(tools[i].HandlerFunc)
52+
tools[i].AddHandlerMiddleware(csvOutputMiddleware)
5553
}
5654
return tools
5755
}
@@ -68,18 +66,18 @@ func isCSVOutputTool(tool inventory.ServerTool) bool {
6866
return strings.HasPrefix(tool.Tool.Name, "list_")
6967
}
7068

71-
func wrapHandlerWithCSVOutput(next inventory.HandlerFunc) inventory.HandlerFunc {
72-
return func(deps any) mcp.ToolHandler {
73-
handler := next(deps)
74-
csvDeps, _ := deps.(ToolDependencies)
69+
func csvOutputMiddleware(deps any) inventory.ToolHandlerMiddleware {
70+
csvDeps, _ := deps.(ToolDependencies)
71+
return func(next mcp.ToolHandler) mcp.ToolHandler {
7572
return func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
76-
result, err := handler(ctx, req)
73+
result, err := next(ctx, req)
7774
if err != nil || result == nil || result.IsError {
7875
return result, err
7976
}
8077
if csvDeps == nil || !csvDeps.IsFeatureEnabled(ctx, FeatureFlagCSVOutput) {
81-
return result, nil
78+
return result, err
8279
}
80+
inventory.PreserveToolHandlerContent(ctx)
8381
return convertJSONTextResultToCSV(result), nil
8482
}
8583
}

‎pkg/github/csv_output_test.go‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/csv"
66
"encoding/json"
7+
"errors"
78
"strings"
89
"testing"
910

@@ -126,6 +127,114 @@ func TestCSVOutputPreservesOriginalJSONWhenFlagOff(t *testing.T) {
126127
assert.JSONEq(t, jsonResponse, text.Text)
127128
}
128129

130+
func TestCSVOutputMiddlewareAppliesToTypedToolRegistration(t *testing.T) {
131+
type row struct {
132+
Number int `json:"number"`
133+
}
134+
tools := withCSVOutput([]inventory.ServerTool{
135+
inventory.NewServerToolWithContextHandler(
136+
mcp.Tool{Name: "list_typed_things"},
137+
ToolsetMetadataRepos,
138+
func(context.Context, *mcp.CallToolRequest, struct{}) (*mcp.CallToolResult, []row, error) {
139+
return &mcp.CallToolResult{
140+
Content: []mcp.Content{&mcp.TextContent{Text: `[{"number":1}]`}},
141+
}, []row{{Number: 1}}, nil
142+
},
143+
),
144+
})
145+
deps := newCSVOutputTestDeps(true)
146+
server := mcp.NewServer(&mcp.Implementation{Name: "test-server", Version: "v0.0.1"}, nil)
147+
tools[0].RegisterFunc(server, deps)
148+
149+
serverTransport, clientTransport := mcp.NewInMemoryTransports()
150+
serverSession, err := server.Connect(context.Background(), serverTransport, nil)
151+
require.NoError(t, err)
152+
t.Cleanup(func() { _ = serverSession.Close() })
153+
client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "v0.0.1"}, nil)
154+
clientSession, err := client.Connect(context.Background(), clientTransport, nil)
155+
require.NoError(t, err)
156+
t.Cleanup(func() { _ = clientSession.Close() })
157+
158+
result, err := clientSession.CallTool(context.Background(), &mcp.CallToolParams{Name: "list_typed_things"})
159+
require.NoError(t, err)
160+
require.False(t, result.IsError)
161+
assert.JSONEq(t, `[{"number":1}]`, mustMarshalCSVJSON(t, result.StructuredContent))
162+
assert.Contains(t, textResult(t, result), "number\n1")
163+
}
164+
165+
func TestCSVOutputTypedArrayPreservesLegacyProtocolResult(t *testing.T) {
166+
type row struct {
167+
Number int `json:"number"`
168+
}
169+
type object struct {
170+
Number int `json:"number"`
171+
}
172+
tools := withCSVOutput([]inventory.ServerTool{
173+
inventory.NewServerToolWithContextHandler(
174+
mcp.Tool{Name: "list_typed_things"},
175+
ToolsetMetadataRepos,
176+
func(context.Context, *mcp.CallToolRequest, struct{}) (*mcp.CallToolResult, []row, error) {
177+
return &mcp.CallToolResult{
178+
Content: []mcp.Content{&mcp.TextContent{Text: `[{"number":1}]`}},
179+
}, []row{{Number: 1}}, nil
180+
},
181+
),
182+
inventory.NewServerToolWithContextHandler(
183+
mcp.Tool{Name: "get_typed_thing"},
184+
ToolsetMetadataRepos,
185+
func(context.Context, *mcp.CallToolRequest, struct{}) (*mcp.CallToolResult, object, error) {
186+
return &mcp.CallToolResult{
187+
Content: []mcp.Content{&mcp.TextContent{Text: `{"number":1}`}},
188+
}, object{Number: 1}, nil
189+
},
190+
),
191+
})
192+
deps := newCSVOutputTestDeps(true)
193+
server := mcp.NewServer(&mcp.Implementation{Name: "test-server", Version: "v0.0.1"}, nil)
194+
server.AddReceivingMiddleware(func(next mcp.MethodHandler) mcp.MethodHandler {
195+
return func(ctx context.Context, method string, request mcp.Request) (mcp.Result, error) {
196+
if method == "server/discover" {
197+
return nil, errors.New("legacy client uses initialize")
198+
}
199+
return next(ctx, method, request)
200+
}
201+
})
202+
for _, tool := range tools {
203+
tool.RegisterFunc(server, deps)
204+
}
205+
206+
serverTransport, clientTransport := mcp.NewInMemoryTransports()
207+
serverSession, err := server.Connect(context.Background(), serverTransport, nil)
208+
require.NoError(t, err)
209+
t.Cleanup(func() { _ = serverSession.Close() })
210+
client := mcp.NewClient(&mcp.Implementation{Name: "test-client", Version: "v0.0.1"}, nil)
211+
clientSession, err := client.Connect(context.Background(), clientTransport, nil)
212+
require.NoError(t, err)
213+
t.Cleanup(func() { _ = clientSession.Close() })
214+
require.Equal(t, "2025-11-25", clientSession.InitializeResult().ProtocolVersion)
215+
216+
list, err := clientSession.ListTools(context.Background(), nil)
217+
require.NoError(t, err)
218+
require.Len(t, list.Tools, 2)
219+
for _, tool := range list.Tools {
220+
assert.Nil(t, tool.OutputSchema, "legacy tools/list must hide typed output schemas")
221+
}
222+
223+
listResult, err := clientSession.CallTool(context.Background(), &mcp.CallToolParams{Name: "list_typed_things"})
224+
require.NoError(t, err)
225+
require.False(t, listResult.IsError)
226+
assert.Nil(t, listResult.StructuredContent)
227+
require.Len(t, listResult.Content, 1, "CSV text must not be duplicated by the SDK's JSON array fallback")
228+
assert.Equal(t, "number\n1\n", textResult(t, listResult))
229+
230+
objectResult, err := clientSession.CallTool(context.Background(), &mcp.CallToolParams{Name: "get_typed_thing"})
231+
require.NoError(t, err)
232+
require.False(t, objectResult.IsError)
233+
assert.Nil(t, objectResult.StructuredContent)
234+
require.Len(t, objectResult.Content, 1)
235+
assert.Equal(t, `{"number":1}`, textResult(t, objectResult))
236+
}
237+
129238
func TestCSVOutputVariantMovesMetadataToPreamble(t *testing.T) {
130239
csvText, err := jsonTextToCSV(`{
131240
"issues": [
@@ -152,6 +261,13 @@ func TestCSVOutputVariantMovesMetadataToPreamble(t *testing.T) {
152261
assert.NotContains(t, row, "totalCount")
153262
}
154263

264+
func mustMarshalCSVJSON(t *testing.T, value any) string {
265+
t.Helper()
266+
data, err := json.Marshal(value)
267+
require.NoError(t, err)
268+
return string(data)
269+
}
270+
155271
func TestJSONTextToCSVFlattensPrimaryRows(t *testing.T) {
156272
csvText, err := jsonTextToCSV(`{
157273
"discussions": [

‎pkg/github/dependencies.go‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,14 @@ func (d BaseDeps) IsFeatureEnabled(ctx context.Context, flag string) bool {
214214
// This avoids creating closures at registration time, which is important for performance
215215
// in servers that create a new server instance per request (like the remote server).
216216
//
217+
// Use concrete input and output structs for migrated tools; registration then
218+
// delegates schema inference and validation to mcp.AddTool. For example, define
219+
// `type SearchInput struct { Query string `json:"query"` }` and a concrete
220+
// `SearchOutput`, then use `NewTool[SearchInput, SearchOutput](...)`. Keep Out
221+
// as any for tools that must retain the raw registration path. A final optional
222+
// inventory.InputNormalizer argument can canonicalize legacy wire values
223+
// (such as case-insensitive enums) before strict SDK schema validation.
224+
//
217225
// The handler function receives deps extracted from context via MustDepsFromContext.
218226
// Ensure ContextWithDeps is called to inject deps before any tool handlers are invoked.
219227
//
@@ -223,11 +231,33 @@ func NewTool[In, Out any](
223231
tool mcp.Tool,
224232
scopeAccess inventory.ScopeAccess,
225233
handler func(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest, args In) (*mcp.CallToolResult, Out, error),
234+
inputNormalizers ...inventory.InputNormalizer,
235+
) inventory.ServerTool {
236+
return NewToolWithSchemaOptions(
237+
toolset,
238+
tool,
239+
scopeAccess,
240+
inventory.TypedSchemaOptions{},
241+
handler,
242+
inputNormalizers...,
243+
)
244+
}
245+
246+
// NewToolWithSchemaOptions is like NewTool, with options for schema inference,
247+
// runtime-only input validation schemas, and pre-decode compatibility checks.
248+
// The original tool schema remains the schema advertised to clients.
249+
func NewToolWithSchemaOptions[In, Out any](
250+
toolset inventory.ToolsetMetadata,
251+
tool mcp.Tool,
252+
scopeAccess inventory.ScopeAccess,
253+
schemaOptions inventory.TypedSchemaOptions,
254+
handler func(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest, args In) (*mcp.CallToolResult, Out, error),
255+
inputNormalizers ...inventory.InputNormalizer,
226256
) inventory.ServerTool {
227-
st := inventory.NewServerToolWithContextHandler(tool, toolset, func(ctx context.Context, req *mcp.CallToolRequest, args In) (*mcp.CallToolResult, Out, error) {
257+
st := inventory.NewServerToolWithContextHandlerAndSchemaOptions(tool, toolset, func(ctx context.Context, req *mcp.CallToolRequest, args In) (*mcp.CallToolResult, Out, error) {
228258
deps := MustDepsFromContext(ctx)
229259
return handler(ctx, deps, req, args)
230-
})
260+
}, schemaOptions, inputNormalizers...)
231261
st.ScopeAccess = scopeAccess
232262
return st
233263
}

0 commit comments

Comments
 (0)