diff --git a/apps/docs/hosted/cloudflare.mdx b/apps/docs/hosted/cloudflare.mdx index 285642d16..1c691ce95 100644 --- a/apps/docs/hosted/cloudflare.mdx +++ b/apps/docs/hosted/cloudflare.mdx @@ -57,6 +57,95 @@ the issued JWT on every request. The Worker serves a streamable-HTTP MCP endpoint at `/mcp`, gated by Access like the rest of the surfaces. Point your client at -`https://executor-cloudflare..workers.dev/mcp`; it authenticates -through Cloudflare Access. See [MCP Proxy](/mcp-proxy) for how the endpoint exposes -your integrations. +`https://executor-cloudflare..workers.dev/mcp`. + +Because Access sits in front of the Worker, an MCP client that reaches `/mcp` +without a valid Access credential is served Cloudflare's HTML login page instead of +the endpoint. The streamable-HTTP client rejects that with +`Streamable HTTP error: Unexpected content type: text/html`: that is the Access login +page, not a Worker response. A browser passes Access via an interactive login, but an +MCP client cannot follow that redirect, so it has to authenticate another way. Choose +the path that matches your client. + +### For interactive clients: Managed OAuth (recommended) + +Cloudflare Access can run the MCP OAuth flow on the Worker's behalf, so a client +like Claude authenticates with a normal login popup and no manual headers. The Worker +keeps doing exactly what it already does (validate the `Cf-Access-Jwt-Assertion`); no +code or redeploy is needed. + +1. Open the self-hosted Access application gating the Worker (the one from the + previous section). +2. Enable **Managed OAuth** in the application's settings (the option that lets + non-browser clients authenticate over OAuth). With it on, Access answers an + unauthenticated `/mcp` request with an OAuth `401` challenge and serves the OAuth + discovery, authorize, and token endpoints itself, instead of the HTML login page. + Keep the Access application scoped to the whole Worker hostname (as set in the + previous section), not a single path like `/mcp`: the discovery probes clients + send to `/.well-known/oauth-authorization-server` and + `/.well-known/oauth-protected-resource` then land on Access too, which answers + them rather than letting them fall through to the Worker's SPA fallback (which the + Worker itself does not serve, so a path-scoped application would return HTML or + `404` there). +3. Point your client at the `/mcp` URL with no extra configuration. On connect it + discovers the OAuth endpoints, opens a browser popup to log in through Access, and + Cloudflare injects the validated JWT into every subsequent request. + + ```bash + npx add-mcp https://executor-cloudflare..workers.dev/mcp \ + --transport http --name executor + ``` + +This is the same flow Cloudflare documents in +[Secure MCP servers with Access](https://developers.cloudflare.com/cloudflare-one/access-controls/ai-controls/secure-mcp-servers/). + +If a client still won't start the OAuth flow after you enable Managed OAuth (it +classifies `/mcp` as not an MCP endpoint, or you still hit the +`Unexpected content type: text/html` error), its probe isn't consuming Access's +`401` challenge. Fall back to the service-token path below: it authenticates with +request headers and needs no client-side OAuth discovery, so it works regardless of +how the client handles the challenge. + +### For headless clients: service token + +When no human is present to complete the OAuth popup (CI, a background agent), +authenticate machine-to-machine with an Access service token instead. + +1. In the Zero Trust dashboard, go to **Access → Service Auth → Service Tokens → + Create Service Token**. Copy the **Client ID** and **Client Secret** (the secret + is shown once). +2. On the Access application gating the Worker, add a policy with **Action: Service + Auth** that includes that service token (Include → Service Token → your token). + The `Service Auth` action lets the token through without an interactive IdP login. +3. Configure your MCP client to send the token on every request as the + `CF-Access-Client-Id` and `CF-Access-Client-Secret` headers. Access exchanges them + for the `Cf-Access-Jwt-Assertion` JWT (carrying the token's `common_name`) that + the Worker validates, exactly like a human login. + + With [`add-mcp`](https://www.npmjs.com/package/add-mcp), which writes the config + for whichever agents it detects: + + ```bash + npx add-mcp https://executor-cloudflare..workers.dev/mcp \ + --transport http --name executor \ + --header "CF-Access-Client-Id: .access" \ + --header "CF-Access-Client-Secret: " + ``` + + For an `mcpServers` config block: + + ```json + { + "executor": { + "type": "http", + "url": "https://executor-cloudflare..workers.dev/mcp", + "headers": { + "CF-Access-Client-Id": ".access", + "CF-Access-Client-Secret": "" + } + } + } + ``` + +Once connected, see [MCP Proxy](/mcp-proxy) for how the endpoint exposes your +integrations.