Skip to content

MCP Security

This page covers the authorization model, how upstream credentials are sealed and never pass through, request guardrails, rate limits, and what the audit record contains. The guiding rule is fail-closed: anything ambiguous denies rather than opens.

View as Markdown Open the plain-text version of this page.

Principal authorization#

Authorization is independent of tool publish state. Two principal kinds may be granted allow or deny policies:

Principal kind principalRef
user An individual user id
api_key A project-scoped API key id

Actions are read (discover) and invoke (call a tool). There is deliberately no manage tool action: mutating the catalog and policies is a management-surface capability (mcp:manage), not a principal-grantable action, so it never appears in the access-rule editor.

Adjudication rules#

  1. Collect matches. Gather policies matching this principal kind and reference, the requested action, and overlapping resources, ordered by priority (highest first).
  2. Deny overrides. If any matched policy denies, deny immediately regardless of priority.
  3. Allow on match. Otherwise, if at least one allow matches, allow, narrowed to the requested resources.
  4. Default deny. Anything else denies.

Narrowing only#

A request may never widen what an allow grants. If the caller names a server or tool that no allow policy covers, the request is rejected even though some other resource was allowed. A policy with serverIds: null or toolNames: null means "all" within scope; an explicit array narrows it.

Deny reasons surface as no_matching_policy (default deny), explicit_deny (a deny matched), or request_outside_allow (the request tried to widen).

Hosted tools are pre-approved#

The five shipped hosted tools (web_search and the trace and Finding readers) carry a default-allow flag. When adjudication reaches default-deny, those tools are still listed and invocable by an mcp:invoke caller; they need no policy of their own. A matched explicit deny still blocks them, and federation tools stay fully default-deny until an allow policy admits them.

Hosted tools act as the project identity: there is no upstream, no sealed secret, and no credential entry. The only key a caller supplies is the project API key used when connecting an agent; it authenticates ingress and is never an upstream credential.

Upstream authentication posture#

Each federation server carries a persisted authentication posture written by the connection probe and credential lifecycle. Reachability, per-credential enabled state, and this posture are orthogonal facts the call path combines; posture is not recomputed from the credential list at read time.

Posture Meaning Callable?
not_required An anonymous probe reached the upstream and it accepts the call; no credential configured Yes, if reachable
missing The upstream answers 401; authentication is required but no credential is configured No; the catalog remains manageable
active The primary credential verified successfully, or an authenticated call succeeded Yes
invalid A configured credential was rejected by the upstream (401) No; the catalog remains manageable
revoked The last primary credential was revoked, leaving no identity to attach No; the catalog remains manageable

A connection that is reachable but not callable (missing, invalid, or revoked) still renders and remains fully manageable: edit, discover, sync, publish, and add, test, pause, resume, or promote credentials. Its tools simply cannot be invoked until an active primary exists.

Two upstream failures stay distinct: a 401 means the credential itself is bad (test and promote a fresh secret); a 403 means an upstream authorization decision, treated as a policy or capability problem rather than a bad credential. A 403, network error, or unsupported probe classification is diagnostics-only and does not overwrite the persisted posture; only a successful probe or a 401 probe does.

Upstream error codes on tool calls#

Upstream HTTP status maps to a JSON-RPC code and differs slightly by transport:

Upstream condition Native MCP, both runtimes OpenAPI on self-hosted Node OpenAPI on hosted edge
401 / 403 -32001 permission denied -32001 permission denied -32003 upstream failure
404 -32601 method not found -32001 permission denied -32003 upstream failure
429 -32002 rate limited -32001 permission denied -32003 upstream failure
Other non-2xx -32003 upstream failure -32001 permission denied -32003 upstream failure
Transport error, timeout, blocked host, or oversize body -32003 upstream failure -32000 session expired / re-auth -32003 upstream failure

The full reserved range is -32000 session expired, -32001 permission denied, -32002 rate limited, -32003 upstream failure, -32004 gateway paused, -32601 method not found, and -32603 internal error. On the HTTP ingress, a paused project gateway returns 503 MCP_PAUSED before the JSON-RPC body; -32004 appears on bypass paths such as the Workbench playground.

Route-level gating#

Caller Management read Management write Tool gateway invoke
Project API key Capability mcp:manage Capability mcp:manage Capability mcp:invoke
Signed-in session Project read permission project:mcp:manage permission Not accepted; ingress is API-key only

An API key needs mcp:manage for both management reads and writes; there is no separate read capability. Both mcp:manage and mcp:invoke are absent from the default key capability set, so they are default-deny until granted explicitly. The streamable-HTTP tool gateway accepts only a project API key; a session or any other actor receives 401 UNAUTHORIZED, and a principal the project cannot place receives 403 FORBIDDEN.

Policy and catalog revisions#

At initialize, the allowed set is resolved together with the current policy and catalog revisions. Any change to access policies advances the policy revision; any publish or sync advances the catalog revision. Authorization is re-adjudicated on every exchange, so a policy or catalog change takes effect immediately rather than waiting for a long-lived session to expire. A cached session snapshot is invalidated when its revision no longer matches.

Sealed credentials, not pass-through#

  • Upstream secrets are sealed at rest and unsealed only on the outbound call path.
  • Listing credentials returns only the id, server id, label, kind, outbound auth method, role (primary or backup), enabled state, last-verified time, and timestamps — never the secret. Exactly one credential is the active primary; every other credential is a backup held for cutover. There is no numeric selection priority.
  • The inbound Bearer token presented by the caller is never forwarded upstream. The only outbound auth channel is the sealed upstream secret, injected under the whitelisted outbound auth method: Authorization: Bearer <secret> or X-Api-Key: <secret>. Caller-supplied header names are not accepted.
  • The secret never appears in spans, logs, error messages, or returned bodies.

Guardrail scans on every call#

A deterministic, transport-neutral guardrail runs before and after each federated call and never mutates the payload:

  • Input: arguments must be JSON-serializable and within 64 KiB.
  • No smuggled credentials: arguments cannot contain credential-shaped keys such as authorization, proxy-authorization, cookie, set-cookie, password, secret, token, api-key, or private-key, matched case-insensitively. A hit fails closed; the sealed outbound credential remains the only auth channel.
  • Output: tool output must be JSON-serializable and within 256 KiB. Over budget, the output is withheld and replaced with an error.

Fail-closed on purpose: a false positive that denies a benign call is recoverable by the caller; silently forwarding a leaked secret to an upstream is not.

Rate limiting and concurrency#

The bucket dimension differs by runtime, and both runtimes return JSON-RPC -32002 when exceeded:

  • Self-hosted Node federation: a fixed-window limiter per (server, tool) at 120 calls per minute, plus a concurrency limit of four in-flight calls per server. These gates live in one process and are not distributed across instances.
  • Self-hosted Node hosted tools: a separate per-process limiter per (project, principal, tool) at 60 calls per minute.
  • Hosted edge: one fixed-window bucket per (project, principal, tool) at 120 calls per minute, shared across the project's requests; hosted and federation calls share the same bucket.

Upstream 429 responses are surfaced independently of these local limiters.

Transport hardening#

Every outbound target — a native MCP HTTP server and an OpenAPI-federated REST call — must point at a permitted public host:

  • The upstream URL must use http or https and carry no embedded credentials.
  • Blocked hostnames include localhost, *.localhost, internal, *.internal, local, *.local, metadata, *.metadata, and metadata.google.internal.
  • Blocked address ranges include loopback and link-local, RFC 1918 private ranges, CGNAT, documentation and benchmark blocks, and multicast or reserved ranges (IPv4 and the IPv6 ULA, link-local, multicast, and documentation prefixes).
  • DNS resolution is re-checked: every resolved address must be public, which blocks DNS rebinding to an internal address.
  • Redirects are not followed; the response body is bounded to 1 MB and the call to 30 seconds.

Audit telemetry#

Each outbound federated call produces a bounded client span named MCP <method>. It carries only non-sensitive attributes:

  • mcp.method, mcp.tool.name, mcp.server.id, mcp.server.sourceKind
  • mcp.principal.kind, mcp.principal.ref, mcp.project.id
  • mcp.policy.version, mcp.catalog.version
  • mcp.outcome, mcp.rpcResponseStatusCode

Tool arguments, content bodies, upstream URLs, and secrets are never attached. Outcome categories are ok, capability_unavailable, permission_denied, session_expired, rate_limited, upstream_error, and internal.