claude-code fix beginner 5 minutes

MCP Tool Schemas Accept Plain Objects — But Only Zod Actually Works

No error. No warning. The tool registers fine, the server starts, and every input is undefined. This is the kind of silent failure that makes you debug the wrong layer for an hour before you suspect the schema format.

Two config files that look the same but behave differently was the last MCP gotcha. This one is worse — two schema formats that both register without error, but only one of them works.

The MCP SDK’s mcpServer.tool() method accepts a schema object as its third argument. That object defines the parameters your tool expects. You can pass it as a plain JSON Schema object or as Zod validators. The SDK doesn’t complain either way. But only Zod actually wires up the input parsing at runtime.

The Problem

You define an MCP tool with a plain JavaScript object describing the parameter types:

mcpServer.tool(
  "my_tool",
  "Does something useful.",
  {
    domain: {
      type: "string",
      description: "Domain to scan (e.g. example.com)"
    }
  },
  async ({ domain }) => {
    console.log(domain); // undefined
    // ... your tool logic
  }
);

The server starts. The tool appears in your MCP client’s tool list. Claude even calls it with the right arguments in the request payload. But inside your handler, domain is undefined.

No error is thrown. No warning is logged. The tool just doesn’t receive its inputs.

Why This Happens

The MCP SDK uses Zod internally for schema validation and input parsing. When you pass a plain JSON Schema object, the SDK stores it as metadata (it appears in the tool listing), but it doesn’t create a Zod parser from it. At runtime, the incoming tool call payload is validated against… nothing. The destructured parameters arrive empty.

This is a type-level mismatch that JavaScript can’t catch. The SDK’s TypeScript types allow either format in the signature, but the runtime code path only processes Zod schemas.

The Fix

Replace plain JSON Schema objects with Zod validators:

import { z } from "zod";

mcpServer.tool(
  "my_tool",
  "Does something useful.",
  { domain: z.string().describe("Domain to scan (e.g. example.com)") },
  async ({ domain }) => {
    console.log(domain); // "example.com" ✓
    // ... your tool logic
  }
);

That’s the whole fix. One import, one syntax change per parameter.

If you have multiple parameters:

mcpServer.tool(
  "search",
  "Search for items.",
  {
    query: z.string().describe("Search query"),
    limit: z.number().optional().describe("Max results (default 10)"),
  },
  async ({ query, limit }) => {
    // Both are properly parsed
  }
);

Install Zod

If you don’t already have it:

npm install zod

Then add the import at the top of your server file:

import { z } from "zod";

How to Tell If You’re Affected

If your MCP tool:

  1. Registers successfully (appears in /mcp status or your client’s tool list)
  2. Gets called by the LLM with the correct arguments (visible in request logs)
  3. Receives undefined for all parameters inside the handler

You’re using plain objects. Switch to Zod.

The Pattern Going Forward

Every MCP tool parameter should use Zod. The .describe() method on Zod types serves double duty — it provides the parameter description for the tool listing and enables runtime validation:

z.string().describe("Human-readable description")
z.number().optional().describe("Optional numeric param")
z.enum(["a", "b", "c"]).describe("One of the allowed values")
z.boolean().default(false).describe("Flag with default")

This isn’t a preference — it’s a requirement for the SDK to work correctly. The plain object syntax should probably throw an error, but it doesn’t. Now you know.

FAQ

Why are my MCP tool inputs undefined even though the tool registered successfully?

You're likely using plain JSON Schema objects for the parameter definition. The MCP SDK's mcpServer.tool() method accepts both plain objects and Zod schemas without error, but only Zod schemas actually validate and pass inputs at runtime. Plain objects register the tool but silently skip input parsing — all arguments arrive as undefined.

Do I need to install Zod separately for MCP tool schemas?

Yes. Install it as a dependency: npm install zod. Then import it at the top of your server file: import { z } from 'zod'. Use z.string(), z.number(), etc. for your tool parameter definitions instead of { type: 'string' } plain objects.

How do I define MCP tool parameters correctly?

Use Zod schemas in the third argument to mcpServer.tool(). Each parameter should be a Zod type with a .describe() call for documentation. Example: { domain: z.string().describe('Domain to scan') }. The SDK uses Zod's runtime validation to parse incoming tool calls.