Skip to content

zod v4: toJSONSchema options are hardcoded — z.date() breaks tools/list; output schemas advertise required/closed shapes the raw structuredContent can't satisfy #2464

Description

@jtemps

Summary

When tools use zod v4 schemas, the SDK's zod→JSON Schema conversion (server/zod-json-schema-compat.ts, toJsonSchemaCompat) calls zod's native toJSONSchema with hardcoded options and no passthrough, producing three behaviors that break real servers. All three stem from the same asymmetry: the SDK converts and validates with the tool's zod schema, but sends the tool's raw structuredContent object (validation does not replace/serialize the payload), so the advertised schema must describe the serialized form of a raw object.

Versions: @modelcontextprotocol/sdk 1.29.0, zod 4.4.3.

1. z.date() anywhere in a tool schema kills tools/list

zod v4's toJSONSchema defaults to unrepresentable: "throw", so listing tools throws:

McpError: MCP error -32603: Date cannot be represented in JSON Schema

The zod v3 path (vendored zod-to-json-schema) rendered z.date() as { "type": "string", "format": "date-time" } — which is also what JSON.stringify actually puts on the wire for a Date. Servers migrating v3→v4 with Date-valued fields (e.g. ORM entities) lose tools/list entirely. Switching schemas to z.iso.datetime() is not a workaround: server-side output validation then rejects the tools' raw Date values before serialization (isError: true, "expected string").

2. Defaulted output-schema fields are advertised as required

With io: "output" semantics, zod v4 lists .default() fields in required. But the server ships the tool's raw return value — zod never fills defaults into the response — so a tool that omits a defaulted field violates its own advertised schema, and spec-compliant validating clients reject the response. (zod-to-json-schema kept defaulted fields optional.)

3. additionalProperties: false on output objects can't be honored

Plain z.object() converts with additionalProperties: false on outputs, while zod validation tolerates and passes through unknown keys on the raw object. Any tool returning extra keys (very common when returning spread ORM/service objects) produces responses that validating clients reject. We caught four production tools failing this way once we added client-side ajv validation of serialized results.

Repro (issues 1 & 2 in one tool)

const server = new McpServer({ name: "repro", version: "1.0.0" });
server.registerTool(
  "t",
  {
    description: "repro",
    inputSchema: { when: z.date() },                       // (1) tools/list throws
    outputSchema: { counted: z.number().default(0),        // (2) advertised as required
                    name: z.string() },
  },
  async () => ({ content: [], structuredContent: { name: "x" } }) // omits `counted` → strict clients reject
);

Proposal

Either (preferred, minimal) expose conversion options on registerTool/server config — at least unrepresentable and override passthrough to toJSONSchema — or make the defaults wire-truthful:

  • unrepresentable: "any" + render ZodDate as { type: "string", format: "date-time" } (matches serialization and the v3 path),
  • drop defaulted fields from required in output conversion,
  • drop additionalProperties: false in output conversion (or strip unknown keys during output validation so the promise is actually kept).

We currently carry exactly these three adjustments as a ~30-line pnpm patch to zod-json-schema-compat (both dists), with contract tests pinning the behaviors — happy to send a PR if maintainers agree on the direction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues affecting some users, edge cases, potentially valuable featurebugSomething isn't workingfix proposedBot has a verified fix diff in the commentready for workEnough information for someone to start working on

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions