MCP Security Fundamentals
Introduction
MCP servers bridge AI assistants with your infrastructure, databases, and APIs. This privileged position makes security critical. A poorly secured MCP server can expose sensitive data, allow unauthorized operations, or become an attack vector. This tutorial covers the essential security practices every MCP server developer should implement.
For authentication-specific patterns, see our dedicated MCP authentication tutorial. For general MCP concepts, start with What is MCP.
Threat Model for MCP Servers
Attack Surface
MCP servers face threats from multiple directions:
- Malicious prompts: AI clients may send crafted inputs designed to exploit tool implementations
- Prompt injection: Users may trick the AI into calling tools with harmful arguments
- Network attacks: Remote MCP servers are exposed to standard network threats
- Supply chain: Compromised dependencies can affect server security
- Data exfiltration: Tools may inadvertently expose sensitive data
Trust Boundaries
┌──────────────────────────────────────────────┐
│ Untrusted Zone │
│ ┌──────────┐ │
│ │ AI Client│ ── User Prompts ──┐ │
│ └──────────┘ │ │
│ v │
│ ┌──────────────────────────────────────┐ │
│ │ MCP Server │ │
│ │ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │ Input │->│ Tool Execution │ │ │
│ │ │Validation│ │ (Sandboxed) │ │ │
│ │ └──────────┘ └────────┬─────────┘ │ │
│ └─────────────────────────┼────────────┘ │
│ │ │
└────────────────────────────┼─────────────────┘
v
┌──────────────────────────────────────────────┐
│ Trusted Zone │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │ Database │ │ File │ │ External │ │
│ │ │ │ System │ │ APIs │ │
│ └──────────┘ └──────────┘ └───────────┘ │
└──────────────────────────────────────────────┘
Input Validation
Every tool argument must be validated before use. Never trust input from AI clients.
String Validation
from mcp.server.fastmcp import FastMCP
import re
import json
mcp = FastMCP("secure-server")
@mcp.tool()
def search_users(query: str) -> str:
"""Search for users by name.
Args:
query: Search query (letters, numbers, spaces only)
"""
# Validate input format
if not query or len(query) > 100:
return json.dumps({"error": "Query must be 1-100 characters"})
if not re.match(r'^[a-zA-Z0-9\s]+
Path Traversal Prevention
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import path from 'path';
import fs from 'fs/promises';
const ALLOWED_ROOT = '/app/data';
server.tool(
'read-file',
'Read a file from the data directory',
{ filepath: z.string() },
async ({ filepath }) => {
// Resolve the absolute path
const resolvedPath = path.resolve(ALLOWED_ROOT, filepath);
// Verify it is within the allowed directory
if (!resolvedPath.startsWith(ALLOWED_ROOT)) {
return {
content: [{ type: 'text', text: 'Error: Access denied - path traversal detected' }],
isError: true,
};
}
// Check file exists
try {
const content = await fs.readFile(resolvedPath, 'utf-8');
return { content: [{ type: 'text', text: content }] };
} catch {
return {
content: [{ type: 'text', text: 'Error: File not found' }],
isError: true,
};
}
}
);
SQL Injection Prevention
Always use parameterized queries:
# WRONG - vulnerable to SQL injection
@mcp.tool()
def bad_query(table: str, filter: str) -> str:
"""Never do this."""
result = db.execute(f"SELECT * FROM {table} WHERE {filter}")
return str(result)
# CORRECT - parameterized query with validated table name
ALLOWED_TABLES = {"users", "orders", "products"}
@mcp.tool()
def safe_query(table: str, column: str, value: str) -> str:
"""Query a table safely.
Args:
table: Table name (users, orders, or products)
column: Column to filter on
value: Value to match
"""
if table not in ALLOWED_TABLES:
return json.dumps({"error": f"Invalid table. Allowed: {ALLOWED_TABLES}"})
if not re.match(r'^[a-zA-Z_]+
Command Injection Prevention
Never pass untrusted input to shell commands:
import subprocess
# WRONG - command injection risk
@mcp.tool()
def bad_exec(command: str) -> str:
"""Never expose raw command execution."""
return subprocess.check_output(command, shell=True, text=True)
# CORRECT - use allowlists and avoid shell=True
ALLOWED_COMMANDS = {
"git-status": ["git", "status", "--short"],
"git-log": ["git", "log", "--oneline", "-10"],
"disk-usage": ["df", "-h"],
}
@mcp.tool()
def run_command(command_name: str) -> str:
"""Run a pre-defined system command.
Args:
command_name: Command to run (git-status, git-log, disk-usage)
"""
if command_name not in ALLOWED_COMMANDS:
return json.dumps({"error": f"Unknown command: {command_name}"})
result = subprocess.run(
ALLOWED_COMMANDS[command_name],
capture_output=True,
text=True,
timeout=10,
)
return result.stdout
Sandboxing
Process Isolation
Run MCP servers in isolated environments:
import resource
import os
def apply_sandbox():
"""Apply process-level restrictions."""
# Limit memory to 256MB
resource.setrlimit(resource.RLIMIT_AS, (256 * 1024 * 1024, 256 * 1024 * 1024))
# Limit CPU time to 30 seconds
resource.setrlimit(resource.RLIMIT_CPU, (30, 30))
# Limit number of open files
resource.setrlimit(resource.RLIMIT_NOFILE, (100, 100))
# Limit number of child processes
resource.setrlimit(resource.RLIMIT_NPROC, (10, 10))
Container Sandboxing
Use Docker with security constraints for the strongest isolation. See our Docker deployment tutorial for details.
docker run -i --rm \
--read-only \
--tmpfs /tmp:noexec,nosuid,size=64m \
--memory=256m \
--cpus=0.5 \
--network=none \
--security-opt=no-new-privileges \
my-mcp-server:latest
Access Control
Principle of Least Privilege
Only expose the minimum necessary tools and resources:
from mcp.server.fastmcp import FastMCP
import os
mcp = FastMCP("restricted-server")
# Only register tools appropriate for the deployment context
ROLE = os.environ.get("MCP_ROLE", "reader")
@mcp.tool()
def read_data(key: str) -> str:
"""Read data by key.
Args:
key: Data key to look up
"""
return json.dumps({"key": key, "value": data_store.get(key)})
# Only register write tools for write-enabled deployments
if ROLE in ("writer", "admin"):
@mcp.tool()
def write_data(key: str, value: str) -> str:
"""Write data to storage.
Args:
key: Data key
value: Value to store
"""
data_store[key] = value
return json.dumps({"status": "written", "key": key})
if ROLE == "admin":
@mcp.tool()
def delete_data(key: str) -> str:
"""Delete data from storage.
Args:
key: Data key to delete
"""
data_store.pop(key, None)
return json.dumps({"status": "deleted", "key": key})
Rate Limiting
Prevent abuse with request rate limits:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
const requestCounts = new Map<string, { count: number; resetAt: number }>();
function checkRateLimit(toolName: string, maxPerMinute: number = 60): boolean {
const now = Date.now();
const entry = requestCounts.get(toolName);
if (!entry || now > entry.resetAt) {
requestCounts.set(toolName, { count: 1, resetAt: now + 60000 });
return true;
}
if (entry.count >= maxPerMinute) {
return false;
}
entry.count++;
return true;
}
server.tool(
'query-database',
'Execute a database query',
{ sql: z.string() },
async ({ sql }) => {
if (!checkRateLimit('query-database', 30)) {
return {
content: [{ type: 'text', text: 'Rate limit exceeded. Try again later.' }],
isError: true,
};
}
// Execute query...
return { content: [{ type: 'text', text: 'Query results...' }] };
}
);
Audit Logging
Log all tool invocations for security monitoring:
import logging
import json
from datetime import datetime
from functools import wraps
logging.basicConfig(
filename='mcp_audit.log',
format='%(message)s',
level=logging.INFO,
)
def audit_tool(func):
@wraps(func)
def wrapper(*args, **kwargs):
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"tool": func.__name__,
"arguments": kwargs,
}
try:
result = func(*args, **kwargs)
log_entry["status"] = "success"
log_entry["result_size"] = len(str(result))
return result
except Exception as e:
log_entry["status"] = "error"
log_entry["error"] = str(e)
raise
finally:
logging.info(json.dumps(log_entry))
return wrapper
@mcp.tool()
@audit_tool
def sensitive_operation(data: str) -> str:
"""Perform a sensitive operation.
Args:
data: Data to process
"""
return process(data)
Data Protection
Sensitive Data Filtering
Prevent accidental exposure of sensitive data:
import re
SENSITIVE_PATTERNS = [
(re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'), '[EMAIL]'),
(re.compile(r'\b\d{3}-\d{2}-\d{4}\b'), '[SSN]'),
(re.compile(r'\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})\b'), '[CARD]'),
(re.compile(r'(?:password|secret|token|api_key)\s*[=:]\s*\S+', re.I), '[REDACTED]'),
]
def sanitize_output(text: str) -> str:
"""Remove sensitive data from tool output."""
for pattern, replacement in SENSITIVE_PATTERNS:
text = pattern.sub(replacement, text)
return text
@mcp.tool()
def read_log(logfile: str) -> str:
"""Read a log file with sensitive data redacted.
Args:
logfile: Log file name
"""
content = Path(f"/app/logs/{logfile}").read_text()
return sanitize_output(content)
Encryption at Rest and in Transit
- Transport: Always use TLS for network MCP servers (see advanced MCP tutorial)
- Storage: Encrypt sensitive data before storing
- Secrets: Never log or return secrets in tool outputs
Dependency Security
Keep Dependencies Updated
# TypeScript
npm audit
npm update
# Python
pip install --upgrade mcp
pip-audit
Minimal Dependencies
Only install what you need. Each dependency is a potential attack vector.
Lock Files
Always commit lock files (package-lock.json, uv.lock) to ensure reproducible builds.
Security Checklist
Use this checklist for every MCP server deployment:
- [ ] All tool inputs are validated (type, length, format)
- [ ] Path traversal prevention is implemented for file operations
- [ ] SQL queries use parameterized statements
- [ ] No shell command injection vectors
- [ ] Rate limiting is in place
- [ ] Audit logging captures all tool invocations
- [ ] Sensitive data is filtered from outputs
- [ ] Server runs as non-root user
- [ ] TLS is configured for network transports
- [ ] Dependencies are up to date and audited
- [ ] Error messages do not leak internal details
- [ ] Resource limits (memory, CPU, file descriptors) are set
Conclusion
Security is not optional for MCP servers. Since they bridge AI with your infrastructure, every tool is a potential attack surface. By implementing input validation, sandboxing, access control, and audit logging, you protect both your systems and your users.
For authentication-specific patterns including OAuth 2.0 and JWT, continue with our MCP authentication tutorial. For a deeper dive into security topics, read our MCP server security guide.
, query):
return json.dumps({"error": "Query contains invalid characters"})
# Safe to use in database query with parameterized queries
results = db.execute("SELECT * FROM users WHERE name LIKE ?", [f"%{query}%"])
return json.dumps({"results": results})
Path Traversal Prevention
SQL Injection Prevention
Always use parameterized queries:
Command Injection Prevention
Never pass untrusted input to shell commands:
Sandboxing
Process Isolation
Run MCP servers in isolated environments:
Container Sandboxing
Use Docker with security constraints for the strongest isolation. See our Docker deployment tutorial for details.
Access Control
Principle of Least Privilege
Only expose the minimum necessary tools and resources:
Rate Limiting
Prevent abuse with request rate limits:
Audit Logging
Log all tool invocations for security monitoring:
Data Protection
Sensitive Data Filtering
Prevent accidental exposure of sensitive data:
Encryption at Rest and in Transit
- Transport: Always use TLS for network MCP servers (see advanced MCP tutorial)
- Storage: Encrypt sensitive data before storing
- Secrets: Never log or return secrets in tool outputs
Dependency Security
Keep Dependencies Updated
Minimal Dependencies
Only install what you need. Each dependency is a potential attack vector.
Lock Files
Always commit lock files (package-lock.json, uv.lock) to ensure reproducible builds.
Security Checklist
Use this checklist for every MCP server deployment:
- [ ] All tool inputs are validated (type, length, format)
- [ ] Path traversal prevention is implemented for file operations
- [ ] SQL queries use parameterized statements
- [ ] No shell command injection vectors
- [ ] Rate limiting is in place
- [ ] Audit logging captures all tool invocations
- [ ] Sensitive data is filtered from outputs
- [ ] Server runs as non-root user
- [ ] TLS is configured for network transports
- [ ] Dependencies are up to date and audited
- [ ] Error messages do not leak internal details
- [ ] Resource limits (memory, CPU, file descriptors) are set
Conclusion
Security is not optional for MCP servers. Since they bridge AI with your infrastructure, every tool is a potential attack surface. By implementing input validation, sandboxing, access control, and audit logging, you protect both your systems and your users.
For authentication-specific patterns including OAuth 2.0 and JWT, continue with our MCP authentication tutorial. For a deeper dive into security topics, read our MCP server security guide.
, column):
return json.dumps({"error": "Invalid column name"})
result = db.execute(
f"SELECT * FROM {table} WHERE {column} = ?",
[value]
)
return json.dumps({"results": result})
Command Injection Prevention
Never pass untrusted input to shell commands:
Sandboxing
Process Isolation
Run MCP servers in isolated environments:
Container Sandboxing
Use Docker with security constraints for the strongest isolation. See our Docker deployment tutorial for details.
Access Control
Principle of Least Privilege
Only expose the minimum necessary tools and resources:
Rate Limiting
Prevent abuse with request rate limits:
Audit Logging
Log all tool invocations for security monitoring:
Data Protection
Sensitive Data Filtering
Prevent accidental exposure of sensitive data:
Encryption at Rest and in Transit
- Transport: Always use TLS for network MCP servers (see advanced MCP tutorial)
- Storage: Encrypt sensitive data before storing
- Secrets: Never log or return secrets in tool outputs
Dependency Security
Keep Dependencies Updated
Minimal Dependencies
Only install what you need. Each dependency is a potential attack vector.
Lock Files
Always commit lock files (package-lock.json, uv.lock) to ensure reproducible builds.
Security Checklist
Use this checklist for every MCP server deployment:
- [ ] All tool inputs are validated (type, length, format)
- [ ] Path traversal prevention is implemented for file operations
- [ ] SQL queries use parameterized statements
- [ ] No shell command injection vectors
- [ ] Rate limiting is in place
- [ ] Audit logging captures all tool invocations
- [ ] Sensitive data is filtered from outputs
- [ ] Server runs as non-root user
- [ ] TLS is configured for network transports
- [ ] Dependencies are up to date and audited
- [ ] Error messages do not leak internal details
- [ ] Resource limits (memory, CPU, file descriptors) are set
Conclusion
Security is not optional for MCP servers. Since they bridge AI with your infrastructure, every tool is a potential attack surface. By implementing input validation, sandboxing, access control, and audit logging, you protect both your systems and your users.
For authentication-specific patterns including OAuth 2.0 and JWT, continue with our MCP authentication tutorial. For a deeper dive into security topics, read our MCP server security guide.
, query):
return json.dumps({"error": "Query contains invalid characters"})
# Safe to use in database query with parameterized queries
results = db.execute("SELECT * FROM users WHERE name LIKE ?", [f"%{query}%"])
return json.dumps({"results": results})
Only install what you need. Each dependency is a potential attack vector.
Always commit lock files (package-lock.json, uv.lock) to ensure reproducible builds.
Security is not optional for MCP servers. Since they bridge AI with your infrastructure, every tool is a potential attack surface. By implementing input validation, sandboxing, access control, and audit logging, you protect both your systems and your users.