Skip to content

Bug: server.tool() silently ignores ZodObject schemas and strips all arguments #1291

@oleduc

Description

@oleduc

Summary

When registering a tool using the TypeScript MCP server, passing a ZodObject as the inputSchema causes the SDK to normalize it into an empty object schema, resulting in all arguments being stripped before reaching the tool handler.

Only the shape-object form works:

inputSchema: {
  message: z.string(),
}

Using the more idiomatic Zod form:

inputSchema: z.object({ message: z.string() })

silently breaks argument parsing.

This leads to confusing behavior: arguments are sent correctly by the client, received in the raw request, but the tool handler receives {}.


Steps to Reproduce

1. Register a tool using z.object(...)

server.tool(
  'test.echo',
  'Echo back the input',
  z.object({
    message: z.string().describe('Message to echo'),
  }),
  async (args) => {
    console.log('ARGS:', args);
    return { content: [{ type: 'text', text: args.message }] };
  }
);

2. Call it with a valid MCP request

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "test.echo",
    "arguments": { "message": "A" }
  }
}

Actual Behavior

Inside the handler:

ARGS: {}
args.message === undefined

Debugging shows:

  • extra.request.params.arguments contains { message: "A" } as expected
  • but validation returns {}
  • the normalized schema inside the SDK has an empty shape ({})

The original ZodObject is correct (verified via safeParse), but the SDK abandons it during normalization.


Expected Behavior

Passing a ZodObject should work exactly like passing a shape object:

inputSchema: {
  message: z.string(),
}

Handler should receive:

{ message: "A" }

Root Cause Analysis

The internal zod-compat normalization appears to only support the "shape object" format.

When a full ZodObject is passed:

  • the normalization logic does not detect or extract its shape
  • it constructs a new empty ZodObject({})
  • unknown keys are stripped
  • all user arguments are silently removed

This results in {} being passed to tool handlers, with no warning or error.

This was tested with the recommended zod@3.24.1.


Proposed Fixes

Any of the following would resolve the issue:

Option A — Support ZodObject directly

Extract and use schema._def.shape() instead of discarding the schema.

Option B — Throw a clear error

For example:

inputSchema must be a Zod shape object (e.g. { message: z.string() }), not a ZodObject.

Option C — Auto-convert ZodObject to shape-based schema

Handle ZodObject input by converting it to the shape form expected by zod-compat.


Additional Notes

  • The shape-object format works perfectly.
  • Many developers naturally write z.object(...), so rejecting it silently is a sharp edge.
  • The current behavior causes argument loss without any warning, making debugging extremely difficult.

PS: If helpful, I can provide a minimal reproduction repo or submit a PR addressing the root cause.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions