MCP Security Best Practices
Comprehensive security guide for MCP servers: sandboxing, authentication, rate limiting, input validation, and production-ready patterns to protect your AI integrations.
SECURITY THREAT MODEL
- Credential leakage: API tokens exposed in logs, config files, or client-side code
- Arbitrary code execution: Unsanitized inputs leading to command injection
- Data exfiltration: Unauthorized access to sensitive files or databases
- Resource exhaustion: DOS attacks via unlimited tool invocations
- Privilege escalation: Servers accessing resources beyond intended scope
1. Credential Management
Never Hardcode Secrets
Always use environment variables for API keys, tokens, and connection strings.
❌ INSECURE - Don't do this
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "ghp_hardcoded_token_123456"
}
}
}
}✓ SECURE - Use environment variables
# Store in .env file (add to .gitignore)
GITHUB_TOKEN=ghp_your_actual_token_here
# Reference in config
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "${GITHUB_TOKEN}"
}
}
}
}Token Rotation
- Set expiration dates on all API tokens (30-90 days recommended)
- Create calendar reminders for token rotation
- Use services like HashiCorp Vault for automated rotation
- Maintain a token inventory document
Least Privilege Principle
Grant only the minimum permissions required for each integration.
Example: GitHub Token Scopes
repo scope for read-only operationsrepo:status + public_repo for public repo read access2. Input Validation
Sanitize All User Inputs
Never trust data from the AI model. Treat all tool arguments as potentially malicious input.
❌ VULNERABLE - No validation
@server.tool()
async def execute_command(command: str) -> str:
"""Execute a shell command"""
result = subprocess.run(command, shell=True, capture_output=True)
return result.stdout.decode()
✓ SECURE - Validated and sandboxed
import shlex
import subprocess
ALLOWED_COMMANDS = ["ls", "cat", "grep", "find"]
@server.tool()
async def execute_command(command: str) -> str:
"""Execute a whitelisted shell command
Args:
command: Command to execute (must be in whitelist)
"""
# Parse command
parts = shlex.split(command)
if not parts:
raise ValueError("Empty command")
# Whitelist check
if parts[0] not in ALLOWED_COMMANDS:
raise ValueError(f"Command '{parts[0]}' not allowed")
# Path traversal check
for arg in parts[1:]:
if ".." in arg or arg.startswith("/"):
raise ValueError("Path traversal detected")
# Execute with timeout and resource limits
try:
result = subprocess.run(
parts,
shell=False, # Never use shell=True
capture_output=True,
timeout=5,
cwd="/safe/working/directory"
)
return result.stdout.decode()
except subprocess.TimeoutExpired:
raise TimeoutError("Command execution timed out")
Common Injection Attacks
1. Command Injection
Attack vector:
filename = "report.pdf; rm -rf /"Defense: Use shlex.split(), avoid shell=True, whitelist commands
2. SQL Injection
Attack vector:
user_id = "1 OR 1=1; DROP TABLE users;"Defense: Use parameterized queries, ORM frameworks, never string concatenation
✓ SECURE - Parameterized query
@server.tool()
async def get_user(user_id: int) -> str:
"""Get user information by ID"""
# Use parameterized query
cursor.execute(
"SELECT * FROM users WHERE id = ?",
(user_id,) # Parameters passed separately
)
return cursor.fetchone()
3. Path Traversal
Attack vector:
filepath = "../../etc/passwd"Defense: Validate paths, use os.path.abspath(), check against allowed directories
✓ SECURE - Path validation
import os
ALLOWED_DIR = "/safe/documents"
@server.tool()
async def read_file(filepath: str) -> str:
"""Read a file from the documents directory"""
# Resolve to absolute path
abs_path = os.path.abspath(os.path.join(ALLOWED_DIR, filepath))
# Ensure it's within allowed directory
if not abs_path.startswith(ALLOWED_DIR):
raise ValueError("Path traversal detected")
# Additional checks
if not os.path.exists(abs_path):
raise FileNotFoundError(f"File not found: {filepath}")
if not os.path.isfile(abs_path):
raise ValueError("Not a file")
with open(abs_path, 'r') as f:
return f.read()
3. Sandboxing and Isolation
Process Isolation
Run MCP servers as separate processes with limited privileges, not as part of your main application.
Benefits of Process Isolation
- Crashes in servers don't affect main app
- Resource limits can be enforced per server
- Easier to restart/update individual servers
- Security boundaries between servers
Docker Containerization
For production deployments, run MCP servers in Docker containers.
Dockerfile for MCP Server
FROM python:3.11-slim # Create non-root user RUN useradd -m -u 1000 mcpuser # Set working directory WORKDIR /app # Install dependencies COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy server code COPY server.py . # Switch to non-root user USER mcpuser # Resource limits (use docker run flags) # --memory="512m" --cpus="0.5" CMD ["python", "server.py"]
Filesystem Restrictions
- Use read-only mounts where possible
- Limit access to specific directories only
- Never grant access to
/etc,/root, or system directories - Use
chrootor Docker volumes for isolation
4. Rate Limiting
Prevent Resource Exhaustion
Implement rate limiting to prevent abuse or accidental DOS attacks.
Rate Limiting Example
from collections import defaultdict
from datetime import datetime, timedelta
from functools import wraps
# Simple in-memory rate limiter
rate_limits = defaultdict(list)
def rate_limit(max_calls: int, period_seconds: int):
"""Rate limit decorator for MCP tools
Args:
max_calls: Maximum calls allowed in period
period_seconds: Time period in seconds
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
now = datetime.now()
tool_name = func.__name__
# Remove old calls outside the time window
cutoff = now - timedelta(seconds=period_seconds)
rate_limits[tool_name] = [
t for t in rate_limits[tool_name] if t > cutoff
]
# Check if limit exceeded
if len(rate_limits[tool_name]) >= max_calls:
raise Exception(
f"Rate limit exceeded: {max_calls} calls per {period_seconds}s"
)
# Record this call
rate_limits[tool_name].append(now)
return await func(*args, **kwargs)
return wrapper
return decorator
# Usage
@server.tool()
@rate_limit(max_calls=10, period_seconds=60)
async def expensive_api_call(query: str) -> str:
"""Call expensive API (rate limited to 10/min)"""
# Your API logic
pass
Resource Quotas
- CPU limits: Prevent runaway processes (use Docker
--cpus) - Memory limits: Prevent memory exhaustion (use Docker
--memory) - Timeout limits: Kill long-running operations (use
asyncio.timeout()) - Request size limits: Limit input payload sizes
5. Authentication & Authorization
User Context Awareness
In multi-user environments, ensure MCP servers know which user is making requests.
User Context Pattern
from mcp.server import Server
from mcp.types import Tool
server = Server("multi-user-server")
# Store user context per connection
user_contexts = {}
@server.on_connection
async def handle_connection(connection_id: str, auth_token: str):
"""Authenticate user when they connect"""
user = authenticate_token(auth_token)
user_contexts[connection_id] = user
@server.tool()
async def get_user_data(connection_id: str) -> str:
"""Get data for authenticated user only"""
user = user_contexts.get(connection_id)
if not user:
raise PermissionError("Not authenticated")
# Check permissions
if not user.has_permission("read_data"):
raise PermissionError("Insufficient permissions")
# Return user-specific data only
return fetch_data_for_user(user.id)
Scope Restrictions
Implement role-based access control (RBAC) for tool access.
Example: Tool Permissions
search_*, get_* toolscreate_*, update_* toolsdelete_*, admin_* tools6. Logging and Monitoring
Security Event Logging
Log all security-relevant events for audit trails and incident response.
Security Logging Example
import logging
import json
from datetime import datetime
# Configure structured logging
security_logger = logging.getLogger("mcp.security")
security_logger.setLevel(logging.INFO)
def log_security_event(event_type: str, details: dict):
"""Log security events in structured format"""
security_logger.info(json.dumps({
"timestamp": datetime.utcnow().isoformat(),
"event_type": event_type,
"details": details
}))
@server.tool()
async def sensitive_operation(user_id: str, action: str) -> str:
"""Perform sensitive operation with audit logging"""
# Log the attempt
log_security_event("sensitive_operation_attempt", {
"user_id": user_id,
"action": action,
"ip_address": get_client_ip()
})
# Perform operation
try:
result = perform_action(user_id, action)
# Log success
log_security_event("sensitive_operation_success", {
"user_id": user_id,
"action": action
})
return result
except Exception as e:
# Log failure
log_security_event("sensitive_operation_failed", {
"user_id": user_id,
"action": action,
"error": str(e)
})
raise
What to Log
- Authentication attempts (success and failure)
- Permission denied errors
- Rate limit violations
- Input validation failures
- Unusual usage patterns
- Configuration changes
What NOT to Log
Never Log:
- API keys, tokens, passwords
- Full credit card numbers
- Personally identifiable information (PII)
- Session tokens or cookies
7. Network Security
Use HTTPS for Remote Servers
If running MCP servers over the network (not local stdio), always use TLS encryption.
Firewall Rules
- Limit incoming connections to trusted IPs only
- Use VPNs for remote access
- Implement network segmentation (MCP servers in isolated subnet)
Secret Transmission
Never send secrets over unencrypted channels. Use secure secret management services.
8. Dependency Security
Keep Dependencies Updated
Regular Security Updates
# Audit npm packages npm audit # Fix vulnerabilities automatically npm audit fix # Python packages pip-audit # Update all packages npm update pip install --upgrade -r requirements.txt
Pin Dependency Versions
Use exact versions in production to prevent unexpected changes.
package.json
{
"dependencies": {
"@modelcontextprotocol/sdk": "0.5.0", // Exact version
"express": "^4.18.0" // Avoid in production
}
}
Use Dependency Scanning
- Snyk: Automated vulnerability scanning
- Dependabot: Automated PR updates (GitHub)
- npm audit / pip-audit: Built-in security checks
9. Production Checklist
PRE-DEPLOYMENT SECURITY CHECKLIST
10. Common Vulnerabilities
| Vulnerability | Risk | Mitigation |
|---|---|---|
| Hardcoded secrets | Critical | Use env vars, secret managers |
| Command injection | Critical | Validate inputs, avoid shell=True |
| Path traversal | High | Validate paths, restrict directories |
| SQL injection | High | Parameterized queries only |
| Resource exhaustion | Medium | Rate limiting, resource quotas |
| Outdated dependencies | Medium | Regular audits, automated updates |
| Insufficient logging | Low | Structured security logging |
Incident Response
If You Detect a Security Breach:
- Isolate: Immediately stop the affected MCP server
- Rotate: Revoke and regenerate all API keys/tokens
- Investigate: Review logs to understand scope of breach
- Notify: Inform affected users and stakeholders
- Patch: Fix the vulnerability before restarting
- Monitor: Watch for continued unauthorized access attempts