BEST PRACTICES • 14 MIN READ

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.

Updated recently

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

❌ Too permissive: Full repo scope for read-only operations
✓ Minimal: repo:status + public_repo for public repo read access

2. 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 chroot or 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

Read-only user: Can call search_*, get_* tools
Editor: Can call create_*, update_* tools
Admin: Can call delete_*, admin_* tools

6. 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

VulnerabilityRiskMitigation
Hardcoded secretsCriticalUse env vars, secret managers
Command injectionCriticalValidate inputs, avoid shell=True
Path traversalHighValidate paths, restrict directories
SQL injectionHighParameterized queries only
Resource exhaustionMediumRate limiting, resource quotas
Outdated dependenciesMediumRegular audits, automated updates
Insufficient loggingLowStructured security logging

Incident Response

If You Detect a Security Breach:

  1. Isolate: Immediately stop the affected MCP server
  2. Rotate: Revoke and regenerate all API keys/tokens
  3. Investigate: Review logs to understand scope of breach
  4. Notify: Inform affected users and stakeholders
  5. Patch: Fix the vulnerability before restarting
  6. Monitor: Watch for continued unauthorized access attempts

Further Reading

Have Questions?

Join the MCP community on GitHub or Discord for help and discussion.