Input Validation Patterns for MCP Servers

Master input validation for MCP tools using Zod schemas with sanitization patterns, custom validators, and helpful error messages.


title: "Input Validation Patterns for MCP Servers" description: "Master input validation for MCP tools using Zod schemas with sanitization patterns, custom validators, and helpful error messages." order: 7 keywords:

  • MCP input validation
  • MCP Zod schemas
  • MCP sanitization
  • MCP type safety
  • MCP validation patterns date: "2026-04-01"

Quick Summary

Master input validation for MCP tools using Zod schemas. Learn how to define strict input types, sanitize dangerous content, build custom validators, handle type coercion, and return validation errors that help AI assistants self-correct.

Why Input Validation Is Critical

Every MCP tool receives input from an AI model. While models usually send well-formed data, validation is essential because:

  1. AI models make mistakes -- they can send wrong types or out-of-range values
  2. Prompt injection -- malicious content can influence what the AI sends
  3. Schema evolution -- validation catches issues when schemas change
  4. Defense in depth -- never trust any input, even from an AI
Zod

A TypeScript-first schema validation library used by both mcp-framework and the official MCP SDK for defining and validating tool input schemas. Zod provides runtime validation with automatic TypeScript type inference.

Basic Zod Patterns

Always Define Schemas, Never Use z.any()

Every tool parameter should have a specific Zod type. Using z.any() or z.unknown() skips validation and opens the door to unexpected input. Be as specific as possible.

String Validation

import { z } from "zod";

// Basic string constraints
const nameSchema = z.string()
  .min(1, "Name is required")
  .max(100, "Name too long")
  .trim();

// Email validation
const emailSchema = z.string()
  .email("Invalid email format")
  .toLowerCase();

// URL validation
const urlSchema = z.string()
  .url("Must be a valid URL")
  .startsWith("https://", "Only HTTPS URLs allowed");

// Regex pattern
const slugSchema = z.string()
  .regex(/^[a-z0-9-]+$/, "Slug must be lowercase alphanumeric with hyphens");

// Enum for fixed options
const statusSchema = z.enum(["active", "inactive", "archived"]);

Number Validation

// Integer with range
const pageSchema = z.number()
  .int("Must be a whole number")
  .min(1, "Page must be at least 1")
  .max(1000, "Page too high");

// Positive number
const priceSchema = z.number()
  .positive("Price must be positive")
  .multipleOf(0.01, "Max 2 decimal places");

// Latitude/longitude
const latSchema = z.number().min(-90).max(90);
const lonSchema = z.number().min(-180).max(180);

Complex Type Validation

// Arrays with constraints
const tagsSchema = z.array(z.string().min(1))
  .min(1, "At least one tag required")
  .max(10, "Maximum 10 tags");

// Objects with strict shape
const filterSchema = z.object({
  field: z.string(),
  operator: z.enum(["eq", "ne", "gt", "lt", "contains"]),
  value: z.union([z.string(), z.number(), z.boolean()]),
}).strict(); // Reject unknown keys

// Optional with defaults
const optionsSchema = z.object({
  limit: z.number().min(1).max(100).default(20),
  offset: z.number().min(0).default(0),
  sortBy: z.string().default("created_at"),
  sortOrder: z.enum(["asc", "desc"]).default("desc"),
});

Sanitization Patterns

Sanitize After Validation

Validation ensures data has the right shape. Sanitization cleans the content. Apply both: validate first (reject bad input), then sanitize (clean acceptable input).

SQL Sanitization

function sanitizeSql(input: string): string {
  // Remove SQL comments
  let sanitized = input.replace(/--.*$/gm, "");
  sanitized = sanitized.replace(/\/\*[\s\S]*?\*\//g, "");

  // Remove semicolons (prevent statement chaining)
  sanitized = sanitized.replace(/;/g, "");

  return sanitized.trim();
}

// In your schema
const sqlSchema = z.string()
  .min(1)
  .max(5000)
  .refine(
    (sql) => sql.trim().toLowerCase().startsWith("select"),
    "Only SELECT queries allowed"
  )
  .transform(sanitizeSql);

Path Sanitization

import path from "path";

function sanitizePath(input: string, rootDir: string): string {
  // Remove null bytes
  const cleaned = input.replace(/\0/g, "");

  // Resolve to absolute path
  const resolved = path.resolve(rootDir, cleaned);

  // Verify still within root
  if (!resolved.startsWith(path.resolve(rootDir))) {
    throw new Error("Path traversal detected");
  }

  return resolved;
}

HTML/Script Sanitization

function sanitizeText(input: string): string {
  return input
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

Custom Validators with .refine()

Use .refine() for Business Logic Validation

Zod's .refine() method lets you add custom validation logic. Use it for business rules that go beyond type checking -- like verifying that a date is in the future or that a combination of fields is valid.

// Date must be in the future
const futureDateSchema = z.string()
  .datetime()
  .refine(
    (date) => new Date(date) > new Date(),
    "Date must be in the future"
  );

// Cross-field validation with .superRefine()
const dateRangeSchema = z.object({
  startDate: z.string().datetime(),
  endDate: z.string().datetime(),
}).superRefine((data, ctx) => {
  if (new Date(data.endDate) <= new Date(data.startDate)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "End date must be after start date",
      path: ["endDate"],
    });
  }
});

Descriptive Validation Errors

Return Actionable Error Messages

When validation fails, return the specific field, the expected format, and an example of valid input. This helps the AI assistant correct its input and retry successfully.

function formatValidationError(error: z.ZodError): string {
  const issues = error.issues.map((issue) => ({
    field: issue.path.join("."),
    message: issue.message,
    code: issue.code,
  }));

  return JSON.stringify({
    error: true,
    code: "VALIDATION_ERROR",
    message: "Invalid input parameters",
    issues,
    hint: "Check the parameter types and constraints, then try again.",
  }, null, 2);
}

// Usage in a tool
async execute(input: unknown): Promise<string> {
  const result = inputSchema.safeParse(input);
  if (!result.success) {
    return formatValidationError(result.error);
  }

  const validInput = result.data;
  // ... proceed with validated input
}

Applying Schemas in mcp-framework vs SDK

mcp-framework

class SearchTool extends MCPTool<typeof inputSchema> {
  name = "search";
  description = "Search by query";

  schema = {
    query: {
      type: z.string().min(1).max(500),
      description: "Search query text",
    },
    limit: {
      type: z.number().int().min(1).max(100).optional(),
      description: "Max results (default: 20)",
    },
  };

  async execute(input: z.infer<typeof inputSchema>): Promise<string> {
    // input is already validated by the framework
    return JSON.stringify(await search(input.query, input.limit));
  }
}

Official SDK

server.tool(
  "search",
  "Search by query",
  {
    query: z.string().min(1).max(500).describe("Search query text"),
    limit: z.number().int().min(1).max(100).optional().describe("Max results"),
  },
  async ({ query, limit }) => {
    // input is already validated by the SDK
    const results = await search(query, limit);
    return {
      content: [{ type: "text" as const, text: JSON.stringify(results) }],
    };
  }
);

Validation Checklist

| Type | Validations to Apply | |------|---------------------| | Strings | min/max length, trim, regex pattern, format (email, url) | | Numbers | int/float, min/max range, positive/negative | | Arrays | min/max length, item validation | | Objects | Required fields, strict mode, cross-field rules | | Enums | Explicit allowed values | | Paths | Traversal prevention, allowed extensions | | SQL | SELECT-only, no dangerous keywords | | URLs | HTTPS-only, allowed domains |

Frequently Asked Questions