title: 'Security Considerations' description: 'Securing your MCP servers and protecting sensitive operations'
Security Considerations
MCP servers often access sensitive data and perform critical operations. Security must be a core consideration from the start. In this lesson, we'll explore essential security practices for MCP servers.
The AI Trust Boundary
A critical concept: Never trust AI-generated input without validation.
When an AI calls your tools, the arguments it provides are AI-generated. They might be:
- Malformed (the AI misunderstood the schema)
- Malicious (if the AI was manipulated through prompt injection)
- Inappropriate (the AI made poor decisions)
Always validate inputs as if they came from an untrusted external API.
Input Validation
Schema Validation
JSON Schema provides a first line of defense, but don't rely on it exclusively:
// JSON Schema ensures structure
const schema = {
type: "object",
properties: {
email: { type: "string", format: "email" },
age: { type: "integer", minimum: 0, maximum: 150 }
},
required: ["email", "age"]
};
// But add application-level validation too
function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error("Invalid email format");
}
// Additional checks: length, domain whitelist, etc.
if (email.length > 254) {
throw new Error("Email too long");
}
return true;
}
SQL Injection Prevention
Always use parameterized queries:
// WRONG - Vulnerable to SQL injection
const query = `SELECT * FROM users WHERE email = '${email}'`;
// RIGHT - Parameterized query
const query = "SELECT * FROM users WHERE email = ?";
const results = await db.query(query, [email]);
In Python with SQLite:
# WRONG
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")
# RIGHT
cursor.execute("SELECT * FROM users WHERE email = ?", (email,))
Path Traversal Prevention
Validate file paths to prevent directory traversal:
import path from "path";
function validateFilePath(userPath: string, baseDir: string): string {
// Resolve to absolute path
const resolvedPath = path.resolve(baseDir, userPath);
// Ensure it's within baseDir
if (!resolvedPath.startsWith(baseDir)) {
throw new Error("Access denied: path outside allowed directory");
}
return resolvedPath;
}
// Usage
const safePath = validateFilePath(
request.arguments.path,
"/allowed/directory"
);
const content = await fs.readFile(safePath, "utf-8");
Authentication & Authorization
Client Authentication
For remote MCP servers (HTTP/SSE), implement authentication:
import { createServer } from "http";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
const server = createServer(async (req, res) => {
// Check authentication header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Unauthorized" }));
return;
}
const token = authHeader.substring(7);
// Validate token
const user = await validateToken(token);
if (!user) {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Invalid token" }));
return;
}
// Proceed with MCP handling
// ...
});
Tool-Level Authorization
Different users may have different permissions:
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
const { name, arguments: args } = request.params;
// Extract user from context
const user = extra.user;
if (name === "delete_database") {
// Only admins can delete
if (!user.roles.includes("admin")) {
throw new Error("Permission denied: admin role required");
}
}
if (name === "view_salary") {
// Only HR or the user themselves
const targetUserId = args.userId;
if (!user.roles.includes("hr") && user.id !== targetUserId) {
throw new Error("Permission denied: cannot view other salaries");
}
}
// Execute tool
// ...
});
Secrets Management
Never hardcode secrets in your server code:
// WRONG
const apiKey = "sk-1234567890abcdef";
// RIGHT
const apiKey = process.env.API_KEY;
if (!apiKey) {
throw new Error("API_KEY environment variable not set");
}
For local development, use .env files (and add .env to .gitignore):
# .env
DATABASE_URL=postgresql://localhost/mydb
API_KEY=sk-1234567890abcdef
import dotenv from "dotenv";
dotenv.config();
const dbUrl = process.env.DATABASE_URL;
Rate Limiting
Prevent abuse through rate limiting:
const rateLimits = new Map();
function checkRateLimit(userId: string): boolean {
const now = Date.now();
const userLimits = rateLimits.get(userId) || { count: 0, resetAt: now + 60000 };
if (now > userLimits.resetAt) {
// Reset window
userLimits.count = 0;
userLimits.resetAt = now + 60000;
}
if (userLimits.count >= 100) { // 100 requests per minute
return false;
}
userLimits.count++;
rateLimits.set(userId, userLimits);
return true;
}
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
const userId = extra.user.id;
if (!checkRateLimit(userId)) {
throw new Error("Rate limit exceeded. Please try again later.");
}
// Process request
// ...
});
Logging & Audit Trails
Log security-relevant events:
import winston from "winston";
const logger = winston.createLogger({
level: "info",
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: "audit.log" }),
],
});
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
const { name, arguments: args } = request.params;
const user = extra.user;
// Log tool calls
logger.info("Tool called", {
tool: name,
user: user.id,
timestamp: new Date().toISOString(),
arguments: args, // Be careful logging sensitive data
});
// Execute tool
// ...
});
Transport Security
Use TLS for Remote Servers
Always use HTTPS for remote MCP servers:
import https from "https";
import fs from "fs";
const options = {
key: fs.readFileSync("private-key.pem"),
cert: fs.readFileSync("certificate.pem"),
};
https.createServer(options, app).listen(443);
stdio Security
For stdio servers, security comes from process isolation. Ensure:
- The server process runs with minimal privileges
- File system access is restricted to necessary directories
- Environment variables don't expose secrets to subprocesses
Principle of Least Privilege
Only expose what's necessary:
// WRONG - Overly powerful tool
{
name: "execute_sql",
description: "Execute arbitrary SQL"
}
// RIGHT - Specific, limited tools
{
name: "search_users",
description: "Search users by name or email"
},
{
name: "update_user_email",
description: "Update a user's email address"
}
Security Checklist
Before deploying an MCP server:
- [ ] All inputs validated beyond JSON Schema
- [ ] SQL queries use parameters, not string concatenation
- [ ] File paths validated against directory traversal
- [ ] Secrets loaded from environment, not hardcoded
- [ ] Authentication required for remote access
- [ ] Authorization checks on sensitive operations
- [ ] Rate limiting implemented
- [ ] Security events logged
- [ ] TLS/HTTPS for remote servers
- [ ] Dependencies up to date (run
npm auditorpip check)
Security is not optional. In the next module, we'll explore production patterns including error handling, observability, and deployment strategies.