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"
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:
- AI models make mistakes -- they can send wrong types or out-of-range values
- Prompt injection -- malicious content can influence what the AI sends
- Schema evolution -- validation catches issues when schemas change
- Defense in depth -- never trust any input, even from an AI
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
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
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, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
Custom Validators with .refine()
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
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 |