modelcontextprotocol / modelcontextprotocol/typescript-sdk

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

Open
#1,562 5 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

bug fix proposed P1 ready for work
Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 15h
Merged PRs (30d)
4

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 (https://github.com/anthropics/claude-code/issues/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.

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 at schemaToJson() and reproduce the registered-type and z.lazy cases shown in the issue. Review the proposed dereferenceLocalRefs() approach and its existing tests; done means local $ref pointers are inlined, $defs/definitions are removed, and tool inputSchema values no longer cause the reported LLM serialization failure.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.