How to Use Zod Schemas in MCP TypeScript Servers: Complete Implementation Guide

Define Zod schemas and pass them to the inputSchema property when registering tools via server.registerTool() to enable automatic runtime validation and TypeScript type inference for MCP server inputs.

MCP (Model Context Protocol) TypeScript servers rely on strict input validation to guarantee that tool calls receive well-formed data. According to the ComposioHQ/awesome-codex-skills repository, implementing Zod schemas in MCP TypeScript servers provides both runtime safety and compile-time guarantees through the SDK's automatic validation pipeline.

Architectural Overview

The MCP TypeScript SDK validates tool inputs automatically when you provide Zod schemas through the inputSchema property. This architecture combines runtime validation with static typing by leveraging Zod's infer utility to extract TypeScript types from schema definitions.

The implementation requires five key steps:

  • Import Zod – Bring the validation DSL into scope with import { z } from "zod"
  • Define Schemas – Build strict schemas describing every input field, constraint, and default value
  • Infer Types – Export TypeScript types using z.infer<typeof Schema> for handler signatures
  • Register Tools – Pass Zod objects to inputSchema in server.registerTool() calls
  • Handle Errors – Catch z.ZodError instances when validation fails

According to mcp-builder/reference/node_mcp_server.md, the inputSchema must be a Zod schema object rather than a JSON schema, and the SDK automatically validates incoming JSON against this schema before invoking your handler【/cache/repos/github.com/ComposioHQ/awesome-codex-skills/master/mcp-builder/reference/node_mcp_server.md†L41-L48】.

Step-by-Step Implementation

1. Import Dependencies and Initialize the Server

Start by importing the MCP SDK and Zod. Create an McpServer instance with your server's metadata:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "example-mcp", version: "1.0.0" });

The package.json in the reference template declares "zod": "^3.23.8" as a dependency, ensuring the library is available for runtime validation.

2. Define Strict Input Schemas

Build Zod schemas that describe every parameter, including constraints, defaults, and documentation via .describe(). Use .strict() to forbid extra properties:

enum ResponseFormat {
  MARKDOWN = "markdown",
  JSON = "json",
}

const UserSearchInputSchema = z.object({
  query: z
    .string()
    .min(2, "Query must be at least 2 characters")
    .max(200, "Query must not exceed 200 characters")
    .describe("Search string to match against names/emails"),
  limit: z
    .number()
    .int()
    .min(1)
    .max(100)
    .default(20)
    .describe("Maximum results to return"),
  offset: z
    .number()
    .int()
    .min(0)
    .default(0)
    .describe("Number of results to skip for pagination"),
  response_format: z
    .nativeEnum(ResponseFormat)
    .default(ResponseFormat.MARKDOWN)
    .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable"),
}).strict(); // Enforce no extra fields

The .strict() modifier ensures that unknown properties are rejected, while .nativeEnum() validates that string literals match the TypeScript enum values【/cache/repos/github.com/ComposioHQ/awesome-codex-skills/master/mcp-builder/reference/node_mcp_server.md†L120-L126】.

3. Extract TypeScript Types

Use z.infer to create a TypeScript type that stays synchronized with your runtime schema:

type UserSearchInput = z.infer<typeof UserSearchInputSchema>;

This type is used for the handler's parameter signature, ensuring compile-time safety while the schema provides runtime guarantees【/cache/repos/github.com/ComposioHQ/awesome-codex-skills/master/mcp-builder/reference/node_mcp_server.md†L128-L130】.

4. Register Tools with Validation

Attach the Zod schema to your tool via the inputSchema property in server.registerTool():

server.registerTool(
  "example_search_users",
  {
    title: "Search Example Users",
    description: "Search for users by name, email, or team",
    inputSchema: UserSearchInputSchema, // Zod schema passed here
    annotations: {
      readOnlyHint: true,
      destructiveHint: false,
      idempotentHint: true,
      openWorldHint: true,
    },
  },
  async (params: UserSearchInput) => {
    // Input is already validated by Zod - proceed safely
    const data = await fetchUserData(params);
    return formatResponse(data, params.response_format);
  }
);

The MCP SDK validates incoming JSON against UserSearchInputSchema before executing the handler【/cache/repos/github.com/ComposioHQ/awesome-codex-skills/master/mcp-builder/reference/node_mcp_server.md†L91-L95】.

5. Handle Validation Errors

When validation fails, the SDK returns a z.ZodError with descriptive messages. Implement error handling at the server level or allow the SDK to surface messages to clients:

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP server running on stdio");
}

main().catch((e: z.ZodError | Error) => {
  if (e instanceof z.ZodError) {
    console.error("Validation failed:", e.issues);
  } else {
    console.error("Server error:", e);
  }
});

Complete Working Example

Here is a full implementation demonstrating Zod schema validation in an MCP TypeScript server:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import axios from "axios";

enum ResponseFormat {
  MARKDOWN = "markdown",
  JSON = "json",
}

/* Initialize server */
const server = new McpServer({ name: "example-mcp", version: "1.0.0" });

/* Define strict schema with constraints */
const UserSearchInputSchema = z.object({
  query: z
    .string()
    .min(2, "Query must be at least 2 characters")
    .max(200)
    .describe("Search string to match against names/emails"),
  limit: z.number().int().min(1).max(100).default(20),
  offset: z.number().int().min(0).default(0),
  response_format: z
    .nativeEnum(ResponseFormat)
    .default(ResponseFormat.MARKDOWN)
    .describe("Output format selection"),
}).strict();

type UserSearchInput = z.infer<typeof UserSearchInputSchema>;

/* Register tool with automatic validation */
server.registerTool(
  "search_users",
  {
    title: "Search Users",
    description: "Search users by query parameters",
    inputSchema: UserSearchInputSchema,
  },
  async (params: UserSearchInput) => {
    const response = await axios.get("https://api.example.com/users", {
      params: { q: params.query, limit: params.limit, offset: params.offset }
    });
    
    const content = params.response_format === ResponseFormat.MARKDOWN
      ? formatAsMarkdown(response.data)
      : JSON.stringify(response.data, null, 2);
      
    return { content: [{ type: "text", text: content }] };
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch(console.error);

Advanced Patterns: Schema Composition

For tools that share common parameters like pagination or response formats, define reusable schemas and merge them:

// src/schemas/pagination.ts
export const PaginationSchema = z.object({
  limit: z.number().int().min(1).max(100).default(20),
  offset: z.number().int().min(0).default(0),
}).strict();

// src/schemas/format.ts
export const FormatSchema = z.object({
  response_format: z.nativeEnum(ResponseFormat).default(ResponseFormat.MARKDOWN),
}).strict();

// Combine schemas for specific tools
const ListItemsSchema = PaginationSchema.merge(FormatSchema).extend({
  category: z.string().optional(),
});

type ListItemsInput = z.infer<typeof ListItemsSchema>;

This pattern is documented in the reference guide under "Zod Schemas for Input Validation" where reusable schema blocks are encouraged for maintainable codebases【/cache/repos/github.com/ComposioHQ/awesome-codex-skills/master/mcp-builder/reference/node_mcp_server.md†L71-L77】.

Summary

  • Zod schemas attached to inputSchema provide automatic runtime validation for all MCP tool calls
  • Type inference via z.infer creates TypeScript types that ensure compile-time safety for handler parameters
  • Strict mode (.strict()) prevents excess properties from passing validation, ensuring data integrity
  • Schema composition with .merge() and .extend() allows you to reuse common validation patterns across multiple tools
  • Error handling catches z.ZodError instances when clients send malformed requests

Frequently Asked Questions

Can I use JSON Schema instead of Zod for MCP tools?

No. According to the ComposioHQ/awesome-codex-skills reference implementation, the inputSchema property must be a Zod schema object, not a JSON schema. The MCP TypeScript SDK is built specifically around Zod for runtime validation and type inference【/cache/repos/github.com/ComposioHQ/awesome-codex-skills/master/mcp-builder/reference/node_mcp_server.md†L41-L48】.

How do I add custom validation logic beyond Zod's built-in methods?

Use Zod's .refine() or .transform() methods within your schema definition. For complex validation, you can also validate within the handler after the initial Zod check passes, though this bypasses the automatic error formatting that the SDK provides for Zod errors.

What happens when a client sends invalid data to my MCP server?

The MCP SDK automatically validates incoming requests against your Zod schema before executing the handler. If validation fails, the SDK returns a z.ZodError to the client with descriptive messages indicating which fields failed validation and why. Your handler code never executes for invalid inputs.

How do I share Zod schemas between multiple tools in my MCP server?

Define common schemas in separate files (e.g., src/schemas/pagination.ts) and import them into your tool definitions. Use schema.merge() or schema.extend() to combine base schemas with tool-specific fields. This pattern keeps your code DRY and ensures consistent validation rules across related tools【/cache/repos/github.com/ComposioHQ/awesome-codex-skills/master/mcp-builder/reference/node_mcp_server.md†L71-L77】.

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 →