Claude Skills Guide

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:

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

Built by theluckystrike — More at zovo.one