MCP Server Security Best Practices
MCP Server Security Best Practices
Model Context Protocol (MCP) servers give AI agents access to your tools, data, and infrastructure. This power demands careful security design. A poorly secured MCP server can expose sensitive data, allow SSRF attacks, or let agents execute unintended operations.
Authentication and Authorization
Require Authentication
Never run an MCP server without authentication in production. Use API keys or OAuth tokens:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({
name: "secure-server",
version: "1.0.0",
});
// Validate auth on every request
function validateAuth(request: Request): boolean {
const apiKey = request.headers.get("Authorization")?.replace("Bearer ", "");
if (!apiKey) return false;
return isValidApiKey(apiKey); // Check against stored hashed keys
}
Scope Permissions
Not every client needs access to every tool. Implement role-based access:
const permissions: Record = {
readonly: ["search", "get_document"],
editor: ["search", "get_document", "update_document"],
admin: ["search", "get_document", "update_document", "delete_document"],
};
function canAccessTool(role: string, toolName: string): boolean {
return permissions[role]?.includes(toolName) ?? false;
}
Input Validation
Validate Every Parameter
AI agents can pass unexpected inputs. Validate and sanitize everything:
import { z } from "zod";
server.tool(
"query_database",
{
table: z.enum(["users", "posts", "comments"]), // Whitelist tables
limit: z.number().int().min(1).max(100).default(10),
filter: z.string().max(500).optional(),
},
async ({ table, limit, filter }) => {
// Safe — table is from enum, limit is bounded
const results = await db.select(table, { limit, filter });
return { content: [{ type: "text", text: JSON.stringify(results) }] };
}
);
Prevent Injection
Never interpolate user input into SQL, shell commands, or URLs:
// DANGEROUS — SQL injection
const query = SELECT * FROM users WHERE name = '\${name}';
// SAFE — parameterized query
const results = await db.query("SELECT * FROM users WHERE name = $1", [name]);
SSRF Prevention
If your MCP server fetches URLs, restrict what it can access:
import { URL } from "url";
import dns from "dns/promises";
async function isSafeUrl(urlString: string): Promise {
const url = new URL(urlString);
// Block private/internal networks
const blockedHosts = ["localhost", "127.0.0.1", "0.0.0.0", "169.254.169.254"];
if (blockedHosts.includes(url.hostname)) return false;
// Block non-HTTP protocols
if (!["http:", "https:"].includes(url.protocol)) return false;
// Resolve DNS and check for internal IPs
const addresses = await dns.resolve4(url.hostname);
for (const addr of addresses) {
if (addr.startsWith("10.") || addr.startsWith("172.") || addr.startsWith("192.168.")) {
return false;
}
}
return true;
}
Sandboxing and Resource Limits
Limit Execution Time
Set timeouts on all operations to prevent resource exhaustion:
async function withTimeout(fn: () => Promise, ms: number): Promise {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
return await fn();
} finally {
clearTimeout(timer);
}
}
Rate Limit Tool Calls
Apply per-client rate limits to prevent abuse:
const rateLimits = new Map();
function checkRateLimit(clientId: string, maxPerMinute: number): boolean {
const now = Date.now();
const entry = rateLimits.get(clientId) ?? { count: 0, resetAt: now + 60000 };
if (now > entry.resetAt) {
entry.count = 0;
entry.resetAt = now + 60000;
}
entry.count++;
rateLimits.set(clientId, entry);
return entry.count <= maxPerMinute;
}
Logging and Auditing
Log every tool invocation with context for security auditing:
function logToolCall(clientId: string, tool: string, params: unknown) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
clientId,
tool,
params,
level: "info",
}));
}
Conclusion
Secure MCP servers with authentication, input validation, SSRF prevention, resource limits, and comprehensive logging. The AI agent should only be able to do what you explicitly allow — treat every tool call as potentially adversarial input and validate accordingly.