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"
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.
Why MCP Security Matters
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
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"),
};
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
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;
}
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.
| Transport | Auth Mechanism | Notes |
|---|---|---|
| stdio | Process-level (OS user) | Inherits permissions of the process owner; no network auth needed |
| Streamable HTTP | Bearer token / OAuth / API key | Must be validated on every request; use HTTPS |
| SSE (legacy) | Bearer token / API key | Validated on initial connection; use HTTPS |
3. Transport Security
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);
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
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) }] };
});
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
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
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
| Area | Requirement | Priority |
|---|---|---|
| Input Validation | Strict Zod schemas on every tool parameter | Critical |
| Input Validation | Path traversal prevention for file operations | Critical |
| Input Validation | SQL injection prevention with parameterized queries | Critical |
| Authentication | Bearer token validation for HTTP transports | Critical |
| Authentication | Role-based access control per tool | High |
| Transport | TLS (HTTPS) for all remote connections | Critical |
| Data Sanitization | Allowlist fields in tool responses | High |
| Data Sanitization | Strip secrets from error messages | High |
| Rate Limiting | Per-client rate limits for HTTP servers | High |
| Audit Logging | Log every tool call with sanitized parameters | Medium |
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.