modelcontextprotocol / modelcontextprotocol/typescript-sdk
Tool inputSchema generation emits $ref for reused Zod instances · breaks strict MCP clients (kimi)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 13.4k
- Forks
- 2.2k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 4
Description
Summary
When a server exposes many tools, repeated Zod schema instances (a common pattern in generated clients) end up producing $ref back-pointers in the tool inputs returned by tools/list. Strict MCP clients — kimi being the immediate trigger — reject those refs and surface a references must start with #/$defs/ error, even though the refs themselves are valid JSON Pointers.
Repro context
Caught downstream in Softeria/ms-365-mcp-server#458. Concrete numbers from the maintainer (@eirikb) after dumping tools/list against a real server:
- 64 of 311 tools had
$refentries - 1190 back-refs total
- All of the form
#/properties/...— JSON Pointers back into each tool's own schema, no$defs - Root cause: the generated client reuses a small set of Zod instances (e.g.
microsoft_graph_dateTimeTimeZone) across many tool inputs, and Zod v4'stoJSONSchemadecides to emit$reffor each reused instance.
Where it lives in this SDK
packages/core/src/util/standardSchema.ts:200 (current main, commit at clone time):
result = z.toJSONSchema(schema as unknown as z.ZodType, { target: 'draft-2020-12', io }) as Record<string, unknown>;
The call is missing reused (Zod v4 controls deduplication behavior via that option). Without it, Zod's default is to emit $ref for every duplicate instance, which is the behavior @eirikb observed.
Proposed fix
Pass reused: 'inline' so tool input schemas stay self-contained:
result = z.toJSONSchema(schema as unknown as z.ZodType, {
target: 'draft-2020-12',
io,
reused: 'inline',
}) as Record<string, unknown>;
That mirrors what the downstream --discovery mode already does in practice (smaller per-tool schemas, no refs — @eirikb confirmed zero $ref in discovery output).
Why inline by default (vs. exposing as a registerTool option)
- Tool inputs are user-facing surface. A picky client failing on a JSON-Schema-valid construct is a worse default than slightly larger response payloads.
- Tool input schemas are typically shallow; the size penalty from inlining is bounded.
- Servers that genuinely want refs (deep nesting, true
$defsreuse) are the minority — and they can be supported via a server-side override on top ofinlinedefault, rather than the current "refs by default + no opt-out" state.
If exposing as an option is preferred over flipping the default, the option name + plumbing should land in McpServer.registerTool's tool-definition path so individual tools can opt out per-tool.
What this unblocks
- kimi k2.6 (immediate)
- any future MCP client whose
$refresolver is stricter than the spec - token-budget reduction on tool listings for ref-heavy servers (already a separately tracked concern — see Softeria/ms-365-mcp-server#438)
Trade-offs to be aware of
- Marginal increase in
tools/listpayload size for ref-heavy servers - Behavior change for any client that was relying on the current
$refshape — mitigated because the inlined schemas are semantically identical, just larger.
Happy to put up a PR with the change + a regression test that asserts no $ref in the output of a tool with reused inner schemas. Wanted to file the issue first to confirm direction.
Refs: downstream investigation thread (full data + @eirikb's tools/list dump).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/core/src/util/standardSchema.ts around line 200 and inspect the z.toJSONSchema call and its current options. Update the tool input schema generation to inline reused schemas, then add a regression test using reused inner schemas and verify that the generated tool input contains no $ref entries.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100