---
title: "MCP: Security"
slug: "mcp-security"
category: "ai-agents"
tags: ["mcp", "ai-agents", "security", "auth", "secrets", "sandboxing"]
status: "stable"
last_updated: 2026-08-14
summary: "Authenticate at the transport, redact secrets from tool outputs, rate-limit per session, and sandbox the filesystem surface to ship MCP servers safely."
related: ["[[ai-agents/mcp-servers]]", "[[ai-agents/mcp-protocol]]", "[[ai-agents/mcp-transports]]", "[[ai-agents/mcp-logging]]", "[[ai-agents/prompt-injection-defense]]", "[[ai-agents/claude-code-mcp]]", "[[ai-agents/claude-code-permissions]]"]
---

## Overview

An MCP server runs with credentials. It has access to APIs, databases, and filesystems that the model would not otherwise reach. Every tool call is an opportunity for a buggy agent loop, a malicious prompt injection, or a misconfigured allowlist to cause real harm. The rules on this page treat the MCP server as an API gateway: authenticate at the boundary, redact what leaves it, rate-limit what enters, and keep the blast radius small.

## Authenticate at the transport, not inside tool logic

Auth belongs at the connection layer, not scattered through individual tool handlers. For [[ai-agents/mcp-transports|stdio transports]], the server process inherits environment variables from the parent. Pass credentials as environment variables resolved by the client at startup. For HTTP transports, require a bearer token or mutual TLS on every request before any method is dispatched.

```json
{
  "mcpServers": {
    "github": {
      "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }
    }
  }
}
```

Centralizing auth means a single misconfiguration is visible in one place. Distributing it through tool handlers means each handler becomes an auth bypass opportunity.

Never embed credentials as literal values in `settings.json` or in tool schema descriptions. Literal values appear in diffs, logs, and PR reviews. See [[ai-agents/claude-code-mcp]] for the env-substitution pattern.

## Redact secrets and PII from tool outputs before they reach the model

Tool outputs flow into the model's context window and potentially into logs. An API response that contains a token, email address, or private key will be quoted verbatim in the conversation unless the server strips it.

Apply a redaction pass to every tool response. Common targets: API keys matching `sk-*` or `gh*`, email addresses, OAuth tokens, private key blocks, internal IPs, and UUIDs that serve as capability tokens.

```python
import re
SECRET_PATTERN = re.compile(r'(sk-[A-Za-z0-9]{20,}|gh[pousr]_[A-Za-z0-9]{36,})')
def redact(text: str) -> str:
    return SECRET_PATTERN.sub('[REDACTED]', text)
```

Redact at the server boundary, not in the client. The server is the only place that has full knowledge of what its responses may contain.

## Apply per-caller rate limits on every tool

A loop in the agent's planning code can call a tool thousands of times in minutes. Without rate limits, this exhausts API quota, incurs cost, and may trigger bans from third-party services.

Set hard rate limits per tool, keyed to the authenticated caller. Useful defaults: 60 calls per minute per tool, 500 calls per hour per tool. On servers still running the pre-2026-07-28 session-scoped transport, key limits on `Mcp-Session-Id`; on the current stateless spec, key them on the request's authenticated identity instead, since there is no connection-scoped session. Expose the limits in the tool description so the model can plan accordingly.

Implement a circuit breaker: after N consecutive errors from a tool, disable it for the session and return a structured error telling the agent to stop retrying. See [[ai-agents/mcp-logging]] for the error logging pattern that feeds the circuit breaker.

## Constrain the filesystem surface to explicit allowlisted paths

A filesystem-capable MCP server that can read or write anywhere on the host is a critical vulnerability. A prompt injection in a document the agent processes could instruct the server to exfiltrate `~/.ssh/id_rsa` or overwrite a system file.

Enforce a path allowlist at the server level:

```python
ALLOWED_ROOTS = [Path("/home/user/projects"), Path("/tmp/agent-workspace")]
def check_path(p: Path) -> None:
    resolved = p.resolve()
    if not any(resolved.is_relative_to(root) for root in ALLOWED_ROOTS):
        raise PermissionError(f"Path outside allowlist: {p}")
```

Resolve symlinks before checking. A symlink inside the allowed root that points outside it is an escape vector.

## Never execute arbitrary strings passed from the model

Tool inputs originate from a language model. That model can be manipulated by [[ai-agents/prompt-injection-defense|prompt injection]] in data it reads. A tool that passes a model-supplied string to `eval()`, `exec()`, `subprocess.run(shell=True)`, or any equivalent is a remote code execution vulnerability.

Design tools to accept structured parameters only. If a tool must run a command, use an allowlist of permitted commands and reject anything outside it. Parameterize the command; never interpolate user-controlled strings into a shell command.

```python
ALLOWED_COMMANDS = {"npm test", "npm run build", "pytest"}
def run_command(cmd: str) -> str:
    if cmd not in ALLOWED_COMMANDS:
        raise ValueError(f"Command not allowed: {cmd}")
    return subprocess.run(cmd.split(), capture_output=True, text=True).stdout
```

## Related

- [[ai-agents/mcp-servers]]
- [[ai-agents/mcp-protocol]]
- [[ai-agents/mcp-transports]]
- [[ai-agents/mcp-logging]]
- [[ai-agents/prompt-injection-defense]]
- [[ai-agents/claude-code-mcp]]
- [[ai-agents/claude-code-permissions]]
