# How to Create Custom Zod Schemas for Tool Input Validation in MCP

> Learn to create custom Zod schemas for tool input validation in MCP. Define schemas, attach them to inputSchema, and parse arguments for type-safe execution in handlers.

- Repository: [Model Context Protocol/servers](https://github.com/modelcontextprotocol/servers)
- Tags: how-to-guide
- Published: 2026-03-01

---

**To create custom Zod schemas for tool input validation in MCP, define a `z.object()` schema describing your tool's arguments, attach it to the tool's `inputSchema` configuration property, and explicitly parse incoming arguments using `schema.parse()` inside the handler to guarantee type-safe execution.**

The Model Context Protocol (MCP) servers rely on **Zod** for runtime validation of tool arguments, ensuring that client requests match expected types before execution logic runs. In the `modelcontextprotocol/servers` repository, every tool declares an `inputSchema` that acts as a strict contract between the client and server. Following the patterns established in the reference implementation ensures your tools handle malformed inputs gracefully while providing clear error messages to clients.

## Define a Zod Schema for Tool Arguments

Every MCP tool begins with a schema that describes the expected shape, types, defaults, and documentation of its arguments. You can define these schemas either inline for simple tools or in separate files for complex, reusable configurations.

### Inline Schema Definition for Simple Tools

For straightforward tools with few parameters, define the schema directly in the tool file. The [`src/sequentialthinking/index.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/sequentialthinking/index.ts) file demonstrates this pattern by embedding the schema inside the registration configuration.

```typescript
import { z } from "zod";

const config = {
  title: "Sequential Thinking",
  description: "A tool for dynamic and reflective problem-solving",
  inputSchema: z.object({
    thought: z.string().describe("The current thinking step"),
    thoughtNumber: z.number().min(1).describe("Current step number"),
    totalThoughts: z.number().min(1).describe("Estimated total steps needed"),
    nextThoughtNeeded: z.boolean().describe("Whether another thought is required"),
  }),
};

```

### Separate Schema Files for Complex Tools

When building complex tools or sharing schemas across multiple modules, export the schema from a dedicated file. The [`src/everything/tools/simulate-research-query.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/everything/tools/simulate-research-query.ts) file illustrates this approach with a reusable, documented schema.

```typescript
// src/everything/tools/simulate-research-query-schema.ts
import { z } from "zod";

export const SimulateResearchQuerySchema = z.object({
  topic: z.string().describe("The research topic to investigate"),
  ambiguous: z.boolean().default(false).describe(
    "Simulate an ambiguous query requiring clarification"
  ),
});

```

Use Zod's chaining methods to encode validation rules:
- **`describe()`** – Adds human-readable documentation that appears in client interfaces
- **`default()`** – Sets fallback values for optional arguments
- **`optional()`** – Marks fields as not required
- **`min()`** / **`max()`** – Enforces numeric or string length constraints

## Attach the Schema to Tool Configuration

After defining the schema, associate it with the tool through the `inputSchema` property. The [`src/everything/tools/trigger-sampling-request.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/everything/tools/trigger-sampling-request.ts) file shows the standard registration pattern.

```typescript
import { z } from "zod";

const TriggerSamplingRequestSchema = z.object({
  prompt: z.string().describe("The prompt to send to the LLM"),
  maxTokens: z.number().default(100).describe("Maximum tokens to generate"),
});

const config = {
  title: "Trigger Sampling Request Tool",
  description: "Trigger a Request from the Server for LLM Sampling",
  inputSchema: TriggerSamplingRequestSchema,
};

server.registerTool("trigger-sampling-request", config, async (args, extra) => {
  // Handler implementation
});

```

For task-based tools using the experimental API, attach the schema within the tool definition object. The implementation in [`src/everything/tools/simulate-research-query.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/everything/tools/simulate-research-query.ts) demonstrates this pattern.

```typescript
server.experimental.tasks.registerToolTask(
  "simulate-research-query",
  {
    title: "Simulate Research Query",
    description: "Demonstrates progressive responses and state management",
    inputSchema: SimulateResearchQuerySchema,
    execution: { taskSupport: "required" },
  },
  { /* task handlers */ }
);

```

## Validate Arguments in the Tool Handler

Although MCP validates arguments against the schema before invoking the handler, explicitly parsing inside the handler provides TypeScript type narrowing and future-proofs your code against runtime changes. The [`src/everything/tools/trigger-long-running-operation.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/everything/tools/trigger-long-running-operation.ts) file demonstrates this idiomatic pattern around line 51.

```typescript
server.registerTool(name, config, async (args, extra) => {
  const validated = TriggerLongRunningOperationSchema.parse(args);
  const { duration, steps } = validated;
  
  // Use validated data with full type safety
  return await executeLongOperation(duration, steps, extra);
});

```

If validation fails, Zod throws a `ZodError` containing detailed path and message information. The MCP runtime automatically catches these errors and returns structured error payloads to the client, eliminating the need for manual error handling in most cases.

## Complete Working Example: Calculate Sum Tool

Below is a self-contained implementation following the repository's established patterns. This example creates a `calculate-sum` tool with required and optional numeric arguments.

```typescript
// src/everything/tools/calculate-sum.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";

/* 1. Define custom Zod schema */
const CalculateSumSchema = z.object({
  a: z.number().describe("First number to add"),
  b: z.number().default(0).describe("Second number to add (defaults to 0)"),
});

/* 2. Configure tool metadata */
const name = "calculate-sum";
const config = {
  title: "Calculate Sum",
  description: "Adds two numbers and returns the result.",
  inputSchema: CalculateSumSchema,
};

/* 3. Register with explicit validation */
export const registerCalculateSumTool = (server: McpServer) => {
  server.registerTool(name, config, async (args): Promise<CallToolResult> => {
    const { a, b } = CalculateSumSchema.parse(args);
    const sum = a + b;

    return {
      content: [
        {
          type: "text",
          text: `The sum of ${a} + ${b} = ${sum}`,
        },
      ],
    };
  });
};

```

To activate this tool, import and call the registration function in your server entry point.

```typescript
import { registerCalculateSumTool } from "./tools/calculate-sum.js";

registerCalculateSumTool(server);

```

## Summary

- **Define schemas using `z.object()`** with `.describe()`, `.default()`, and validation chains to create self-documenting, robust input contracts
- **Attach schemas via `inputSchema`** in either standard `registerTool()` configurations or experimental task-based registrations
- **Parse arguments explicitly** using `Schema.parse(args)` inside handlers to obtain TypeScript-narrowed types and ensure runtime safety
- **Reference existing implementations** in [`src/everything/tools/trigger-sampling-request.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/everything/tools/trigger-sampling-request.ts) and [`src/sequentialthinking/index.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/sequentialthinking/index.ts) for authoritative patterns

## Frequently Asked Questions

### Do I need to manually parse arguments if MCP already validates them against the schema?

Yes. While the MCP runtime performs initial validation, explicitly calling `MySchema.parse(args)` inside the handler serves two purposes: it provides TypeScript type narrowing that eliminates `any` types, and it protects against future changes where the runtime might pass unchecked data. This pattern is consistently used in [`src/everything/tools/trigger-long-running-operation.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/everything/tools/trigger-long-running-operation.ts) and other reference implementations.

### How do I handle optional fields and default values in MCP tool schemas?

Use Zod's `.optional()` and `.default()` methods. For optional fields without defaults, append `.optional()` to the type. For fields with fallback values, chain `.default(value)` instead—this marks the field as optional in the input but guarantees a value exists after parsing. The [`trigger-sampling-request.ts`](https://github.com/modelcontextprotocol/servers/blob/main/trigger-sampling-request.ts) file demonstrates this with `z.number().default(100)`.

### Can I reuse Zod schemas across multiple tools or export them for client use?

Absolutely. Export schemas from dedicated files (as shown in [`src/everything/tools/simulate-research-query.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/everything/tools/simulate-research-query.ts)) and import them into multiple tool registrations. This approach maintains consistency across related tools and simplifies maintenance when input requirements change.

### What happens when Zod validation fails in an MCP tool?

When `schema.parse()` encounters invalid data, it throws a `ZodError` detailing exactly which fields failed validation and why. The MCP SDK catches this error automatically and returns a structured error response to the client, including the validation messages defined in your `.describe()` chains, without requiring explicit try-catch blocks in your handler.