fix: inline local $ref in tool inputSchema for LLM consumption#1563
Open
gogakoreli wants to merge 1 commit intomodelcontextprotocol:mainfrom
Open
fix: inline local $ref in tool inputSchema for LLM consumption#1563gogakoreli wants to merge 1 commit intomodelcontextprotocol:mainfrom
gogakoreli wants to merge 1 commit intomodelcontextprotocol:mainfrom
Conversation
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
Tool schemas containing $ref cause LLM failures across multiple MCP clients. LLMs cannot resolve JSON Schema $ref pointers — they serialize referenced parameters as strings instead of objects. While $ref was always possible in tool schemas, modelcontextprotocol#1460's switch from zod-to-json-schema to z.toJSONSchema() widened the blast radius: registered types (z.globalRegistry) and recursive types (z.lazy) now produce $ref on common patterns that previously rarely triggered it. Adds dereferenceLocalRefs() to schemaToJson() which inlines all local $ref pointers, ensuring tool schemas are self-contained and LLM-consumable. Fixes: modelcontextprotocol#1562
cc0d216 to
925ab05
Compare
🦋 Changeset detectedLatest commit: 925ab05 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Author
|
I also have a detailed ADR documenting the investigation, alternatives analysis, and design rationale. Happy to include it as |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Inline local
$refin toolinputSchemafor LLM consumptionFixes: #1562
Related: anthropics/claude-code#18260
Problem
Tool
inputSchemacontaining$refpointers causes LLM failures across multiple MCP clients. LLMs cannot resolve JSON Schema$ref— they treat referenced parameters as untyped and serialize objects as string literals:$refin tool schemas has always been possible (non-Zod servers, and the oldzod-to-json-schemawith its default$refStrategy: "root"for identity-based deduplication). However, #1460's switch toz.toJSONSchema()significantly widened the blast radius — registered types (z.globalRegistry) now produce$refeven on first and only use, and all recursive types (z.lazy) produce$ref. The old library only triggered on the second encounter of the same JS object reference.Confirmed across Claude Code (#18260) and Kiro CLI (independently).
Solution
Add
dereferenceLocalRefs()toschemaToJson()— inlines all local$refpointers (#/$defs/...,#/definitions/...) so tool schemas are self-contained.~95 lines of implementation, zero external dependencies.
Behavior:
$ref→ inlined,$defs/definitionsstripped$ref(URLs) → left as-is{ type: "object" }$ref→ preserved per JSON Schema 2020-12dereferenceLocalRefsis@internal— not intended as public APIAlternatives considered
zod-to-json-schemadereference-json-schemanpm packagereused: "inline"option$refregardless (verified)overridecallback$defsaren't fully built at override time; would need two-pass generationz.globalRegistry"globalRegistryis a legitimate Zod featureLimitations
schemaToJson(). Non-Zod servers sending raw JSON Schema with$refare not affected — their schemas don't hit this code path.{ type: "object" }vs the full type).Test plan
pnpm lint:allpassespnpm test:allpasses (all existing tests + 14 new)pnpm build:allpasses9 unit tests (
packages/core/test/schema.test.ts) — testdereferenceLocalRefsdirectly with crafted JSON Schema:$ref), registered types, recursive types, diamond references, non-existent$def(left as-is), external$ref(left as-is), sibling keyword preservation,$ref: "#"root self-reference, registry cleanup5 integration tests (
test/integration/test/server/mcp.test.ts) — full server→client pipeline:discriminatedUnion+ registry, mixed$ref+ inline params (Notion repro),$reftooneOfunion, recursive types$ref) AND runtimecallToolround-tripAll integration tests clean up
z.globalRegistryviaafterEach.Files changed
packages/core/src/util/schema.tsdereferenceLocalRefs(), modifyschemaToJson()to call itpackages/core/test/schema.test.tstest/integration/test/server/mcp.test.tsdescribeblock.changeset/inline-ref-in-tool-schema.md