Part of #5743. Found during the #5742 regression-gate audit (matrix §12).
Problem
When vMCP ingests a backend's tools/list, a tool whose inputSchema uses a top-level JSON-Schema keyword outside the shim struct's fixed fields — oneOf, anyOf, allOf, $ref, enum, patternProperties, … — has that keyword silently dropped, and a type-less schema gains a spurious "type": "". Nested keywords under properties survive (they ride an opaque map[string]any).
Root cause (upstream, toolhive-core)
The shim's mcp.Tool.InputSchema is a fixed-field struct (type, properties, required, $defs, additionalProperties), and the alternative RawInputSchema field is tagged json:"-" with no custom UnmarshalJSON (mcpcompat/mcp/tools.go). So when the SDK client decodes a tools/list response into mcp.Tool, any top-level keyword outside those five fields is discarded before vMCP's conversion.ConvertToolInputSchema (pkg/vmcp/client/client.go) ever runs. RawInputSchema is never populated on the ingest path.
There is no clean in-repo fix: re-decoding raw bytes in ToolHive would duplicate the SDK's job and leak SDK internals across the client boundary (vMCP anti-pattern #5). This must be fixed upstream.
Verified with a live probe against toolhive-core v0.0.32:
{"oneOf":[...]} → projected {"properties":{},"required":[],"type":""} (oneOf gone).
{"properties":{"x":{...}},"required":["x"]} (no type) → gains "type":"".
- nested
anyOf under properties survives.
Fix
- Upstream (toolhive-core): add
Tool.UnmarshalJSON (symmetric with the existing MarshalJSON) that populates RawInputSchema from the raw inputSchema bytes, honoring the existing errToolSchemaConflict invariant. Cut a release.
- ToolHive: bump
toolhive-core, then unskip the in-repo regression test (TestRegression_ToolSchemaFidelity_PreservesCompositors in pkg/vmcp/client), which asserts a oneOf/type-less backend schema is projected intact. The skip→green flip is the acceptance signal.
The in-repo skipped test lands first (with the #5742 gate work) so the regression is documented and the flip is a one-line change once the bump is available.
Generated with Claude Code
Part of #5743. Found during the #5742 regression-gate audit (matrix §12).
Problem
When vMCP ingests a backend's
tools/list, a tool whoseinputSchemauses a top-level JSON-Schema keyword outside the shim struct's fixed fields —oneOf,anyOf,allOf,$ref,enum,patternProperties, … — has that keyword silently dropped, and a type-less schema gains a spurious"type": "". Nested keywords underpropertiessurvive (they ride an opaquemap[string]any).Root cause (upstream, toolhive-core)
The shim's
mcp.Tool.InputSchemais a fixed-field struct (type,properties,required,$defs,additionalProperties), and the alternativeRawInputSchemafield is taggedjson:"-"with no customUnmarshalJSON(mcpcompat/mcp/tools.go). So when the SDK client decodes atools/listresponse intomcp.Tool, any top-level keyword outside those five fields is discarded before vMCP'sconversion.ConvertToolInputSchema(pkg/vmcp/client/client.go) ever runs.RawInputSchemais never populated on the ingest path.There is no clean in-repo fix: re-decoding raw bytes in ToolHive would duplicate the SDK's job and leak SDK internals across the client boundary (vMCP anti-pattern #5). This must be fixed upstream.
Verified with a live probe against toolhive-core v0.0.32:
{"oneOf":[...]}→ projected{"properties":{},"required":[],"type":""}(oneOf gone).{"properties":{"x":{...}},"required":["x"]}(no type) → gains"type":"".anyOfunderpropertiessurvives.Fix
Tool.UnmarshalJSON(symmetric with the existingMarshalJSON) that populatesRawInputSchemafrom the rawinputSchemabytes, honoring the existingerrToolSchemaConflictinvariant. Cut a release.toolhive-core, then unskip the in-repo regression test (TestRegression_ToolSchemaFidelity_PreservesCompositorsinpkg/vmcp/client), which asserts aoneOf/type-less backend schema is projected intact. The skip→green flip is the acceptance signal.The in-repo skipped test lands first (with the #5742 gate work) so the regression is documented and the flip is a one-line change once the bump is available.
Generated with Claude Code