From 4759c3e6fce9a1c1c3ab9509a5eb533a65fc0d16 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 25 Jun 2026 18:42:08 +0900 Subject: [PATCH 1/3] docs(host-cloudflare): document MCP client auth for Access-gated /mcp Explain why an MCP client hits 'Unexpected content type: text/html' when connecting to the Access-gated /mcp endpoint (it receives the Cloudflare Access HTML login page, not a Worker response), and document the two ways to authenticate: Managed OAuth for interactive clients and an Access service token for headless clients. --- apps/docs/hosted/cloudflare.mdx | 81 +++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/apps/docs/hosted/cloudflare.mdx b/apps/docs/hosted/cloudflare.mdx index 285642d16..69951e143 100644 --- a/apps/docs/hosted/cloudflare.mdx +++ b/apps/docs/hosted/cloudflare.mdx @@ -57,6 +57,81 @@ 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. +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/). + +### 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. From 4889aa4d151b6d87fb1f5ba071443439bc31021b Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 25 Jun 2026 20:05:07 +0900 Subject: [PATCH 2/3] docs(cloudflare): clarify Managed OAuth covers discovery routes Note that the Access application must be scoped to the whole Worker hostname so /.well-known OAuth discovery probes are answered by Access, not the Worker SPA fallback. --- apps/docs/hosted/cloudflare.mdx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/docs/hosted/cloudflare.mdx b/apps/docs/hosted/cloudflare.mdx index 69951e143..243550e85 100644 --- a/apps/docs/hosted/cloudflare.mdx +++ b/apps/docs/hosted/cloudflare.mdx @@ -80,6 +80,13 @@ code or redeploy is needed. 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. From 86b955c45d11223b7a4899a834448af731123da2 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 25 Jun 2026 20:11:33 +0900 Subject: [PATCH 3/3] docs(cloudflare): add service-token fallback when Managed OAuth challenge is unconsumable If a client cannot consume Access's Managed OAuth 401 challenge, point readers to the service-token path, which needs no client-side OAuth discovery. --- apps/docs/hosted/cloudflare.mdx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/docs/hosted/cloudflare.mdx b/apps/docs/hosted/cloudflare.mdx index 243550e85..1c691ce95 100644 --- a/apps/docs/hosted/cloudflare.mdx +++ b/apps/docs/hosted/cloudflare.mdx @@ -99,6 +99,13 @@ code or redeploy is needed. 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),