-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
Description
schemaToJson() returns JSON Schema with $ref pointers for registered types (z.globalRegistry) and recursive types (z.lazy). LLMs consuming tool inputSchema cannot resolve $ref — they treat referenced parameters as untyped and serialize objects as string literals:
Expected: "parent": {"database_id": "2275ad9e-..."}
Received: "parent": "{\"database_id\":\"2275ad9e-...\"}"
→ Server rejects: MCP error -32602: Invalid arguments: expected object, received string
This is related to #1175 (AJV failing on $ref in tool schemas) — same root cause ($ref in inputSchema), different symptom (LLM stringification vs validator error).
Reproduction
import * as z from 'zod/v4';
import { schemaToJson } from '@modelcontextprotocol/core';
const Address = z.object({ street: z.string(), city: z.string() });
z.globalRegistry.add(Address, { id: 'Address' });
console.log(JSON.stringify(schemaToJson(z.object({ home: Address, work: Address }), { io: 'input' }), null, 2));Output contains $ref instead of inline types:
{
"properties": {
"home": { "$ref": "#/$defs/Address" },
"work": { "$ref": "#/$defs/Address" }
},
"$defs": { "Address": { "type": "object", ... } }
}Context
$ref in tool schemas has always been possible — the old zod-to-json-schema library used $refStrategy: "root" by default (identity-based deduplication on second encounter of the same JS object). However, #1460's switch to z.toJSONSchema() widened the blast radius significantly: registered types produce $ref even on first and only use, and all recursive types produce $ref.
Confirmed across Claude Code (anthropics/claude-code#18260) and Kiro CLI (independently).
Proposed fix
Add a dereferenceLocalRefs() step to schemaToJson() that inlines all local $ref pointers and strips $defs/definitions. ~95 lines, zero external dependencies.
I already have a working implementation with tests — wanted to file the issue for discussion before submitting the PR per contributing guidelines. Happy to submit if this approach looks right.