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

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 file demonstrates this pattern by embedding the schema inside the registration configuration.

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 file illustrates this approach with a reusable, documented schema.

// 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 file shows the standard registration pattern.

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 demonstrates this pattern.

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 file demonstrates this idiomatic pattern around line 51.

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.

// 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.

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 and 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 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 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) 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →