MCP Security Checklist

Essential security checklist for MCP servers covering input validation, authentication, transport security, and data protection.


title: "MCP Security Checklist" description: "Essential security checklist for MCP servers covering input validation, authentication, transport security, and data protection." order: 4 keywords: ["MCP security", "MCP security checklist", "MCP input validation", "MCP authentication security"] date: "2026-04-01"

Quick Summary

MCP servers sit between AI models and your data, making them a critical security boundary. This checklist covers input validation, authentication, transport security, data sanitization, rate limiting, and audit logging. Whether you build with the official TypeScript SDK (@modelcontextprotocol/sdk) or mcp-framework, every item on this checklist applies.

6security areas every MCP server must address before going to production

Why MCP Security Matters

MCP Security Boundary

An MCP server is a security boundary — it receives instructions from an AI model (which may be influenced by untrusted user input) and executes them against real systems. Every tool call is effectively user input that must be validated, authorized, and constrained.

AI models can be prompted to call tools in unexpected ways. An MCP server that trusts all inputs without validation is vulnerable to prompt injection, data exfiltration, and unauthorized access.

1. Input Validation

Validate Every Input with Zod Schemas

Define strict Zod schemas for every tool parameter. Never accept z.any() or z.unknown() for parameters that will be used in database queries, file paths, or API calls. Use .min(), .max(), .regex(), and .refine() to constrain inputs to exactly what the tool expects.

import { z } from "zod";

// Bad: overly permissive
const badSchema = {
  query: z.string(), // No length limit, no sanitization
  path: z.string(),  // Allows directory traversal
};

// Good: strict validation
const goodSchema = {
  query: z.string()
    .min(1, "Query cannot be empty")
    .max(500, "Query too long")
    .describe("Search query"),
  path: z.string()
    .regex(/^[a-zA-Z0-9_\-./]+$/, "Path contains invalid characters")
    .refine(p => !p.includes(".."), "Directory traversal not allowed")
    .describe("File path within the workspace"),
};
Prevent Path Traversal Attacks

Any tool that accepts file paths must validate that the resolved path stays within the allowed directory. Use path.resolve() and check that the result starts with your root directory. Never trust user-provided paths directly.

import path from "path";

const WORKSPACE_ROOT = "/home/user/workspace";

function validatePath(userPath: string): string {
  const resolved = path.resolve(WORKSPACE_ROOT, userPath);
  if (!resolved.startsWith(WORKSPACE_ROOT)) {
    throw new Error("Access denied: path is outside the workspace.");
  }
  return resolved;
}

2. Authentication & Authorization

Authenticate Before Executing Any Tool

For MCP servers exposed over HTTP (Streamable HTTP or SSE transport), require authentication on every request. Use bearer tokens, API keys, or OAuth tokens validated on the server side. Never rely on the AI model to provide authentication — it is not a trusted principal.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";

function requireAuth(token: string | undefined): void {
  if (!token || !isValidToken(token)) {
    throw new McpError(
      ErrorCode.InvalidParams,
      "Authentication required. Provide a valid API token."
    );
  }
}

// Middleware approach for HTTP transport
function authMiddleware(req: Request): string {
  const auth = req.headers.get("Authorization");
  if (!auth?.startsWith("Bearer ")) {
    throw new Error("Missing Bearer token");
  }
  const token = auth.slice(7);
  if (!isValidToken(token)) {
    throw new Error("Invalid or expired token");
  }
  return token;
}
Implement Least-Privilege Access

Not every user should have access to every tool. Implement role-based access control so that read-only users cannot call write tools, and administrative tools are restricted to admin tokens. Check permissions inside each tool handler.

TransportAuth MechanismNotes
stdioProcess-level (OS user)Inherits permissions of the process owner; no network auth needed
Streamable HTTPBearer token / OAuth / API keyMust be validated on every request; use HTTPS
SSE (legacy)Bearer token / API keyValidated on initial connection; use HTTPS

3. Transport Security

Always Use TLS for Remote Transports

Any MCP server accessible over the network must use HTTPS (TLS). MCP protocol messages may contain sensitive data — database query results, file contents, API responses. Transmitting these over unencrypted HTTP exposes them to interception.

import https from "https";
import fs from "fs";

const server = https.createServer({
  cert: fs.readFileSync("/etc/ssl/certs/server.crt"),
  key: fs.readFileSync("/etc/ssl/private/server.key"),
}, app);
stdio Transport Is Local Only

The stdio transport communicates over standard input/output within a single machine. It does not need TLS because data never crosses a network boundary. However, the server process inherits the OS user's permissions, so ensure it runs under a restricted user account.

4. Data Sanitization

Sanitize Data Before Returning It

Tool responses may contain data from databases, APIs, or files. Before returning this data, strip sensitive fields like passwords, tokens, API keys, and internal identifiers. Define an allowlist of fields to return rather than a blocklist of fields to remove.

function sanitizeUser(raw: any) {
  return {
    id: raw.id,
    name: raw.name,
    email: raw.email,
    role: raw.role,
    // Deliberately omit: password_hash, api_key, internal_notes
  };
}

server.tool("get-user", "Get user profile", {
  userId: z.string(),
}, async ({ userId }) => {
  const raw = await db.getUser(userId);
  const safe = sanitizeUser(raw);
  return { content: [{ type: "text", text: JSON.stringify(safe, null, 2) }] };
});
Prevent SQL Injection

Never interpolate user input directly into SQL queries. Always use parameterized queries or an ORM. This applies to every tool that interacts with a database — whether built with mcp-framework or the official TypeScript SDK.

// Bad: SQL injection vulnerability
const result = await db.query(`SELECT * FROM users WHERE name = '${name}'`);

// Good: parameterized query
const result = await db.query("SELECT * FROM users WHERE name = $1", [name]);

5. Rate Limiting

Rate Limit Tool Calls

For HTTP-based MCP servers, implement rate limiting to prevent abuse. An AI model in a loop could call your tools thousands of times per minute. Set per-client and per-tool rate limits and return a clear error when limits are exceeded.

class RateLimiter {
  private requests = new Map<string, { count: number; resetAt: number }>();

  check(clientId: string, limit: number, windowMs: number): boolean {
    const now = Date.now();
    const entry = this.requests.get(clientId);

    if (!entry || now > entry.resetAt) {
      this.requests.set(clientId, { count: 1, resetAt: now + windowMs });
      return true;
    }

    if (entry.count >= limit) return false;
    entry.count++;
    return true;
  }
}

const limiter = new RateLimiter();

// In tool handler
if (!limiter.check(clientId, 60, 60000)) {
  return {
    content: [{ type: "text", text: "Rate limit exceeded. Maximum 60 requests per minute." }],
    isError: true,
  };
}

6. Audit Logging

Log Every Tool Invocation

Maintain an audit log of every tool call, including the tool name, parameters (sanitized), timestamp, client identifier, and result status (success or error). This is essential for security incident investigation and compliance.

function auditLog(entry: {
  tool: string;
  params: Record<string, unknown>;
  clientId: string;
  status: "success" | "error";
  duration: number;
}) {
  const sanitized = { ...entry.params };
  for (const key of ["password", "token", "secret", "key"]) {
    if (key in sanitized) sanitized[key] = "[REDACTED]";
  }

  // Always log to stderr in MCP servers
  console.error(JSON.stringify({
    type: "audit",
    ...entry,
    params: sanitized,
    timestamp: new Date().toISOString(),
  }));
}

Security Checklist Summary

AreaRequirementPriority
Input ValidationStrict Zod schemas on every tool parameterCritical
Input ValidationPath traversal prevention for file operationsCritical
Input ValidationSQL injection prevention with parameterized queriesCritical
AuthenticationBearer token validation for HTTP transportsCritical
AuthenticationRole-based access control per toolHigh
TransportTLS (HTTPS) for all remote connectionsCritical
Data SanitizationAllowlist fields in tool responsesHigh
Data SanitizationStrip secrets from error messagesHigh
Rate LimitingPer-client rate limits for HTTP serversHigh
Audit LoggingLog every tool call with sanitized parametersMedium
AI Models Are Not Trusted Principals

Never treat the AI model as a trusted user. AI models relay user requests, and those users may attempt prompt injection to manipulate tool calls. Validate every input as if it came from an untrusted external source — because it did. This principle applies equally to servers built with mcp-framework and the official TypeScript SDK.

Frequently Asked Questions