mcp-remote's 1-hour OAuth token refresh path is fragile and breaks when Claude Desktop sleeps mid-refresh. Fixed by replacing all three with a single custom stdio MCP server that uses a long-lived API token instead. No more OAuth flows. Ever.
Three Cloudflare MCP connectors had been added to claude_desktop_config.json via the mcp-remote proxy:
cloudflare-graphql → https://graphql.mcp.cloudflare.com/ssecloudflare-observability → https://observability.mcp.cloudflare.com/ssecloudflare-workers-builds → https://builds.mcp.cloudflare.com/sseAll three connect via OAuth: mcp-remote spins up a localhost callback listener on a random port, opens the browser to dash.cloudflare.com for consent, then receives the redirect to complete auth. Tokens are 1-hour bearer tokens with an offline_access scope for silent refresh.
One by one, each connector started looping. Browser dialogs would open showing "There was an error fetching accounts. Please try again." with a fresh consent challenge each time. Clicking Authorize hit localhost:22749/oauth/callback which returned ERR_CONNECTION_REFUSED — the listener was already dead.
Three separate failure modes feeding the same loop:
mcp-remote falls back to interactive auth — opens the browser. But Cowork respawns the MCP process aggressively, and each respawn kills the prior listener before the user can click Authorize.~/.mcp-auth/ blocked clean retries even after killing all processes.This left every retry hitting a dead localhost port, generating fresh consent challenges that auto-expired, with the browser auto-refreshing tabs from earlier failed attempts. The loop was self-sustaining as long as the connector entry remained in config.
Initial fix: clear stale state, run mcp-remote manually in terminal so the OAuth flow completes without Cowork interfering. Tokens get cached, Cowork picks them up on next spawn. Worked for ~22 hours, then refresh failed overnight, restarting the loop.
The pattern was clear: any fix relying on the OAuth refresh path will break on the next refresh failure. We needed to bypass OAuth entirely.
Built a custom stdio MCP server in Node (zero dependencies) at ~/.claude/scripts/cloudflare-graphql-mcp/index.js. It speaks the MCP JSON-RPC protocol over stdin/stdout and authenticates to api.cloudflare.com using a long-lived API token.
Key design properties:
claude_desktop_config.json's env block (same pattern as the existing meta-marketing MCP)fetch and WebSocketThe Cloudflare API token has minimal-scope permissions: Account Analytics:Read, Account Settings:Read, Workers Scripts:Read, Workers Tail:Read, Workers Builds Configuration:Read, Zone:Read, Zone Analytics:Read.
| Capability | Old connector | New tool | Status |
|---|---|---|---|
| Run GraphQL Analytics queries | cloudflare-graphql | graphql_query | Working |
| List zones / get zone details | cloudflare-graphql | zones_list, zone_details | Working |
| List accounts | cloudflare-graphql | accounts_list | Working |
| Introspect GraphQL schema | cloudflare-graphql | graphql_schema_introspect | Working |
| List Workers in account | cloudflare-observability | workers_list | 109 workers, sorted by recency |
| Get Worker source code | cloudflare-observability | worker_get_code | ES module multipart parsed |
| Get Worker bindings/settings | cloudflare-observability | worker_metadata | Working |
| Real-time Worker log tail | cloudflare-observability | worker_tail | WebSocket-based, 1–60 sec windows |
| Search Cloudflare docs | cloudflare-observability | cloudflare_docs_search | Uses cached llms-full.txt |
| Workers Builds CI listing | cloudflare-workers-builds | workers_builds_list | Endpoint not in public REST API — returns informative empty |
builds.mcp.cloudflare.com uses an internal API path that isn't published in the public REST API surface. Since you deploy via wrangler deploy from CLI rather than the Workers Builds CI feature, no builds exist in the CI system anyway. The tools are vestigial — if you ever connect a worker to git auto-deploys, we'll revisit the endpoint.
MCP servers in claude_desktop_config.json:
meta-marketing — local Python (unchanged)cloudflare-graphql — our custom stdio Node serverOAuth state cleared from ~/.mcp-auth/: all three Cloudflare endpoint hashes (graphql, observability, builds) have lock files, code verifiers, and tokens removed. No mcp-remote processes for Cloudflare endpoints will respawn.
Cached on disk: ~/.claude/cache/cf-docs-llms-full.txt (49MB Cloudflare developer documentation archive, refreshed every 7 days for the docs search tool).
worker_deploy using a Workers Scripts:Edit token scope. Decided against it — wrangler does too much (bundling, binding resolution, migration handling, source maps, type checking) and getting any of that wrong via raw API risks bricking workers or unbinding production KV namespaces. Wrangler stays the deploy path.~/Library/Application Support/Claude/claude_desktop_config.json under cloudflare-graphql, restart Cowork.
claude_desktop_config.json sit next to the original with .bak.<timestamp> suffix. The MCP server itself lives at ~/.claude/scripts/cloudflare-graphql-mcp/index.js and can be re-copied from the workspace folder if needed.
~/.claude/scripts/cloudflare-graphql-mcp/index.js — the MCP server (~415 lines, zero deps)~/Library/Application Support/Claude/claude_desktop_config.json — connector registration with API token in env~/.claude/cache/cf-docs-llms-full.txt — cached docs archive~/.mcp-auth/mcp-remote-0.1.37/ — vestigial OAuth state (cleared for the three CF endpoints)