MCP Security Best Practices

MCP servers often have direct access to databases, file systems, internal APIs, and sensitive user data. A poorly secured MCP server is not just a vulnerability — it's an open door for automated exploitation by anyone who can reach the endpoint. These 10 practices cover the essential baseline every MCP server should meet.


Section 1: Why MCP Security Matters

Traditional web APIs are consumed by human developers who read documentation, handle errors, and can recognise anomalies. MCP servers are consumed by AI agents that call tools automatically, at scale, often with elevated permissions.

This creates a different risk profile:


Section 2: The 10 Essential Practices

1. Always Validate Tool Inputs

What: Reject tool calls that contain unexpected parameter types, values outside expected ranges, or patterns matching injection payloads.

Why: AI models generate tool arguments from natural language. Malicious content in user input can produce tool arguments containing SQL injection, path traversal, or shell metacharacters. Validating at the MCP layer protects all downstream systems.

# Validate before passing to any downstream system
def search_records(query: str, limit: int) -> list:
    if not isinstance(query, str) or len(query) > 200:
        raise ValueError("Invalid query")
    if not isinstance(limit, int) or not (1 <= limit <= 100):
        raise ValueError("limit must be 1–100")
    # Safe to pass to database now
    return db.search(query=query, limit=limit)

2. Use HTTPS for All MCP Endpoints

What: Serve your MCP server exclusively over HTTPS. Redirect or reject plain HTTP connections.

Why: MCP traffic contains tool arguments, responses, and potentially credentials. Unencrypted HTTP exposes all of this to network interception. This is the highest-weighted check in AgentGrade's scoring (20 points) because it is a prerequisite for all other security measures.

# nginx: redirect HTTP to HTTPS
server {
    listen 80;
    return 301 https://$host$request_uri;
}

3. Implement Authentication on Your MCP Server

What: Require valid credentials before serving any tool list or tool call. Return HTTP 401 or 403 for unauthenticated requests.

Why: An unauthenticated MCP server allows any client on the internet to invoke your tools. This is the second-highest-weighted check (15 points) and one of the most commonly failed. Bearer tokens, API keys, and OAuth are all viable approaches depending on your use case.

from fastapi import Header, HTTPException

async def verify_api_key(x_api_key: str = Header(...)):
    if not secrets.compare_digest(x_api_key, EXPECTED_API_KEY):
        raise HTTPException(status_code=401, detail="Invalid API key")

4. Rate Limit Tool Invocations

What: Enforce per-client rate limits on tool calls. Return HTTP 429 with a Retry-After header when limits are exceeded.

Why: AI agents can generate tool calls in tight loops. Without rate limiting, a single misconfigured agent or attacker can exhaust your server resources, trigger excessive third-party API charges, or scrape all your data in a single session.

from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@app.post("/tools/call")
@limiter.limit("60/minute")
async def call_tool(request: Request, body: ToolCallRequest):
    ...

5. Sanitize Tool Outputs Before Returning to the LLM

What: Filter or escape content returned by tools before passing it back to the AI model, especially content from untrusted external sources.

Why: Prompt injection attacks embed instructions in data that gets included in the AI's context. If your fetch_url or search_web tool returns a page containing "Ignore all previous instructions and...", the AI model may follow those instructions. Sanitize or flag untrusted content before it enters the model's context.

INJECTION_PATTERNS = [
    r"ignore (all |previous )?instructions",
    r"system prompt",
    r"you are now",
]

def sanitize_tool_output(content: str) -> str:
    for pattern in INJECTION_PATTERNS:
        if re.search(pattern, content, re.IGNORECASE):
            return "[Content filtered: potential prompt injection detected]"
    return content

6. Pin Dependency Versions

What: Lock all dependencies to specific versions in your package manager. Audit them regularly for known CVEs.

Why: MCP servers are long-running services. An unpinned dependency can silently introduce a vulnerable version during the next deployment. Use pip-audit, npm audit, or cargo audit as part of your CI pipeline.

# Python: generate a locked requirements file
uv pip compile pyproject.toml -o requirements.lock

# Audit for known CVEs
uv run pip-audit -r requirements.lock

7. Avoid Exposing Admin Interfaces

What: Do not serve admin panels, management UIs, or internal dashboards on the same port or domain as your public MCP endpoint.

Why: Admin interfaces at predictable paths (/admin, /dashboard, /_internal) are scanned constantly by automated bots. If your MCP server and admin panel share a host, a vulnerability in either affects both. Serve admin interfaces on a separate internal-only port or subdomain, protected by network-level access controls.

8. Handle Errors Without Leaking Stack Traces

What: Return generic error messages to clients. Log full stack traces internally. Never include file paths, framework names, or SQL errors in API responses.

Why: Verbose error messages help developers but also help attackers. Framework names, file paths, and SQL syntax reveal your internal architecture and help an attacker select targeted exploits.

@app.exception_handler(Exception)
async def generic_error_handler(request: Request, exc: Exception):
    # Log internally with full detail
    logger.exception("Unhandled error: %s", exc)
    # Return nothing useful to the client
    return JSONResponse(
        status_code=500,
        content={"error": "Internal server error"}
    )

9. Log All Tool Invocations for Audit

What: Record every tool call with the client identifier, tool name, timestamp, and (sanitized) arguments. Retain logs for at least 90 days.

Why: When something goes wrong — a data leak, unexpected tool calls, a compromised client — logs are the only way to reconstruct what happened. Without audit logs, you cannot detect abuse, investigate incidents, or demonstrate compliance.

def log_tool_call(client_id: str, tool: str, args: dict) -> None:
    logger.info(
        "tool_call",
        extra={
            "client_id": client_id,
            "tool": tool,
            "args_keys": list(args.keys()),  # log keys, not values
            "timestamp": datetime.utcnow().isoformat(),
        }
    )

10. Run Regular Security Scans

What: Continuously monitor your MCP server's observable security posture. Check that HTTPS is enforced, auth is required, headers are set, and no sensitive files are exposed.

Why: Security posture drifts. A deployment change can accidentally remove authentication. A new route can expose a sensitive path. Certificate renewals can fail. Regular automated scanning catches regressions before attackers do.

AgentGrade scans publicly-accessible MCP servers on a regular schedule and grades them A–F on 14 observable security checks. Check your server's grade →


Section 3: Common Vulnerabilities We Find

AgentGrade's scanner runs 14 passive security checks against every MCP server it can reach. Based on our scanning data, the most commonly failed checks are:

Check Weight Failure Rate
Authentication Required 15 pts (Critical) High — most remote MCP servers have no auth
CORS Wildcard Policy 8 pts (High) High — wildcard CORS is common in development configs left in production
Content-Security-Policy Header 3 pts (Low) Very high — CSP is rarely set on API-only servers
HTTP Strict Transport Security 3 pts (Low) Very high — HSTS requires deliberate configuration
Server Version Disclosure 5 pts (Medium) Moderate — default server configs expose version strings

The good news: most of these are configuration changes, not code rewrites. An F-graded server can reach a B by adding authentication, fixing CORS, and setting three security headers — work that can often be completed in an afternoon.

Read the full methodology →