Skip to content

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 be http://, not https://)

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 Authmust 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 in CF_ACCESS_TEAM_DOMAIN. (Also visible in the discovery metadata at https://365-mcp.example.com/.well-known/oauth-authorization-server.)

Set those in .env, CF_ACCESS_ENFORCE=true, and docker compose up -d --build.