MCP Security Best Practices
Essential security guidelines for building and configuring MCP servers, including API key management, input validation, and least-privilege access.
Security is critical when connecting AI assistants to your tools and data. MCP servers have access to whatever resources you grant them — a misconfigured server can expose sensitive data or allow unintended operations. This guide covers the essential security practices for MCP.
1. Environment Variables for Secrets
Never hardcode API keys, tokens, or passwords in your MCP server source code or configuration files.
Bad
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["server.js"],
"env": {
"API_KEY": "sk-1234567890abcdef"
}
}
}
}
This embeds the key in claude_desktop_config.json, which might be committed to version control or exposed on screen shares.
Better
Use a .env file:
# .env (never commit this file)
API_KEY=sk-1234567890abcdef
Reference it in your server:
const apiKey = process.env.API_KEY;
And configure your MCP server to load it. The config file references the env var name only:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["-r", "dotenv/config", "server.js"],
"env": {
"API_KEY": "${API_KEY}"
}
}
}
}
Best
Use a secrets manager or OS keychain. On macOS:
security add-generic-password -s "mcp-my-server" -a "API_KEY" -w "sk-1234567890abcdef"
Then in your server:
import { execSync } from 'child_process';
const apiKey = execSync(
`security find-generic-password -s "mcp-my-server" -a "API_KEY" -w`
).toString().trim();
2. Least-Privilege IAM Policies
When using AWS MCP servers, grant only the permissions your server needs.
Bad
{
"Effect": "Allow",
"Action": ["s3:*"],
"Resource": ["*"]
}
Good
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-app-data",
"arn:aws:s3:::my-app-data/*"
]
}
For DynamoDB, restrict to specific tables and operations:
{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:Query"],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/Users"
}
3. Input Validation
MCP tools receive arguments from AI clients. Always validate inputs before processing them.
from pydantic import BaseModel, Field
class FileReadRequest(BaseModel):
path: str = Field(..., description="Path to read")
@classmethod
def validate_path(cls, path: str) -> str:
# Prevent directory traversal
if ".." in path or path.startswith("/"):
raise ValueError(f"Invalid path: {path}")
return path
For file system servers, always sanitize paths:
import { resolve, relative } from 'path';
const ALLOWED_ROOT = resolve('./data');
function safePath(userPath: string): string {
const full = resolve(ALLOWED_ROOT, userPath);
if (!full.startsWith(ALLOWED_ROOT)) {
throw new Error('Path traversal detected');
}
return full;
}
4. Rate Limiting
Prevent abuse by limiting how often tools can be called:
import time
from functools import wraps
rate_limits: dict[str, list[float]] = {}
def rate_limit(max_calls: int, per_seconds: int):
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
now = time.time()
key = func.__name__
if key not in rate_limits:
rate_limits[key] = []
rate_limits[key] = [t for t in rate_limits[key] if now - t < per_seconds]
if len(rate_limits[key]) >= max_calls:
raise ToolError(f"Rate limit exceeded. Try again in {per_seconds} seconds.")
rate_limits[key].append(now)
return await func(*args, **kwargs)
return wrapper
return decorator
@server.tool()
@rate_limit(max_calls=10, per_seconds=60)
async def expensive_operation(data: str) -> str:
"""An expensive operation with rate limiting"""
return f"Processed: {data}"
5. Read-Only Mode for Sensitive Resources
If a tool only needs to read data, don’t expose write operations:
@server.tool()
async def read_document(path: str) -> str:
"""Read a document (read-only)"""
safe = safePath(path)
with open(safe, 'r') as f:
return f.read()
# Don't add a write_document tool unless needed
6. Audit Logging
Log all tool calls and resource accesses:
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("mcp-server")
@server.tool()
async def delete_user(user_id: str) -> dict:
"""Delete a user from the system"""
logger.warning(f"DELETE USER requested: {user_id}")
result = database.delete_user(user_id)
logger.info(f"User {user_id} deleted by {os.environ.get('USER', 'unknown')}")
return {"deleted": user_id, "status": "success"}
7. Network Restrictions
For MCP servers that connect to external services:
- Use private VPC endpoints for AWS services
- Whitelist specific IP ranges for API calls
- Never expose MCP servers directly to the internet (they communicate over stdio)
8. Regular Audits
Periodically review your MCP configuration:
- Which servers are running?
- What permissions do they have?
- Are there any unused servers?
- Are API keys rotated regularly?
Security Checklist
- No secrets in source code or config files
- IAM policies follow least privilege
- Input validation prevents injection attacks
- Rate limiting is configured
- Audit logging is enabled
- Network access is restricted
- Secrets are rotated at least every 90 days
- Unused servers are removed from config
Next Steps
- Use the MCPConfig Builder to generate secure configurations
- Validate your config with the Validator
- Browse server templates that follow security best practices