Cloudflare Setup¶
Tunnel — public hostname¶
In the Cloudflare Zero Trust dashboard, on the existing tunnel, add a public hostname:
- Hostname:
365-mcp.example.com - Service:
http://365-mcp:3000(must behttp://, nothttps://)
Access — protect the hostname¶
Identity provider¶
Under Zero Trust → Settings → Authentication, ensure at least one IdP exists. One-time PIN (email OTP) works out of the box. (Used only if you add a browser/identity policy; the service-token path below does not need it.)
Create a self-hosted Access application¶
Zero Trust → Access → Applications → Add an application → Self-hosted
- Application name:
365-mcp.example.com - Destination → Public hostname:
365-mcp.example.com(no path) - Leave Managed OAuth = OFF (Additional settings → OAuth). Managed OAuth is only for browser/OAuth clients and is incompatible with service tokens on the same app.
Create a service token¶
Zero Trust → Access → Service Auth → Create Service Token
- Name e.g.
365 MCP, duration 1 year. - Copy the Client ID and Client Secret now — the secret is shown only once.
Add a Service Auth policy to the app¶
App → Policies → Add a policy
- Action:
Service Auth← must be Service Auth, not Allow - Include → Service Token → select
365 MCP - Save. Confirm the policy list shows
SERVICE AUTH.
With
Allow+ a service-token selector, Cloudflare also demands an interactive identity login and the headers won't work — Cloudflare's own UI warns about this.
(Optional: to also allow human browser access, add a second Allow policy with an identity selector, e.g. Emails → your address. Not required for Claude.)
Collect the two values the origin needs¶
- AUD tag: App → Additional settings → AUD tag → put in
CF_ACCESS_AUD. - Team domain:
https://<team>.cloudflareaccess.com→ put inCF_ACCESS_TEAM_DOMAIN. (Also visible in the discovery metadata athttps://365-mcp.example.com/.well-known/oauth-authorization-server.)
Set those in .env, CF_ACCESS_ENFORCE=true, and docker compose up -d --build.