Cloudflare Worker hosting a remote Model Context Protocol server for the Productive.io API, fronted by Cloudflare Access for SaaS acting as the upstream OAuth provider.
This Worker uses the Access for SaaS pattern via @cloudflare/workers-oauth-provider. MCP clients (e.g. Claude Desktop) perform standard OAuth 2.1 dynamic client registration against the Worker; the Worker brokers the actual login through your Access for SaaS OIDC application.
Flow:
- MCP client hits
/authorize. The Worker shows a consent screen, then redirects to your Access for SaaS authorization endpoint (PKCE). - User authenticates through Access (any IdP you have configured).
- Access redirects back to
/callbackwith an authorization code. The Worker exchanges it foraccess_token+id_token, verifies theid_tokenagainstACCESS_JWKS_URL, and reads theemailclaim. - Email is looked up in the
USER_MAPPINGsecret. Unknown emails get403. - Worker calls
OAUTH_PROVIDER.completeAuthorization()withpropscontaining the resolvedproductiveUserId+productiveApiToken. Those props are persisted inOAUTH_KVand exposed asthis.propsinside the MCP Durable Object on every subsequent/mcprequest.
| Path | Purpose |
|---|---|
GET /authorize / POST /authorize |
OAuth authorization endpoint |
POST /token |
OAuth token endpoint |
POST /register |
OAuth dynamic client registration |
GET /callback |
Upstream Access redirect target |
/mcp |
Streamable HTTP MCP transport (auth required) |
npx wrangler kv namespace create OAUTH_KVCopy the resulting id into wrangler.jsonc (replacing <Add-KV-ID>).
In the Cloudflare dashboard: Zero Trust → Access → Applications → Add an application → SaaS → OIDC.
- Application name:
Productive MCP - Redirect URLs:
https://<your-worker-host>/callback - Scopes:
openid,email,profile - Configure Access policies (e.g. emails on your domain).
Copy these from the OIDC settings:
| Field in dashboard | Secret name |
|---|---|
| Client ID | ACCESS_CLIENT_ID |
| Client secret | ACCESS_CLIENT_SECRET |
| Authorization endpoint | ACCESS_AUTHORIZATION_URL |
| Token endpoint | ACCESS_TOKEN_URL |
| Key endpoint (JWKS) | ACCESS_JWKS_URL |
# Productive
wrangler secret put PRODUCTIVE_ORG_ID
wrangler secret put USER_MAPPING < users.json
# Access for SaaS
wrangler secret put ACCESS_CLIENT_ID
wrangler secret put ACCESS_CLIENT_SECRET
wrangler secret put ACCESS_AUTHORIZATION_URL
wrangler secret put ACCESS_TOKEN_URL
wrangler secret put ACCESS_JWKS_URL
# Cookie signing key for the consent screen / OAuth state
openssl rand -hex 32 | wrangler secret put COOKIE_ENCRYPTION_KEYusers.json:
{
"alice@example.com": { "userId": 123456, "apiToken": "alice-token" },
"bob@example.com": { "userId": 234567, "apiToken": "bob-token" }
}Email keys are looked up case-insensitively (lowercased).
Copy .dev.vars.example to .dev.vars and fill in values, then:
npm install
npm run devA real Access for SaaS app is still required for the OAuth dance to work locally. Either add http://localhost:8787/callback as a redirect URL on your existing app, or create a separate dev app.
npm run deploy{
"mcpServers": {
"productive": {
"url": "https://<your-worker-host>/mcp"
}
}
}The first request triggers the OAuth flow in your browser. After successful Access login, subsequent requests reuse the issued token