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 [email protected].


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

    P2Moderate issues affecting some users, edge cases, potentially valuable featurebugSomething isn't workinghelp wantedContributions especially welcome if you have a good knowledge of the codebase and language

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions