modelcontextprotocol / modelcontextprotocol/typescript-sdk

Tool inputSchema generation emits $ref for reused Zod instances · breaks strict MCP clients (kimi)

Open Beginner friendly
#2,100 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
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 $ref entries
  • 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's toJSONSchema decides to emit $ref for 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 $defs reuse) are the minority — and they can be supported via a server-side override on top of inline default, 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 $ref resolver 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/list payload size for ref-heavy servers
  • Behavior change for any client that was relying on the current $ref shape — 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.