Any Claude instance with the Cloudflare Developer Platform MCP connected can deploy this Worker from scratch — no local machine, no Wrangler CLI, no terminal required.
This runbook documents every API call needed. Claude executes them directly via the CF MCP and bash_tool.
Connect the following MCP servers to your Claude session:
| MCP | Purpose |
|---|---|
| Cloudflare Developer Platform | Worker deploy, KV namespace create, route management |
| GitHub (optional) | Read worker.js directly from this repo |
You also need:
-
A Cloudflare account ID
-
A CF API token with all four of the following permissions:
Permission Scope Needed for Workers Scripts Account Deploy / update Worker code Workers KV Storage Account Create KV namespace Workers Routes Zone (All zones) Add subdomain route to Worker DNS Zone (All zones) Add A record for subdomain Important: Workers Scripts and Workers KV Storage are Account-level permissions. Workers Routes and DNS are Zone-level permissions — set them to "All zones" or select your specific zone. Missing any one of these will cause a partial failure mid-deploy.
Use the "Edit Cloudflare Workers" template at
dash.cloudflare.com/profile/api-tokensas a starting point, then add DNS: Edit manually. -
Your upstream MCP server PAT/token (e.g. GitHub PAT with
reposcope) -
A chosen AUTH_PIN (see Generating an AUTH_PIN)
-
A domain on Cloudflare DNS for the Worker route
curl -s "https://api.cloudflare.com/client/v4/accounts" \
-H "Authorization: Bearer $CF_TOKEN" \
| python3 -c "import json,sys; [print(a['id'], a['name']) for a in json.load(sys.stdin)['result']]"curl -s -X POST \
"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/storage/kv/namespaces" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "github-mcp-oauth"}' \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('KV ID:', d['result']['id'])"Save the KV ID — you need it in Step 3.
Fetch worker.js from this repo and deploy it with all three bindings in one API call:
import urllib.request, json, base64
CF_TOKEN = "your-cf-token"
ACCOUNT_ID = "your-account-id"
KV_ID = "kv-namespace-id-from-step-2"
UPSTREAM_PAT = "ghp_your-github-pat"
AUTH_PIN = "YOUR8PIN"
# Fetch worker.js from GitHub
req = urllib.request.Request(
"https://api.github.com/repos/LIFEAI/cf-oauth-mcp-proxy/contents/worker.js",
headers={"Accept": "application/vnd.github.v3.raw"}
)
with urllib.request.urlopen(req) as r:
worker_code = r.read()
metadata = json.dumps({
"main_module": "worker.js",
"bindings": [
{"type": "kv_namespace", "name": "OAUTH_KV", "namespace_id": KV_ID},
{"type": "secret_text", "name": "UPSTREAM_TOKEN", "text": UPSTREAM_PAT},
{"type": "secret_text", "name": "AUTH_PIN", "text": AUTH_PIN},
{"type": "plain_text", "name": "BASE_URL", "text": "https://YOUR_WORKER_DOMAIN"},
{"type": "plain_text", "name": "UPSTREAM_MCP", "text": "https://api.githubcopilot.com/mcp"},
]
}).encode()
boundary = b"----MCPProxyBoundary"
body = (
b"--" + boundary + b"\r\n"
b'Content-Disposition: form-data; name="metadata"\r\n'
b"Content-Type: application/json\r\n\r\n" +
metadata + b"\r\n"
b"--" + boundary + b"\r\n"
b'Content-Disposition: form-data; name="worker.js"; filename="worker.js"\r\n'
b"Content-Type: application/javascript+module\r\n\r\n" +
worker_code + b"\r\n"
b"--" + boundary + b"--\r\n"
)
req = urllib.request.Request(
f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/workers/scripts/cf-oauth-mcp-proxy",
data=body,
method="PUT",
headers={
"Authorization": f"Bearer {CF_TOKEN}",
"Content-Type": f"multipart/form-data; boundary={boundary.decode()}",
}
)
with urllib.request.urlopen(req) as r:
d = json.load(r)
print("Deployed:", d.get("success"))
print("Errors:", d.get("errors", []))CF_TOKEN=your-cf-token
ZONE_ID=your-zone-id # find at dash.cloudflare.com → your domain → Overview
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "A",
"name": "YOUR_SUBDOMAIN",
"content": "192.0.2.1",
"proxied": true,
"ttl": 1
}' \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('DNS:', d.get('success'), d.get('result',{}).get('name',''))"The IP
192.0.2.1is a placeholder — Cloudflare proxied DNS for Workers doesn't route to the IP, it routes to the Worker via the route in Step 5.
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/workers/routes" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"pattern":"YOUR_SUBDOMAIN.YOUR_DOMAIN/*","script":"cf-oauth-mcp-proxy"}' \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('Route:', d.get('success'), d.get('result',{}).get('id',''))"# OAuth discovery
curl -s "https://YOUR_WORKER_DOMAIN/.well-known/oauth-authorization-server" | python3 -m json.tool
# MCP proxy (expect 401 Unauthorized — correct, no token yet)
curl -s -o /dev/null -w "HTTP %{http_code}" -X POST "https://YOUR_WORKER_DOMAIN/mcp"Both should return valid responses. If /.well-known/ returns a 522/526, wait 30 seconds for DNS + SSL to propagate.
- Settings → Connectors → Add custom connector
- URL:
https://YOUR_WORKER_DOMAIN/mcp - Click Connect → browser opens PIN consent page
- Enter your AUTH_PIN → Authorize Access → done ✅
Repeat Step 3 with the latest worker.js from the repo. Bindings are preserved if you include them in the metadata. Existing KV tokens remain valid — no re-authorization needed.
To rotate the PAT or PIN, repeat Step 3 with the new secret values. KV tokens are unaffected.
| Symptom | Cause | Fix |
|---|---|---|
526 on all requests |
SSL cert not issued yet | Wait 30–60s, CF Traefik needs time |
401 on /mcp |
No Bearer token | Expected — complete OAuth flow in claude.ai |
invalid_grant on /token |
Code expired or PKCE mismatch | Re-trigger OAuth flow |
| PIN page returns 401 | Wrong PIN | Check AUTH_PIN secret, redeploy if rotated |
Authentication error on deploy |
CF token missing permissions | Add Workers Scripts: Edit + Workers KV Storage: Edit |