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

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions