Skip to content

Docs: passing z.object() as inputSchema on v1 fails silently (empty schema) or cryptically (tools/list crash) — not covered in troubleshooting or migration guide #2627

Description

@giacomomaria81

Summary

On SDK v1 (still latest on npm as of this writing — 1.30.0), passing a ZodObject (z.object({...})) where the API expects a Zod raw shape ({ field: z.string() }) produces failure modes that are either silent or cryptic, depending on the version and the API used. None of them point the developer at the actual mistake, and neither the troubleshooting page nor the v1→v2 migration guide mentions these symptoms.

I hit this while building scorm-mcp-server and lost the better part of a day on it: the server started fine, tools/list looked plausible, but every client silently stripped my tool arguments. I've since tested the behavior across v1 versions — full matrix below.

Reproduction matrix

Minimal repro: register one tool with inputSchema: z.object({ name: z.string() }) (instead of the raw shape { name: z.string() }), connect an in-memory client, call tools/list.

SDK version registerTool + z.object() server.tool() (positional) + z.object()
1.12.0 → 1.21.0 tools/list crashes: MCP error -32603: Cannot read properties of null (reading '_def') ⚠️ Silent: publishes an empty schema ({"type":"object"}, no properties) — clients strip all arguments; handler receives no args
1.22.0 → 1.26.0 ✅ normalized, works ⚠️ Silent empty schema (same as above)
1.28.0 → 1.30.0 ✅ normalized, works ✅ throws a clear error at registration
Repro script (run with any of the versions above)
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { z } from 'zod';

const server = new McpServer({ name: 't', version: '1.0.0' });

// WRONG on v1 (correct on v2): ZodObject instead of raw shape
server.tool('greet', 'desc', z.object({ name: z.string() }),
  async (args) => ({ content: [{ type: 'text', text: JSON.stringify(args) }] }));

const [ct, st] = InMemoryTransport.createLinkedPair();
const client = new Client({ name: 'c', version: '1.0.0' });
await Promise.all([server.connect(st), client.connect(ct)]);

const { tools } = await client.listTools();
console.log(JSON.stringify(tools[0].inputSchema));
// 1.12.0–1.26.0 → {"type":"object"}  (empty — no properties, no error anywhere)

Why this still matters even though recent versions behave better

  1. Older 1.x versions are massively deployed. Plenty of published servers pin ^1.x ranges resolved months ago; every tutorial written before mid-v1 shows raw shapes, while the v2 docs and README now show z.object() — so developers moving between examples get bitten in both directions.
  2. The failure modes never mention the cause. An empty {"type":"object"} schema looks like a client bug ("my client strips arguments"), and Cannot read properties of null (reading '_def') looks like an SDK internals bug. Neither error string is searchable back to "you passed a ZodObject where a raw shape was expected".
  3. The normalization added for z.object() still doesn't cover other Zod types — see registerTool() silently drops inputSchema for z.discriminatedUnion() — normalizeObjectSchema only passes z.object() #1643 (z.discriminatedUnion() silently dropped), so the general class of "wrong schema kind fails silently" is still live on latest.

Suggested fix (happy to PR either)

  1. Troubleshooting page: add an entry keyed on the two observable symptoms — "my tool's inputSchema is empty / clients don't send arguments" and "tools/list fails with reading '_def'" — explaining the raw-shape vs ZodObject distinction on v1 and which versions normalize it.
  2. Migration guide: the v1→v2 guide already documents the API shift (raw shape → Standard Schema), but a one-line warning that doing it backwards on v1 ≤1.21 crashes tools/list, and ≤1.26 silently empties the schema via server.tool(), would save people real debugging time.

I'm happy to open a PR with the docs wording if maintainers agree this belongs in the docs.


Environment used for the matrix: Node v22, zod 3.23.8, in-memory transport, each SDK version installed clean from npm.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions