Overview

An MCP server holds credentials and reaches APIs, databases, and filesystems the model could not otherwise touch, and its tool inputs come from a model that prompt injection can steer. Treat the server as an API gateway: authenticate at the boundary, validate every input, redact every output, and keep the blast radius small. The rules below follow the 2026-07-28 spec and its security best-practices guide.

Authenticate HTTP servers as OAuth 2.1 resource servers

Authorization is optional in the spec, but an HTTP server that does it follows these rules. The full client-and-server flow (discovery, registration, step-up) is in mcp-authorization.

  • Implement Protected Resource Metadata (RFC 9728). An unauthenticated request gets 401 with WWW-Authenticate: Bearer resource_metadata="..." and, ideally, a scope parameter.
  • Clients send resource (RFC 8707) in authorization and token requests and PKCE on the code flow. The server must validate that each token was issued for it as the audience.
  • Clients send Authorization: Bearer <token> on every request and never put a token in the query string. Invalid or expired tokens get 401; insufficient scope gets 403 with error="insufficient_scope" and all required scopes in one challenge.
  • Clients should register with Client ID Metadata Documents; Dynamic Client Registration is deprecated. Clients must validate iss in authorization responses against the recorded issuer.
  • stdio servers skip this flow and read credentials from the environment. Never write a literal secret into .mcp.json, settings.json, or a tool description.

Never pass tokens through

A server must not accept a token that was not issued to it, and must not forward the client’s token to a downstream API. Obtain separate downstream credentials, for example through URL-mode elicitation for third-party OAuth (mcp-elicitation). A proxy that fronts a third-party OAuth server with one static client ID must run its own per-client consent page before redirecting, or an attacker can reuse the consent cookie to steal codes (confused deputy).

Bind handles and scopes to the caller

Since the protocol has no sessions, servers that mint handles (a cart ID, a workflow ID) must not treat possession of a handle as authentication. Generate handles with a secure random source, store state under <user_id>:<handle> where the user ID comes from the verified token, and reject a handle presented by anyone else. Start with minimal scopes and raise them through targeted WWW-Authenticate scope challenges; do not publish every scope in scopes_supported.

Redact secrets before output reaches the model

Tool results enter the context window and logs, so strip tokens, keys, emails, private-key blocks, and internal addresses at the server, the only place that knows what a response can contain.

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

Log messages must also exclude credentials and personal data. See mcp-logging.

Rate limit per authenticated caller

The spec requires servers to rate limit tool invocations. Key the limit on the authenticated identity, not a connection or process, because requests can land on any instance. Add a circuit breaker that disables a tool after repeated errors and returns a structured error telling the agent to stop retrying; a looping agent will otherwise exhaust API quota in minutes.

Constrain files and commands to allowlists

Validate all tool inputs and resource URIs, and sanitize paths served under file://. Resolve symlinks before checking, because a link inside an allowed root can point outside it.

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

Never pass a model-supplied string to eval, exec, or subprocess.run(shell=True). If a tool must run commands, accept an enum or an allowlisted command and parameterize it.

For local servers prefer stdio, bind HTTP to 127.0.0.1, and validate Origin. Clients should treat tool annotations as untrusted unless the server is trusted, show tool inputs before calls, and, for one-click installs, show the exact command before running it. For the wider threat model of tool-using agents, see prompt-injection-defense.