---
title: "MCP Support"
type: concept
tags: [mcp, gatekeepers, developer, foundational, well-established]
created: 2026-08-05
updated: 2026-08-05
sources: ["raw/github_doc-packages-gatekeeper-mcp-readme-md.md", "raw/github_doc-packages-gatekeeper-mcp-portal-readme-md.md", "raw/github_doc-packages-mcp-shared-readme-md.md"]
confidence: high
cfos_snapshot: "2026-08-05"
---

# MCP Support

## Definition

Cloudflare OS supports the [Model Context Protocol](https://modelcontextprotocol.io) (MCP) via two [[concepts/gatekeepers|Gatekeeper]] connectors that share a common library, `@gadgets/mcp-shared`:

- [[entities/gatekeeper-mcp]] — a user pastes an arbitrary MCP server's endpoint URL; one Worker covers every possible MCP server, so a server needs no Gadgets-specific integration work to become usable.
- [[entities/gatekeeper-mcp-portal]] — an administrator configures a single organizational MCP portal URL (e.g. a [Cloudflare MCP server portal](https://developers.cloudflare.com/cloudflare-one/access-controls/ai-controls/mcp-portals/)); everyone in the org then reaches every approved upstream server through it without ever typing an endpoint.

`mcp-shared` is not itself a Worker — it's a library both connectors import, holding "code that would eventually disagree in a way that would be a security bug": tool classification, the scope grammar, the OAuth lifecycle, and approval-queue wiring.

## How It Works

**Connecting.** For `gatekeeper-mcp`, the user supplies an endpoint URL (validated against a host blocklist; HTTPS required unless `MCP_ALLOW_INSECURE`), the gatekeeper opens a Streamable HTTP session and calls `initialize`; a server that completes the handshake unauthenticated is recorded as public. A `401` triggers the official MCP client's OAuth discovery chain:

```
401 + WWW-Authenticate  ->  protected resource metadata   (RFC 9728)
                        ->  authorization server metadata (RFC 8414)
                        ->  dynamic client registration   (RFC 7591)
                        ->  authorization code + PKCE     (RFC 7636)
                            + resource indicator          (RFC 8707)
```

with a fallback to conventional `/authorize`, `/token`, `/register` paths. Tokens are stored in an `McpAccount` Durable Object and refreshed proactively before expiry; a `401` mid-session does *not* trigger a refresh (it means the server rejected a token this Worker believed valid), so the account is instead marked as needing reconnection. For `gatekeeper-mcp-portal` there is no connect form — pressing "connect" goes straight to the portal's sign-in, using `MCP_PORTAL_AUTH`: `oauth` (same discovery chain, against the portal), `token` (presents `MCP_PORTAL_TOKEN`), or `none`.

**Tools become typed methods.** Each server's own `inputSchema` generates a named method on the Gadget-facing session (e.g. `env.MCP_LINEAR.searchIssues({...})`), plus a `callTool(name, args)` fallback for tool names the RPC layer can't deliver as identifiers (`then`, `map`, `2fa`, case collisions like `list_issues`/`listIssues`). The agent discovers the API statically via `describeGatekeeper()`, which sends the full generated `.d.ts` with each method's tool description as JSDoc and whether it needs approval.

**Approvals.** A tool the server annotates `readOnlyHint: true` is treated as an observation and returns immediately; every other call is queued via `submitAction()` and only reaches the server when the Overseer calls `applyAction()`. Because MCP annotations are optional and self-declared, every hint is compared with strict `=== true`/`=== false`, so an *unannotated* tool is always treated as an action requiring approval on either trust tier.

**Trust tiers** govern how far a server's own annotation claims are believed:
- **`byo`** ("bring your own") — a user typed the URL in. `readOnlyHint` classifies reads; nothing the server claims can auto-apply a *write*. Both `gatekeeper-mcp` (always) and `gatekeeper-mcp-portal` (by default) use this tier.
- **`vetted`** — a deployment has asserted an endpoint's annotations can be relied on, so `destructiveHint: false` plus `idempotentHint: true` may drive auto-approval. Only `gatekeeper-mcp-portal` can reach this tier, and only with `MCP_PORTAL_TRUST_ANNOTATIONS=true` set, because a portal aggregates upstream servers whose annotations the administrator never individually reviewed.

**Scoping.** Both connectors let a grant be scoped to "all tools" (including ones added later) or "named tools only" (anything else refused, including future additions). The portal additionally scopes every grant to exactly one upstream server behind it — recovered from the fact that the portal flattens tool names as `{server_id}_{original_name}` and exposes a `portal_list_servers` tool; `portal_*` tools themselves are never grantable, since they could let a Gadget widen which upstream servers its own session can reach.

## Key Parameters

- `gatekeeper-mcp` config: `BASE_URL`, `MCP_CLIENT_NAME`, `MCP_ALLOW_INSECURE` (local dev only — disables the host blocklist and HTTPS requirement).
- `gatekeeper-mcp-portal` config: `MCP_PORTAL_URL` (unset hides the connector entirely), `MCP_PORTAL_NAME`, `MCP_PORTAL_AUTH` (`oauth`/`none`/`token`), `MCP_PORTAL_TOKEN`, `MCP_PORTAL_TRUST_ANNOTATIONS`, `MCP_ALLOW_INSECURE`.
- Fixed limits from `mcp-shared` (not configurable): 200 tools/server, 96 KiB UTF-8 catalog size, 4 KB tool description, 20 KB tool input schema, 50 `tools/list` pages, 1 MiB response body, 128 KB retained action result, 100 retained actions, 50 actions awaiting decision, 3 redirect hops, 10-minute connect link, 1-hour unfinished-connect expiry.
- SSRF boundary: enforced by the `global_fetch_strictly_public` compatibility flag (rejects reserved IP ranges post-DNS-resolution, on every request and redirect hop), not by the endpoint hostname blocklist, which is only a legible connect-time refusal.

## When To Use

Use `gatekeeper-mcp` to let end users connect any MCP server they know about (self-hosted, third-party, or ad hoc) without requiring per-server integration work. Use `gatekeeper-mcp-portal` when an organization already runs (or wants to run) a centralized MCP portal so Access can gate who connects and Cloudflare Gateway can log/inspect traffic, and so end users never need to know or type individual server endpoints.

## Risks & Pitfalls

- **No simulation** — MCP has no way to predict a tool's effect, so a queued call is not reflected in later reads; the agent's turn suspends until the human decides.
- **No revert** and **no hooks** — MCP describes no inverse operation, and `notifications/tools/list_changed` is session-scoped, not durable.
- **No scoping below tool names** — MCP tools take arguments, not capabilities, so "this repo only" cannot be expressed; a named-tool list is the narrowest available grant.
- **Only `tools/*`** is implemented — prompts, resources, sampling, and elicitation are out of scope (sampling/elicitation would let a server drive the agent).
- **Tool-list changes are adopted, not pinned** — a changed catalog is logged and taken, since refusing new tools would break working Gadgets; a tool-scoped binding cannot widen this way, which is why "Choose tools" is preferred for anything that writes.
- **Owner-only sharing** — `addObserver` refuses unconditionally on both connectors; being able to authenticate to a server is not evidence of being allowed to see what the *owner* read from it, and the Gadget runs on the owner's credentials throughout. To share the work, publish the Gadget as a Blueprint and let each collaborator connect their own server. See [[concepts/observers]] and [[concepts/sharing]].
- **Sharing UI reports late** — `GadgetMetadata.sharingProhibited` derives only from `prohibitAllSharing`, so creating a share key can appear to succeed and only fail when the recipient opens it; fixing this needs a kernel change.
- **Honoring `readOnlyHint` on `byo` is a knowing departure** from MCP's own guidance (treat annotations as untrusted unless from a trusted server) — accepted because prompting on every read/list call would make the connector unusable, but it means a dishonest `byo` server *can* act on any call it's granted, including approved writes.
- Applying an approved MCP call only guarantees *at most once*, not exactly once — MCP has no idempotency key and no inverse, so ambiguous failures (dropped connections, malformed replies, oversized bodies) are closed as failed-and-not-retryable rather than risking a double write.

## Related Concepts

- [[concepts/gatekeepers]] — MCP support is implemented as two ordinary Gatekeepers.
- [[concepts/writing-a-gatekeeper]] — the general connector-authoring workflow these two follow.
- [[concepts/observers]] — both connectors opt out of per-resource observer strategies entirely (always refuse).
- [[entities/gatekeeper-mcp]]
- [[entities/gatekeeper-mcp-portal]]

## Sources

- raw/github_doc-packages-gatekeeper-mcp-readme-md.md
- raw/github_doc-packages-gatekeeper-mcp-portal-readme-md.md
- raw/github_doc-packages-mcp-shared-readme-md.md
