MCP Server Input Validation Security Patterns
When building MCP servers that interact with external systems, input validation serves as your first line of defense against malicious requests. Poorly validated inputs can lead to injection attacks, data breaches, and unauthorized system access. This guide presents practical patterns for securing your MCP server inputs while maintaining functionality and usability.
Why Input Validation Matters for MCP Servers
MCP servers act as bridges between Claude Code and your backend systems. Every tool call that reaches your server potentially carries user-provided data. Without proper validation, attackers can craft requests designed to exploit vulnerabilities in downstream systems.
Consider a simple MCP server that executes shell commands based on user input. If you pass user data directly to shell execution without validation, you create a command injection vulnerability. The same principle applies to database queries, API calls, and file operations.
Core Validation Strategies
Type Checking and Schema Validation
Define explicit schemas for your tool inputs. Use libraries like Zod or JSON Schema to enforce expected types and structures:
import { z } from 'zod';
const UserQuerySchema = z.object({
userId: z.string().uuid(),
limit: z.number().int().min(1).max(100).default(10),
sortBy: z.enum(['name', 'created', 'lastLogin']).default('created')
});
export async function handleUserQuery(input: unknown) {
const validated = UserQuerySchema.parse(input);
// Proceed with validated data
}
This pattern ensures that only properly structured data reaches your business logic. The validation layer rejects unexpected types before they can cause issues.
Allowlist Validation for Discrete Values
When you know the valid options, use allowlists rather than blocklists. For example, if your tool accepts a status parameter:
const ALLOWED_STATUSES = ['pending', 'active', 'completed', 'archived'];
function validateStatus(status: string): string {
if (!ALLOWED_STATUSES.includes(status)) {
throw new Error(`Invalid status. Allowed: ${ALLOWED_STATUSES.join(', ')}`);
}
return status;
}
Allowlists prevent attackers from discovering new attack vectors by testing unexpected values.
Sanitizing String Inputs
String inputs require special attention because they can contain dangerous characters or patterns. Sanitize based on the context where the string will be used:
function sanitizeForShell(input: string): string {
// Remove or escape characters that could enable command injection
return input
.replace(/[`$]/g, '') // Remove command substitution chars
.replace(/[;&|`$]/g, ''); // Remove shell metacharacters
}
function sanitizeForSql(input: string): string {
// Use parameterized queries instead, but sanitize as backup
return input.replace(/['";]/g, '');
}
function sanitizeFilename(input: string): string {
// Prevent path traversal
return input.replace(/[.\/\\]/g, '_').substring(0, 255);
}
Context-Aware Validation
Validation rules should depend on the calling context. A request from an authenticated user with elevated permissions might pass different checks than an anonymous request.
interface ValidationContext {
userId: string;
userRole: 'reader' | 'editor' | 'admin';
requestSource: 'cli' | 'api' | 'webhook';
}
const PermissionMatrix = {
reader: { canWrite: false, maxQueryLength: 100 },
editor: { canWrite: true, maxQueryLength: 1000 },
admin: { canWrite: true, maxQueryLength: 10000 }
};
function validateWithContext(input: string, context: ValidationContext) {
const limits = PermissionMatrix[context.userRole];
if (input.length > limits.maxQueryLength) {
throw new Error(`Input exceeds ${limits.maxQueryLength} character limit for ${context.userRole} role`);
}
return input;
}
This approach prevents privilege escalation where a user attempts actions beyond their assigned role.
Rate Limiting and Abuse Prevention
Input validation alone cannot prevent all attacks. Implement rate limiting to stop attackers from overwhelming your server with requests:
const rateLimiter = new Map<string, { count: number; resetTime: number }>();
function checkRateLimit(identifier: string, maxRequests: number, windowMs: number): boolean {
const now = Date.now();
const record = rateLimiter.get(identifier);
if (!record || now > record.resetTime) {
rateLimiter.set(identifier, { count: 1, resetTime: now + windowMs });
return true;
}
if (record.count >= maxRequests) {
return false;
}
record.count++;
return true;
}
// Usage in tool handler
if (!checkRateLimit(context.userId, 100, 60000)) {
throw new Error('Rate limit exceeded. Please try again later.');
}
Integrating with Claude Skills
The tdd skill provides excellent patterns for writing tests that verify your validation logic works correctly. Create test cases that check:
- Valid inputs pass through
- Invalid inputs are rejected with clear error messages
- Edge cases like empty strings, extremely long inputs, and special characters
// Test example using tdd patterns
test('rejects invalid UUID format', () => {
expect(() => UserQuerySchema.parse({ userId: 'not-a-uuid' }))
.toThrow();
});
test('accepts valid input', () => {
const result = UserQuerySchema.parse({
userId: '550e8400-e29b-41d4-a716-446655440000',
limit: 50
});
expect(result.limit).toBe(50);
});
After implementing validation, use the pdf skill to generate security audit reports documenting your validation rules and test coverage.
Error Handling Best Practices
Validation failures should provide enough information for legitimate users to fix their requests without revealing implementation details to attackers:
function handleValidationError(error: unknown, context: ValidationContext) {
// Log full details internally
console.error('Validation failed:', {
error,
userId: context.userId,
timestamp: new Date().toISOString()
});
// Return sanitized error to user
if (error instanceof z.ZodError) {
return {
success: false,
message: 'Invalid input format',
details: error.errors.map(e => ({
field: e.path.join('.'),
issue: e.message
}))
};
}
return {
success: false,
message: 'Validation failed'
};
}
Conclusion
Input validation forms the foundation of secure MCP server development. By implementing type checking, allowlists, context-aware validation, and rate limiting, you create multiple layers of defense against malicious requests. Combine these patterns with thorough testing using the tdd skill and comprehensive documentation with the pdf skill to build robust, secure MCP integrations.
Remember that validation is not a one-time implementation but an ongoing process. Review and update your validation rules as new attack vectors emerge and your system evolves.
Related Reading
- MCP Server Permission Auditing Best Practices
- Claude Code MCP Server Setup: Complete Guide 2026
- Integrations Hub
Built by theluckystrike — More at zovo.one