wikis / Cloudflare OS / wiki / concepts / mcp-support.md view as markdown report a mistake
MCP Support
Definition
Cloudflare OS supports the Model Context Protocol (MCP) via two Gatekeeper connectors that share a common library, @gadgets/mcp-shared:
- 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.
- gatekeeper mcp portal โ an administrator configures a single organizational MCP portal URL (e.g. a Cloudflare MCP server portal); 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.readOnlyHintclassifies reads; nothing the server claims can auto-apply a write. Bothgatekeeper-mcp(always) andgatekeeper-mcp-portal(by default) use this tier.vettedโ a deployment has asserted an endpoint's annotations can be relied on, sodestructiveHint: falseplusidempotentHint: truemay drive auto-approval. Onlygatekeeper-mcp-portalcan reach this tier, and only withMCP_PORTAL_TRUST_ANNOTATIONS=trueset, 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-mcpconfig:BASE_URL,MCP_CLIENT_NAME,MCP_ALLOW_INSECURE(local dev only โ disables the host blocklist and HTTPS requirement).gatekeeper-mcp-portalconfig: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, 50tools/listpages, 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_publiccompatibility 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_changedis 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 โ
addObserverrefuses 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 observers and sharing. - Sharing UI reports late โ
GadgetMetadata.sharingProhibitedderives only fromprohibitAllSharing, so creating a share key can appear to succeed and only fail when the recipient opens it; fixing this needs a kernel change. - Honoring
readOnlyHintonbyois 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 dishonestbyoserver 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
- gatekeepers โ MCP support is implemented as two ordinary Gatekeepers.
- writing a gatekeeper โ the general connector-authoring workflow these two follow.
- observers โ both connectors opt out of per-resource observer strategies entirely (always refuse).
- gatekeeper mcp
- 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
