MCP Server Security: A Practical Guide for Developers
đź“– TL;DR
- MCP servers expose tools that AI agents can call automatically—without human review
- Remote MCP servers are HTTP APIs; treat them with the same security as any public API
- Core protections: authentication, input validation, least-privilege access, and logging
What Is an MCP Server?
The Model Context Protocol (MCP) is an open standard created by Anthropic that allows AI assistants to interact with external tools and data sources. An MCP server is a program that exposes these tools via a standardized interface.
There are two types of MCP servers:
| Type | Transport | Security Context |
|---|---|---|
| Local | STDIO (standard input/output) | Runs on your machine, inherits your permissions |
| Remote | HTTP/SSE | Accessible over the network, requires authentication |
When you run an MCP server locally, it operates within your system's security boundary. But when you host an MCP server remotely, it becomes a network-accessible API that anyone could potentially reach.
Why MCP Security Is Different
Traditional APIs have a human in the loop—someone reviews requests, catches errors, and provides oversight. MCP servers are different because AI agents operate autonomously.
The Automation Problem
When an AI agent interacts with your MCP server:
- It can make hundreds of tool calls per minute
- It follows instructions literally, including malicious prompts
- It won't question suspicious requests or ask for confirmation
- Errors compound quickly without human intervention
This means a single vulnerability can be exploited at machine speed, causing damage before anyone notices.
Real Attack Vectors
Here are specific ways unsecured MCP servers can be compromised:
| Attack | How It Works | Impact |
|---|---|---|
| Path Traversal | Input like ../../etc/passwd escapes safe directories |
Access to system files, credentials, configs |
| Prompt Injection | Malicious instructions embedded in data the AI reads | AI performs unintended actions using your tools |
| Unauthenticated Access | No API key or token verification | Anyone can call your tools directly |
| Over-Privileged Tools | Tools that can delete, modify, or access too much | Accidental or malicious data destruction |
Essential Security Controls
These are the minimum security measures every remote MCP server should implement:
1. Authentication
Verify the identity of every client before processing requests.
// Express middleware for API key auth
app.use((req, res, next) => {
const apiKey = req.headers["authorization"]?.replace("Bearer ", "");
if (!apiKey || apiKey !== process.env.MCP_API_KEY) {
return res.status(401).json({
error: "Invalid or missing API key"
});
}
next();
});
Options: API keys (simple), OAuth 2.0 (for user context), or JWT tokens (for stateless auth).
2. Input Validation
Never trust input from the AI agent. Validate everything.
// Validate file path to prevent traversal
function validateFilePath(userInput, allowedDir) {
// Normalize and resolve the path
const resolved = path.resolve(allowedDir, userInput);
// Ensure it's still within the allowed directory
if (!resolved.startsWith(path.resolve(allowedDir))) {
throw new Error("Path traversal detected");
}
return resolved;
}
Common validations:
- Reject paths containing
..or starting with/ - Sanitize SQL/NoSQL queries to prevent injection
- Validate data types and lengths match expected schemas
- Whitelist allowed values for enum-type parameters
3. Least Privilege
Only expose the minimum tools and permissions needed.
- Read-only by default — Add write permissions only when necessary
- Scope access — Limit file tools to specific directories
- Separate environments — Use different API keys for dev/staging/production
- Time-limited tokens — Expire credentials regularly
4. Rate Limiting
Prevent abuse and runaway AI loops.
import rateLimit from "express-rate-limit";
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: { error: "Rate limit exceeded" }
});
app.use("/mcp", limiter);
5. Logging and Monitoring
You can't secure what you can't see.
Log these events:
- All tool calls with parameters (redact sensitive values)
- Authentication attempts (success and failure)
- Errors and exceptions
- Response times and sizes
Do NOT log:
- Full API keys or tokens
- Passwords or credentials
- Personal identifiable information (PII)
- Full file contents from read operations
Complete Secure Server Example
Here's a production-ready MCP server with all security controls:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import rateLimit from "express-rate-limit";
import { readFile } from "fs/promises";
import path from "path";
const app = express();
app.use(express.json());
// Security: Rate limiting
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
standardHeaders: true,
});
app.use(limiter);
// Security: Authentication middleware
function authenticate(req, res, next) {
const token = req.headers["authorization"]?.replace("Bearer ", "");
if (!token || token !== process.env.MCP_API_KEY) {
console.log(`Auth failed: ${req.ip}`);
return res.status(401).json({ error: "Unauthorized" });
}
next();
}
// Security: Input validation
function validatePath(filename, baseDir) {
if (!filename || typeof filename !== "string") {
throw new Error("Invalid filename");
}
// Block path traversal
if (filename.includes("..") || path.isAbsolute(filename)) {
throw new Error("Invalid path");
}
const resolved = path.resolve(baseDir, filename);
if (!resolved.startsWith(path.resolve(baseDir))) {
throw new Error("Path outside allowed directory");
}
return resolved;
}
// Create MCP server
const server = new McpServer({
name: "secure-file-server",
version: "1.0.0",
});
const SAFE_DIR = process.env.SAFE_DIR || "./public-files";
// Read-only tool with validation
server.tool(
"readFile",
"Read a file from the public directory",
{
filename: {
type: "string",
description: "Filename (no path traversal allowed)",
},
},
async ({ filename }) => {
const safePath = validatePath(filename, SAFE_DIR);
try {
const content = await readFile(safePath, "utf-8");
console.log(`File read: ${filename}`);
return { content: [{ type: "text", text: content }] };
} catch (err) {
throw new Error("File not found or not readable");
}
}
);
// List files tool (read-only)
server.tool(
"listFiles",
"List files in the public directory",
{},
async () => {
const { readdir } = await import("fs/promises");
const files = await readdir(SAFE_DIR);
return {
content: [{ type: "text", text: files.join("\n") }]
};
}
);
// Mount with authentication
app.all("/mcp", authenticate, async (req, res) => {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Secure MCP server running on port ${PORT}`);
});
Security Checklist
Use this checklist before deploying any remote MCP server:
| Check | Status |
|---|---|
| HTTPS enabled (not HTTP) | Required |
| Authentication on all endpoints | Required |
| Input validation on all tool parameters | Required |
| Rate limiting configured | Required |
| Logging enabled (without sensitive data) | Required |
| Tools follow least-privilege principle | Required |
| Error messages don't leak internals | Required |
| API keys stored in environment variables | Required |
Local vs Remote: When to Use Which
| Use Case | Recommendation |
|---|---|
| Personal development tools | Local (STDIO) — simpler, inherits your permissions |
| Team shared tools | Remote with auth — controlled access for team members |
| Public API integration | Remote with strong auth — full security stack required |
| Accessing sensitive systems | Local only — keep within your security boundary |
Key Takeaways
- MCP servers are powerful — they give AI agents real capabilities in your systems
- Remote servers need full API security — authentication, validation, rate limiting, logging
- AI agents don't verify requests — your server must validate everything
- Start with least privilege — only expose what's needed, nothing more
- Log for visibility — you need to see what's happening to respond to issues
Test your MCP server: Try it in our MCP Playground →
Frequently Asked Questions
Do local MCP servers need authentication?
What's the difference between STDIO and HTTP transport?
How do I prevent prompt injection attacks?
Should I use API keys or OAuth for MCP authentication?
What should I do if my MCP server is compromised?
Nikhil Tiwari
15+ years of experience in product development, AI enthusiast, and passionate about building innovative solutions that bridge the gap between technology and real-world applications. Specializes in creating developer tools and platforms that make complex technologies accessible to everyone.