Skip to content

fix: inline $ref pointers in schemaToJson for self-contained tool schemas#1671

Open
Vadaski wants to merge 2 commits intomodelcontextprotocol:mainfrom
Vadaski:fix/1562-inline-ref-in-tool-schemas
Open

fix: inline $ref pointers in schemaToJson for self-contained tool schemas#1671
Vadaski wants to merge 2 commits intomodelcontextprotocol:mainfrom
Vadaski:fix/1562-inline-ref-in-tool-schemas

Conversation

@Vadaski
Copy link

@Vadaski Vadaski commented Mar 12, 2026

Problem

Fixes #1562.

z.toJSONSchema() can emit $ref/$defs in two independent ways:

  1. Reused sub-schemas – when the same schema object appears in two or more places, Zod extracts it to $defs and emits $ref pointers. Controlled by the reused option.
  2. Schemas registered in z.globalRegistry with an id – Zod reads the _idmap of the metadata registry and extracts any schema with an id to $defs, regardless of reused. This fires whenever a user calls z.globalRegistry.add(schema, { id: 'Foo' }) or .meta({ id: 'Foo' }).

Tool inputSchema / outputSchema objects sent over MCP must be fully self-contained JSON Schema objects. LLMs and most downstream validators cannot resolve $ref pointers – especially $ref: "#/$defs/Foo" references that point into a sibling $defs block.

Fix

Update schemaToJson() in packages/core/src/util/schema.ts:

  1. Pass reused: 'inline' to prevent multiply-referenced sub-schemas from becoming $ref pointers.
  2. Pass a proxy metadata registry that wraps z.globalRegistry but:
    • strips the id field from returned metadata (so the serialiser skips the id-based $defs extraction pass)
    • exposes an empty _idmap
    • forwards all other metadata (e.g. .describe() descriptions, .meta() annotations) unchanged

This means schemas annotated with .describe('some description') still emit a description field in the JSON Schema output, while schemas registered with an explicit id are inlined instead of becoming $ref pointers.

Tests

Added packages/core/test/util/schemaToJson.test.ts with five tests:

  • Shared schemas are inlined, not $ref/$defs
  • No $ref for plain schemas
  • Correct output for z.object()
  • io: 'input' option is respected
  • .describe() metadata is preserved after id-stripping

Checklist

  • All existing tests pass (pnpm test:all)
  • TypeScript type-check passes (pnpm typecheck:all)
  • New tests added covering both the fix and the preserve-metadata invariant

…emas (modelcontextprotocol#1562)

`z.toJSONSchema()` can produce `$ref`/`$defs` in two ways:
- Reused sub-schemas: controlled by `reused: 'ref'|'inline'`
- Schemas registered in `z.globalRegistry` with an `id`: extracted to
  `$defs` regardless of the `reused` setting

Tool `inputSchema` and `outputSchema` objects sent to LLMs must be fully
self-contained — LLMs and most downstream validators cannot resolve `$ref`
pointers that point into `$defs` within the same document.

Fix `schemaToJson()` to:
1. Pass `reused: 'inline'` to prevent multiply-referenced sub-schemas from
   becoming `$ref` pointers.
2. Pass a proxy metadata registry that wraps `z.globalRegistry` but strips
   the `id` field from returned metadata and exposes an empty `_idmap`, so
   schemas registered with an `id` are inlined rather than extracted to
   `$defs`. Non-id metadata (e.g. `.describe()` descriptions) is preserved.

Add `packages/core/test/util/schemaToJson.test.ts` with five tests covering:
- Shared schemas inlined instead of producing `$ref`/`$defs`
- No `$ref` for basic schemas
- Correct output for a plain `z.object()`
- `io: 'input'` option respected
- `.describe()` metadata preserved after id-stripping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Vadaski Vadaski requested a review from a team as a code owner March 12, 2026 07:57
@changeset-bot
Copy link

changeset-bot bot commented Mar 12, 2026

⚠️ No Changeset found

Latest commit: ab6ff2c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 12, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@1671

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@1671

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@1671

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@1671

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@1671

commit: 34eac31

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

schemaToJson() produces $ref in tool inputSchema, causing LLM failures

1 participant