Skip to content

fix(server): honor Zod v4 toJSONSchema output semantics to match raw structuredContent#2467

Open
chakshu-dhannawat wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
chakshu-dhannawat:fix/zod-v4-json-schema-options
Open

fix(server): honor Zod v4 toJSONSchema output semantics to match raw structuredContent#2467
chakshu-dhannawat wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
chakshu-dhannawat:fix/zod-v4-json-schema-options

Conversation

@chakshu-dhannawat

Copy link
Copy Markdown

Closes #2464

The bug

toJsonSchemaCompat's Zod v4 branch (src/server/zod-json-schema-compat.ts) calls zod v4's toJSONSchema with hardcoded options and no passthrough. That produces three real defects, all stemming from the same root cause: the server validates a tool's structuredContent against its zod schema, but ships the tool's raw object as-is, it never runs the object through the schema's output transform. So the schema advertised in tools/list must describe that raw shape, not the fully-resolved one.

  1. z.date() anywhere in a tool schema throws during conversion (zod v4 defaults unrepresentable to "throw"), which crashes the entire tools/list response, not just the one tool.
  2. Output fields with .default() were listed in required, so a tool response correctly omitting a defaulted field gets rejected by the client's own schema validation.
  3. additionalProperties: false was set on output schemas the server doesn't actually enforce, so a response with extra fields also gets rejected client-side.

I reproduced all three against a real in-memory client/server pair before touching anything:

DEFECT1 (z.date breaks tools/list): REPRODUCED -> MCP error -32603: Date cannot be represented in JSON Schema
DEFECT2 (.default() field wrongly required): REPRODUCED -> count is in required[] despite having a default
DEFECT2 tool call omitting defaulted field: CLIENT REJECTED THE CALL -> MCP error -32602: Structured content does not match the tool's output schema: data must have required property 'count'
DEFECT3 (additionalProperties:false on output): REPRODUCED -> schema forbids extra keys

Defect 2 in particular isn't just a schema-advertisement nitpick, it makes the SDK's own client reject a perfectly legitimate tool response.

The fix

In the v4 branch only (the v3 vendored converter is a separate code path, untouched):

  • Always pass io: 'input' regardless of opts.pipeStrategy. Since the server ships the raw object for both input and output, 'input' semantics (defaulted fields optional, no additionalProperties: false) is correct in both cases, fixing defects 2 and 3 together.
  • Add unrepresentable: 'any' so a single unsupported type degrades to an unconstrained schema instead of crashing the whole tools/list call.
  • Add an override that rewrites z.date() specifically to { type: "string", format: "date-time" }, the actual wire format JSON.stringify produces for a Date. Without this, unrepresentable: 'any' alone would silence the crash but lose the date-time format entirely (falls back to an empty/any schema).

Testing

Added three regression tests to test/server/mcp.test.ts, gated to the Zod v4 matrix entry, covering each defect end-to-end through a real client/server pair (not just the schema shape, but that a previously-rejected legitimate tool call now succeeds). Verified they fail against the pre-fix code and pass with the fix. Full suite (npx vitest run): 2721 tests passed, 76 files, no regressions. npm run lint clean. Added a changeset.

zod-json-schema-compat.ts called zod v4's toJSONSchema with hardcoded
options and no passthrough for the asymmetry between the schema used
for validation and the raw structuredContent object the server
actually ships (validation never replaces or serializes the tool's
returned object).

This caused three real defects:
- z.date() anywhere in a tool schema throws during conversion, since
  v4 defaults unrepresentable types to 'throw'. That crashes the
  entire tools/list response, not just the one tool.
- Output schema fields with .default() were listed in required even
  though the server never runs structuredContent through the schema's
  output transform, so a tool response correctly omitting a defaulted
  field got rejected by the client's own schema validation.
- additionalProperties: false was set on output schemas the server
  doesn't actually enforce, so a tool response with extra fields also
  got rejected client-side.

Fixed by always using io: 'input' semantics for the v4 branch
(matching what the server actually ships, for both input and output
schemas), adding unrepresentable: 'any' so an unsupported type
degrades gracefully instead of crashing tools/list, and overriding
z.date() specifically to the date-time string format JSON.stringify
already produces on the wire.

Added regression tests for all three cases, gated to the v4 matrix
entry since the v3 vendored converter is a separate code path this
doesn't touch.
@chakshu-dhannawat chakshu-dhannawat requested a review from a team as a code owner July 9, 2026 01:24
@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b8301bf

The changes in this PR will be included in the next version bump.

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

@pkg-pr-new

pkg-pr-new Bot commented Jul 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@2467

commit: b8301bf

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.

1 participant