Skip to content

feat(inventory): add typed MCP tool registration foundation - #3371

Draft
SamMorrowDrums wants to merge 14 commits into
mainfrom
sammorrowdrums-typed-schema-foundation
Draft

SamMorrowDrums wants to merge 14 commits into
mainfrom
sammorrowdrums-typed-schema-foundation

Conversation

@SamMorrowDrums

@SamMorrowDrums SamMorrowDrums commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Add the shared typed MCP registration foundation with cached schemas, SDK argument/output validation, and request-version output gating. No production tool definitions are migrated; later layers supply concrete DTOs and compatibility rules.

Why

Provide the bottom layer of the replacement typed-schema stack split from #3360, which remains open and unchanged. Preserve legacy text, input coercions, business validation, permissions, and raw-handler behavior while enabling typed schema contracts.
Fixes N/A — shared foundation split from #3360.

What changed

  • Add typed constructors, TypedSchemaOptions, cached schema/enum helpers, validation-only input schemas, preflight preparation, and compatibility input normalizers. The pinned jsonschema-go version does not infer enum constraints from description tags, so shared registration-time enum overrides handle that explicitly.
  • Use SDK generic registration with concrete input types and concrete-output schema inference. The adapter keeps error/short-circuit handling out of artificial zero-value output validation while successful outputs remain validated against the explicit concrete-output schema.
  • Expose typed output schemas and structured results only for the exact supported 2026-07-28 request version. Older, absent, and unrecognized versions retain handler text; raw tools' existing structured/multi-round-trip content remains unchanged.
  • Cache immutable inferred/explicit schema pointers across fresh registrations and both protocol eras. Cache-owned pointers avoid repeated canonical serialization; caller-owned schemas retain content-based lookup and mutation protection. Runtime schemas preserve legacy omission semantics without SDK-injected defaults.
  • Normalize argument-dependent OAuth scope checks before challenge evaluation and reuse canonical request arguments without rerunning non-idempotent normalizers. Availability/auth middleware runs before preflight, which remains before SDK decoding; ordinary preflight errors remain tool-error results and explicit JSON-RPC errors preserve protocol semantics.
  • Deep-clone nested schema nodes and mutable metadata while preserving concrete Go value types. Typed any input registrations without a declared schema advertise the SDK-compatible object schema without mutating the caller-owned tool definition.
  • Add protocol, stateless HTTP, schema/cache, metadata-mutation/concurrency, output-shape, error/short-circuit, CSV, and raw-handler regression coverage. No production tool definitions or snapshots are migrated in this foundation PR.

MCP impact

  • No tool or API changes
  • Tool schema or behavior changed — adds shared library registration APIs and typed protocol behavior; no production tool definitions are migrated and existing tool snapshots remain unchanged.
  • New tool added

Prompts tested (tool changes only)

  • N/A — no production tool migration. Fixture-based protocol and compatibility tests exercise the registration path; no provider-acceptance claim.

Security / limits

  • No security or limits impact
  • Auth / permissions considered — normalized arguments are used for dynamic OAuth scope challenges; authorization/availability guards precede preflight, and dependency/feature middleware remains active on typed and raw paths.
  • Data exposure, filtering, or token/size limits considered — legacy clients do not receive new typed schemas or structured output; CSV handler content and raw multi-round-trip results are preserved. Final stack compact-output/performance acceptance is deferred.

Tool renaming

  • I am renaming tools as part of this PR (e.g. a part of a consolidation effort)
    • I have added the new tool aliases in deprecated_tool_aliases.go
  • I am not renaming tools as part of this PR

Note: if you're renaming tools, you must add the tool aliases. For more information on how to do so, please refer to the official docs.

Lint & tests

  • Linted locally with ./script/lint — GOMAXPROCS=2 GOFLAGS=-p=1 script/lint passed with 0 issues using available golangci-lint v2.14.0. Repository-pinned v2.9.0 cannot decode Go 1.27.1 export data in this environment.
  • Tested locally with ./script/test — GOMAXPROCS=2 GOFLAGS=-p=1 script/test passed (full go test -race ./...).

Final ordered validation passed on 431b7a2170f27aff13a02dc2e31ba5a726d4f571:

  • GOMAXPROCS=2 GOFLAGS=-p=1 UPDATE_TOOLSNAPS=true go test ./... — passed. Snapshot generation produced only trailing-newline noise in merge_pull_request.snap; inspected and restored. No snapshot changes remain.
  • GOMAXPROCS=2 GOFLAGS=-p=1 script/lint — passed, 0 issues with golangci-lint v2.14.0.
  • GOMAXPROCS=2 GOFLAGS=-p=1 script/test — passed, including the race-enabled full suite.
  • GOMAXPROCS=2 GOFLAGS=-p=1 script/generate-docs — passed; generated documentation unchanged.
  • git diff --check — passed; no uncommitted changes remained after publication.

The exact prior foundation patch through e65cac17 was measured through the unchanged real HTTP handler/factory (six repeats): warm registration 14.38 ms / 7.150 MiB / 24.01k allocations; legacy tools/list 29.90 ms / 14.91 MiB / 43.77k; modern tools/list 32.65 ms / 15.00 MiB / 43.89k. Tools/list payloads were SHA-256 identical to main. Commit 431b7a21 adds schema-clone and typed-any correctness fixes; it has not had a separate performance run, and no final-stack performance claim is made.

Fresh Copilot review findings on e65cac17 were addressed in 431b7a21. The dynamic-scope review thread was fixed and resolved; the prior empty-content fallback finding was verified against its existing regression. A fresh review of 431b7a21 is being requested; review acceptance and live CI on this new head are not claimed. E2E/provider testing was not run.

Docs

  • Not needed
  • Updated (README / docs / examples) — added docs/typed-tool-schemas.md, linked from CONTRIBUTING; generated README/tool documentation unchanged.

@SamMorrowDrums
SamMorrowDrums added this pull request to stack #3385 October 2, 2026 10:03
@SamMorrowDrums
SamMorrowDrums marked this pull request as ready for review October 2, 2026 10:05
@SamMorrowDrums
SamMorrowDrums requested a review from a team as a code owner October 2, 2026 10:05
Copilot AI balanced review requested due to automatic review settings October 2, 2026 10:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Non-object outputs can duplicate legacy content, and per-registration schema allocation can cause unbounded shared-cache growth.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
What changed in this PR

Adds foundational typed MCP tool registration with schema validation, protocol-gated structured output, input normalization, and shared middleware support.

Changes:

  • Adds typed registration and protocol-aware output handling.
  • Introduces legacy-input normalizers and middleware providers.
  • Extends CSV conversion to typed tools with comprehensive tests.
File Description
pkg/​inventory/​typed_output.go Implements typed output processing and protocol gating.
pkg/​inventory/​typed_output_test.go Tests schemas, normalization, middleware, and protocols.
pkg/​inventory/​server_tool.go Adds typed registration and normalization APIs.
pkg/​inventory/​registry.go Registers shared typed-output middleware.
pkg/​github/​dependencies.go Exposes normalizers through NewTool.
pkg/​github/​csv_output.go Converts CSV handling into shared middleware.
pkg/​github/​csv_output_test.go Tests CSV behavior for typed tools.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/inventory/server_tool.go Outdated
Comment thread pkg/inventory/typed_output.go Outdated
@SamMorrowDrums
SamMorrowDrums force-pushed the sammorrowdrums-typed-schema-foundation branch from 47f6c2a to 6e9caa4 Compare October 2, 2026 10:26
SamMorrowDrums and others added 8 commits October 2, 2026 22:23
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>
Auto-generated by license-check workflow
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>
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>
Auto-generated by license-check workflow
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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>
@SamMorrowDrums
SamMorrowDrums force-pushed the sammorrowdrums-typed-schema-foundation branch from 0aaaf77 to 22e536a Compare October 2, 2026 20:50
@SamMorrowDrums
SamMorrowDrums requested a balanced review from Copilot October 3, 2026 04:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Raw registrations can normalize inputs twice, and fallback cleanup can remove legitimate handler content.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
Resolved since last review (2)

Comment thread pkg/inventory/server_tool.go Outdated
Comment thread pkg/inventory/typed_output.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Typed middleware short-circuits, raw-output normalization, fallback cleanup, and scope normalization have unresolved correctness or security issues.

Review effort: Balanced
Findings: 3 High severity · 2 Medium severity

Open (5)

Comment thread pkg/inventory/server_tool.go Outdated
Comment thread pkg/inventory/typed_output.go Outdated
Comment thread pkg/inventory/typed_output.go
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Preflight errors are incorrectly propagated as JSON-RPC failures, and preflight currently runs before availability and authorization guards.

Review effort: Balanced
Findings: 3 High severity

Open (3)
Resolved since last review (4)

Comment thread pkg/inventory/typed_output.go Outdated
Comment thread pkg/inventory/typed_output.go Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The any-input typed path can advertise a nil input schema, and cached schemas retain mutable caller-owned collections.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (2)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Modern tool registration omits synthesized input schema

pkg/​inventory/​server_tool.go:429

When In is any and no input schema is supplied, the SDK synthesizes its object schema only on the internal tool copy passed to AddTool. The protocol middleware later replaces that tools/list entry with registration.modernTool, which still has a nil InputSchema, so NewServerToolWithContextHandler[any, ConcreteOutput] advertises an invalid tool definition. Materialize the same cached object schema here (and cover both protocol eras) rather than relying on the SDK copy.

This issue also appears on line 531 of the same file.

Medium severity Schema cloning leaves mutable fields shared

pkg/​inventory/​typed_schema.go:180

Schema.CloneSchemas only deep-copies nested schema nodes; mutable non-schema fields such as Enum, Examples, Extra, DependencyStrings, and PropertyOrder remain shared. Consequently, mutating those fields on the caller-owned schema after this call changes the globally cached value (and can race with request-scoped registrations), despite this API's cloning/immutability contract. Fully detach all mutable fields recursively before publishing the cached pointer.

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>
@SamMorrowDrums

Copy link
Copy Markdown
Collaborator Author

Follow-up for Copilot review 5399416378, fixed in 431b7a2170f27aff13a02dc2e31ba5a726d4f571: typed [any, ConcreteOutput] registrations with no declared input schema now materialize the SDK-compatible { "type": "object" } schema in the advertised modern and legacy tools. The caller-owned ServerTool.Tool is unchanged. CloneSchema now recursively detaches mutable schema metadata (including nested enum/example/extra/const/default/dependency/order collections) while preserving concrete Go metadata types; cache, enum derivation, runtime-default stripping, and inference overrides use it. Added both-era any-input discovery/call coverage, mutation/copy and concurrent marshal isolation tests, null-const coverage, and inference-option cloning coverage. Final sequence passed on 431b7a2: GOMAXPROCS=2 GOFLAGS=-p=1 UPDATE_TOOLSNAPS=true go test ./...; GOMAXPROCS=2 GOFLAGS=-p=1 script/lint (0 issues); GOMAXPROCS=2 GOFLAGS=-p=1 script/test; GOMAXPROCS=2 GOFLAGS=-p=1 script/generate-docs; git diff --check. Snapshot generation produced only trailing-newline noise in merge_pull_request.snap, inspected and restored; no snapshot or generated doc deltas remain.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Cached schemas still alias validation pointers, middleware errors bypass tool-result finalization, and default removal can traverse schemas exponentially.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Convert middleware errors into tool results

pkg/​inventory/​typed_output.go:234

Configured tool middleware now executes outside the SDK's tool-handler finalization. Returning an ordinary error here therefore becomes a JSON-RPC method error, unlike raw tools and typed handler errors, which become IsError tool results. This is reachable when OAuth middleware returns github authorization failed; convert non-JSON-RPC middleware errors with CallToolResult.SetError, while preserving explicit *jsonrpc.Error values.

Comment thread pkg/inventory/typed_schema.go
Comment thread pkg/inventory/typed_schema.go
…rsal

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Pointer input registration can panic, and some valid output-schema representations bypass short-circuit validation.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (2)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Remarshal all output schema representations before validation

pkg/​inventory/​typed_output.go:291

The SDK accepts any JSON-marshalable output-schema representation, including maps, but this helper only converts json.RawMessage. For a map-backed schema, a successful preflight or handler-middleware short circuit bypasses the SDK handler and receives a nil schema here, so invalid structured content is returned without validation. Remarshal every non-*jsonschema.Schema representation before validating.

Comment thread pkg/inventory/typed_schema.go
SamMorrowDrums and others added 2 commits October 3, 2026 14:39
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>
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Modern successful short-circuit results can bypass required structured-output validation.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Remarshal map-valued output schemas before validation

pkg/​inventory/​typed_output.go:290

The SDK accepts explicit schemas represented as maps by remarshal, but this helper recognizes only *jsonschema.Schema and json.RawMessage. Consequently, a typed tool with a valid map-valued OutputSchema skips validation for successful preflight/middleware short-circuit results. Remarshal every non-pointer schema representation here so adapter validation matches mcp.AddTool.

Medium severity Require structured content for successful typed tool results

pkg/​inventory/​typed_output.go:296

A successful preflight or handler-middleware short circuit can return text-only content, and this guard treats the missing StructuredContent as valid even though the modern tool advertises an output schema. That emits a successful result which does not satisfy the typed contract. Skip validation only for errors/input requests; for a normal success, require structured content (JSON null remains distinguishable as a non-nil raw value).

@SamMorrowDrums
SamMorrowDrums marked this pull request as draft October 3, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants