MCP / AI Tools

ZodSchemaBasicsforMCPTools

Quick reference for the Zod patterns used in MCP tool definitions. Covers string, number, enum, optional, default, and describe -- everything you need to define tool input schemas.

Zod is the schema validation library used by the MCP SDK to define tool parameters. Every MCP tool declares its input shape as a Zod object, and the SDK validates incoming parameters before your handler runs.

This guide covers the Zod patterns that show up in MCP tool definitions. It's not a full Zod tutorial — just what you need to define tool inputs.

Install

npm install zod

Basic Types

The building blocks:

import { z } from "zod";

z.string()    // "hello"
z.number()    // 42
z.boolean()   // true
z.object({})  // nested object
z.array(z.string())  // ["a", "b"]

In an MCP tool, the input schema is a plain object with Zod types as values:

server.tool(
  "my_tool",
  "Does something useful.",
  {
    query: z.string(),
    limit: z.number(),
  },
  async ({ query, limit }) => { /* ... */ }
);

.describe()

This is the most important Zod method for MCP. The description tells the AI what each parameter does:

{
  key: z.string().describe("The lookup key"),
  format: z.string().describe("Output format: json or text"),
}

Without .describe(), the AI has to guess what each field means from the name alone. Always add descriptions.

.optional()

Makes a parameter not required:

{
  query: z.string().describe("Search query"),
  limit: z.number().optional().describe("Max results. Defaults to 10."),
}

The AI can call this tool with just {"query": "test"} — no limit needed. In your handler, limit will be undefined if not provided.

.default()

Sets a default value when the parameter is omitted:

{
  text: z.string().describe("Input text"),
  operation: z.string().default("uppercase").describe("Transform operation"),
}

If the AI sends {"text": "hello"}, your handler receives operation as "uppercase". This is different from .optional() — the handler always gets a value.

.enum()

Restricts a string to specific values:

{
  operation: z.enum(["uppercase", "lowercase", "reverse", "slug"])
    .describe("The operation to perform"),
}

If the AI sends an operation not in the list, the SDK rejects it before your handler runs. The enum values are also visible to the AI in the tool schema, so it knows exactly what's valid.

Combining Them

Common patterns in real MCP tools:

// Required string with description
key: z.string().describe("The cache key to look up")

// Optional number with default
limit: z.number().default(10).describe("Max results to return")

// Enum with default
method: z.enum(["GET", "POST", "PUT", "DELETE"])
  .default("GET").describe("HTTP method")

// Optional boolean
include_metadata: z.boolean().optional()
  .describe("Include metadata in response")

Zero-Input Tools

Some tools don't need any input (health checks, system info). Pass an empty object:

server.tool(
  "system_info",
  "Get system information. No input needed.",
  {},
  async () => {
    return {
      content: [{ type: "text", text: JSON.stringify({ status: "ok" }) }],
    };
  }
);

Configuration Notes

  • Descriptions drive AI behavior. A tool with z.string().describe("SQL query to execute") will get very different inputs than z.string().describe("Search keyword"). Be specific.
  • The SDK handles validation. You don't need try/catch around parameter parsing — if the types don't match the schema, the SDK returns an error to the AI before your handler is called.
  • Keep schemas flat. Nested z.object() structures work, but AI models handle flat key-value schemas more reliably than deeply nested ones.
  • Zod version: The MCP SDK works with Zod v3.x. If you're on Zod v4, use the zod/v3 compatibility import.