How to Secure MCP Server?

Published: (December 11, 2025 at 12:36 AM EST)
5 min read
Source: Dev.to

Source: Dev.to

Drawing from over a decade and a half in API development, I’ve internalized a critical principle: security must be foundational, not an afterthought. For an MCP server managing your files, data, and critical assets, multi‑layered defense is indispensable. Yet, bolstering security doesn’t equate to increased complexity. This article outlines the implementation of four vital security safeguards:

  • Rigorous input validation to thwart malicious data
  • Robust authentication to verify user identities
  • Precise authorization to define access privileges
  • Intelligent resource limiting to prevent misuse

Upon completion, your server will be primed for a production environment.

Core Principles of MCP Server Security

Before diving into code, let’s establish our defense‑in‑depth strategy.

1. Input Validation

Principle: Treat all incoming data with suspicion. Rigorously validate, sanitize, and verify every piece of information.

Rationale: Inadequate parameter validation can open doors to critical vulnerabilities, such as directory traversal attacks (e.g., accessing ../../etc/passwd), code injection exploits, or server instability.

2. Authentication

Principle: Confirm the identity of every entity interacting with your server. Every incoming request needs to be linked to a confirmed and verified identity.

Rationale: Lacking authentication leaves your tools completely exposed to unauthorized access, much like an unlocked front door invites uninvited guests.

3. Authorization

Principle: Validate specific access rights. Even after successful authentication, users should only be permitted to perform actions aligned with their roles.

Rationale: An intern, for instance, has no business accessing human‑resources records. Implementing fine‑grained permissions is key to safeguarding sensitive data.

4. Resource Limiting

Principle: Establish clear quotas, set size caps, and implement connection timeouts.

Rationale: These measures are crucial to prevent a single malicious actor or an unforeseen error from overwhelming your server with an exorbitant number of requests, such as 10 000 per second.

Robust Input Validation

We begin with perhaps the most critical defense: validating all incoming data. Implement this by creating src/security/validator.ts as follows:

// src/security/validator.ts
import path from 'path';
import { InputSchema } from '../mcp/protocol';

/**
 * Validation error
 */
export class ValidationError extends Error {
  constructor(
    message: string,
    public field?: string,
    public expected?: string
  ) {
    super(message);
    this.name = 'ValidationError';
  }
}

/**
 * Parameter validator based on JSON Schema
 */
export class ParameterValidator {
  /**
   * Validate parameters according to schema
   */
  static validate(params: any, schema: InputSchema): void {
    // Check that params is an object
    if (typeof params !== 'object' || params === null) {
      throw new ValidationError('Parameters must be an object');
    }

    // Check required fields
    for (const requiredField of schema.required) {
      if (!(requiredField in params)) {
        throw new ValidationError(
          `Field '${requiredField}' is required`,
          requiredField
        );
      }
    }

    // Validate each property
    for (const [fieldName, fieldValue] of Object.entries(params)) {
      const fieldSchema = schema.properties[fieldName];

      if (!fieldSchema) {
        throw new ValidationError(
          `Field '${fieldName}' is not allowed`,
          fieldName
        );
      }

      this.validateField(fieldName, fieldValue, fieldSchema);
    }
  }

  /**
   * Validate a specific field
   */
  private static validateField(
    fieldName: string,
    value: any,
    schema: any
  ): void {
    // Type validation
    const actualType = typeof value;
    const expectedType = schema.type;

    if (expectedType === 'string' && actualType !== 'string') {
      throw new ValidationError(
        `Field '${fieldName}' must be a string`,
        fieldName,
        expectedType
      );
    }

    if (expectedType === 'number' && actualType !== 'number') {
      throw new ValidationError(
        `Field '${fieldName}' must be a number`,
        fieldName,
        expectedType
      );
    }

    if (expectedType === 'boolean' && actualType !== 'boolean') {
      throw new ValidationError(
        `Field '${fieldName}' must be a boolean`,
        fieldName,
        expectedType
      );
    }

    // Enumeration validation
    if (schema.enum && !schema.enum.includes(value)) {
      throw new ValidationError(
        `Field '${fieldName}' must be one of: ${schema.enum.join(', ')}`,
        fieldName
      );
    }

    // Length validation for strings
    if (expectedType === 'string') {
      if (schema.minLength && value.length < schema.minLength) {
        throw new ValidationError(
          `Field '${fieldName}' must be at least ${schema.minLength} characters`,
          fieldName
        );
      }
      if (schema.maxLength && value.length > schema.maxLength) {
        throw new ValidationError(
          `Field '${fieldName}' cannot exceed ${schema.maxLength} characters`,
          fieldName
        );
      }
    }

    // Range validation for numbers
    if (expectedType === 'number') {
      if (schema.minimum !== undefined && value < schema.minimum) {
        throw new ValidationError(
          `Field '${fieldName}' cannot be less than ${schema.minimum}`,
          fieldName
        );
      }
      if (schema.maximum !== undefined && value > schema.maximum) {
        throw new ValidationError(
          `Field '${fieldName}' cannot exceed ${schema.maximum}`,
          fieldName
        );
      }
    }

    // Pattern validation for strings
    if (expectedType === 'string' && schema.pattern) {
      const regex = new RegExp(schema.pattern);
      if (!regex.test(value)) {
        throw new ValidationError(
          `Field '${fieldName}' doesn't match expected format`,
          fieldName
        );
      }
    }
  }
}

/**
 * File path validator
 */
export class PathValidator {
  private allowedDirectories: string[];
  private blockedPaths: string[];

  constructor(allowedDirectories: string[], blockedPaths: string[] = []) {
    // Resolve all paths to absolute
    this.allowedDirectories = allowedDirectories.map(dir => path.resolve(dir));
    this.blockedPaths = blockedPaths.map(p => path.resolve(p));
  }

  /**
   * Validate that a path is safe
   */
  validatePath(filePath: string): string {
    // Resolve absolute path
    const absolutePath = path.resolve(filePath);

    // Check path traversal (../)
    if (absolutePath.includes('..')) {
      throw new ValidationError(
        'Paths with ".." are not allowed (path traversal)'
      );
    }

    // Check that path is in an allowed directory
    const isInAllowedDir = this.allowedDirectories.some(dir =>
      absolutePath.startsWith(dir)
    );

    if (!isInAllowedDir) {
      throw new ValidationError('Path is outside allowed directories');
    }

    // Optional: block explicitly blacklisted paths
    if (this.blockedPaths.includes(absolutePath)) {
      throw new ValidationError('Path is explicitly blocked');
    }

    return absolutePath;
  }
}
Back to Blog

Related posts

Read more »