Prerequisites
- A Cloudflare account with Workers and Zero Trust (Access) enabled.
- The Executor repo checked out, with
dependencies installed (
bun install).
Deploy
Fromapps/host-cloudflare:
deploy:setup is idempotent: it creates or reuses the executor D1 database,
writes its id into wrangler.jsonc, generates and uploads the EXECUTOR_SECRET_KEY
secret, and deploys the Worker. It then prints the one remaining manual step.
Put it behind Cloudflare Access
Until the Worker is behind an Access application, every API and MCP route returns401. In the Zero Trust dashboard:
- Go to Access → Applications → Add an application → Self-hosted.
-
Set the application domain to
executor-cloudflare.<your-subdomain>.workers.dev. -
Add an Access policy (for example, emails ending in
@yourcompany.com). -
Copy the application Audience (AUD) tag, then redeploy with your Access settings:
You can also set
ACCESS_AUDandACCESS_TEAM_DOMAINinwrangler.jsoncand redeploy.
Connect an agent
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.<your-subdomain>.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 theCf-Access-Jwt-Assertion); no
code or redeploy is needed.
- Open the self-hosted Access application gating the Worker (the one from the previous section).
-
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
/mcprequest with an OAuth401challenge 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-serverand/.well-known/oauth-protected-resourcethen 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 or404there). -
Point your client at the
/mcpURL 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.
/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.- 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).
-
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 Authaction lets the token through without an interactive IdP login. -
Configure your MCP client to send the token on every request as the
CF-Access-Client-IdandCF-Access-Client-Secretheaders. Access exchanges them for theCf-Access-Jwt-AssertionJWT (carrying the token’scommon_name) that the Worker validates, exactly like a human login. Withadd-mcp, which writes the config for whichever agents it detects:For anmcpServersconfig block: