Claude Bridge (chief-bridge)
A small remote MCP server on Cloudflare Workers that lets a Claude session and a Grok Bot agent ("Chief") talk to each other directly.
Claude gets a few MCP tools (send_to_chief, wait_for_reply, ...). When Claude sends a message, the bridge stores it in a Durable Object queue and fires a webhook that wakes your Grok Bot agent. The agent reads its inbox over a small authenticated HTTP API, posts a reply, and Claude picks the reply up with wait_for_reply.
This is a plain remote MCP server (Streamable HTTP + OAuth). It is not a Claude Code "Channels" server.
Architecture
flowchart LR
subgraph Claude
C[Claude.ai connector<br/>remote MCP client]
end
subgraph CF[Cloudflare Worker: chief-bridge]
O[OAuth provider<br/>workers-oauth-provider]
M["/mcp<br/>MCP tools"]
A["/chief/*<br/>bot HTTP API"]
Q[(Durable Object BoxmcpQueue<br/>SQLite queue + alarms)]
K[(KV OAUTH_KV<br/>OAuth grants/tokens)]
end
GH[GitHub OAuth<br/>owner check]
G[Grok Bot agent<br/>Chief]
C -- OAuth 2.1 + PKCE --> O
O -- login --> GH
O --- K
C -- Bearer token --> M
M --> Q
Q -- "webhook (Bearer CHIEF_WEBHOOK_KEY)" --> G
G -- "Bearer BOXMCP_CHIEF_API_KEY" --> A
A --> Q
Message flow
- Claude calls
send_to_chief(text). The worker stores it in theBoxmcpQueueDurable Object (a single SQLite-backed instance) and returns{thread_id, id}. - The Durable Object schedules a webhook with an alarm and
POSTs{"source":"chief-bridge","pending":N}toCHIEF_WEBHOOK_URLwithAuthorization: Bearer <CHIEF_WEBHOOK_KEY>(header name and scheme are configurable). The message text is not sent in the webhook. - The woken agent calls
GET /chief/inbox, thenPOST /chief/reply {thread_id, text}andPOST /chief/ack {ids}. - Meanwhile Claude loops on
wait_for_reply(thread_id, after_id). Each call long-polls the Durable Object for up to 55 s; a reply wakes it immediately. The server instructions tell Claude to keep looping for about 5 minutes and to callread_chief_repliesbefore concluding there is no reply.
Webhook scheduling (in src/queue.ts):
- No debounce: the first message after a quiet period fires immediately.
- At least 5 s between webhook fires; messages arriving inside that window are coalesced into the next fire, not dropped.
- Hard cap of 60 fires per hour (retries included).
- Non-2xx or network error: retries after +1, +5, +15 minutes (25 s request timeout, redirects not followed).
- If new messages arrive while a fire is in flight, a follow-up webhook is scheduled after a success; if retries are exhausted, the chain restarts once for messages that arrived during it.
MCP tools (Claude side)
| Tool | What it does |
|---|---|
send_to_chief |
Send a message (max 8000 chars). Omit thread_id to start a new thread. Rate-limited to 30/hour. |
wait_for_reply |
Long-poll for a reply in thread_id with id > after_id. timeout_s is clamped to 1..55 (default 50). |
read_chief_replies |
Read replies with id > since, optionally for one thread (max 20). |
get_thread |
All retained messages in a thread, both directions. |
chief_status |
Pending count, last activity, whether a webhook is configured/scheduled. |
Replies are returned wrapped as <chief_message untrusted="true" ...> with </> escaped and control/bidi characters stripped, and the server instructions tell Claude to treat that content as data, not instructions.
Bot API (agent side)
All /chief/* endpoints require Authorization: Bearer <BOXMCP_CHIEF_API_KEY> (or _NEXT, for rotation; compared in constant time). Rate-limited to 60/min.
| Method & path | Body | Result |
|---|---|---|
GET /chief/health |
– | {ok, pending, webhook_configured, webhook_url_valid} |
GET /chief/inbox?limit=1..100 |
– | Unacked Claude→Chief messages (untrusted: true) |
POST /chief/reply |
{thread_id, text} |
Reply in an existing thread; wakes waiting wait_for_reply calls |
POST /chief/ack |
{ids: [..]} |
Mark inbox messages handled (1..200 ids) |
POST /chief/send |
{text} |
Start a new Chief→Claude thread |
Request bodies are capped at 64 KB.
Setup
Requirements: a Cloudflare account (Workers + Durable Objects + KV), Node.js 22+, a GitHub account, and an agent that can receive an HTTPS webhook and make HTTP calls (built for a Grok Bot routine, but any agent works).
git clone https://github.com/asasds145/claude-bridge.git
cd claude-bridge
npm ci
npm test # vitest, runs in workerd via @cloudflare/vitest-plugin
npx tsc --noEmit
KV namespace
npx wrangler kv namespace create OAUTH_KVPut the returned id into
wrangler.jsonc(<YOUR_KV_NAMESPACE_ID>).GitHub OAuth app (GitHub → Settings → Developer settings → OAuth Apps):
- Homepage URL:
https://chief-bridge.<your-subdomain>.workers.dev - Authorization callback URL:
https://chief-bridge.<your-subdomain>.workers.dev/callback - Put the Client ID into
GITHUB_CLIENT_IDinwrangler.jsonc.
- Homepage URL:
Owner allowlist: set
BOXMCP_OWNER_GITHUB_IDto your numeric GitHub user id (curl https://api.github.com/users/<login>→id). Only this account can authorize.Public origin: set
BOXMCP_PUBLIC_ORIGINto the worker's https origin (recommended; requests to other hosts get421).Deploy and set secrets
npx wrangler deploy npx wrangler secret put GITHUB_CLIENT_SECRET npx wrangler secret put BOXMCP_CHIEF_API_KEY # e.g. openssl rand -hex 32 npx wrangler secret put CHIEF_WEBHOOK_URL # your agent's webhook (https) npx wrangler secret put CHIEF_WEBHOOK_KEY # bearer token the webhook expectsUntil
GITHUB_CLIENT_SECRETis set (and the owner id is non-zero),/authorizereturns 503. WithoutCHIEF_WEBHOOK_URL, messages queue but nothing is woken.Add the connector in Claude: Claude.ai → Settings → Connectors → Add custom connector → URL
https://chief-bridge.<your-subdomain>.workers.dev/mcp. Approve on the consent page, log in with GitHub.Wire up the agent: give your agent (e.g. a Grok Bot routine triggered by the webhook) the API key and tell it to read
/chief/inbox, reply via/chief/reply, then/chief/ack.Smoke test (read-only):
printf 'Authorization: Bearer %s\n' "$KEY" > chief.hdr # gitignored BRIDGE_URL=https://chief-bridge.<your-subdomain>.workers.dev CHIEF_HEADER_FILE=./chief.hdr npm run verify
For local development, copy .dev.vars.example to .dev.vars and run npx wrangler dev.
Configuration
| Name | Kind | Example / placeholder | Purpose |
|---|---|---|---|
OAUTH_KV |
KV binding | <YOUR_KV_NAMESPACE_ID> |
OAuth clients, grants, tokens |
QUEUE |
Durable Object | BoxmcpQueue |
Message queue, webhook scheduler, rate limits |
BOXMCP_OWNER_GITHUB_ID |
var | 12345678 |
The only GitHub user id allowed to authorize |
GITHUB_CLIENT_ID |
var | <your-github-oauth-client-id> |
GitHub OAuth app |
BOXMCP_PUBLIC_ORIGIN |
var | https://chief-bridge.<sub>.workers.dev |
Canonical origin (OAuth resource metadata) |
CHIEF_WEBHOOK_AUTH_HEADER |
var | Authorization |
Header used for the webhook key |
CHIEF_WEBHOOK_AUTH_SCHEME |
var | Bearer |
Scheme prefix (empty = raw key) |
GITHUB_CLIENT_SECRET |
secret | – | GitHub OAuth app secret |
BOXMCP_CHIEF_API_KEY |
secret | – | Key for /chief/* |
BOXMCP_CHIEF_API_KEY_NEXT |
secret (optional) | – | Second key for rotation |
CHIEF_WEBHOOK_URL |
secret | https://<agent-webhook-host>/... |
Where to send the wake-up webhook (must be https) |
CHIEF_WEBHOOK_KEY |
secret | – | Token sent with the webhook |
Security notes
- Single-owner OAuth. The worker is its own OAuth 2.1 authorization server (
@cloudflare/workers-oauth-provider) and uses GitHub only to prove identity: it requests no GitHub scopes, checks the numeric user id againstBOXMCP_OWNER_GITHUB_ID, and discards the GitHub token immediately. - PKCE S256 required, explicit consent page showing where the token will go, and
redirect_uripinned to Claude.ai's callback (https://claude.ai/api/mcp/auth_callback). Error redirects only go to that URI. - Access tokens live 1 hour.
/oauth/registeris limited to 5/hour per IP;/mcpto 20/min per token. - Bot endpoints need a separate bearer API key (constant-time compare, rotation via
_NEXT). - Webhook carries only a pending count, authenticated with its own bearer key; https-only, no redirects followed.
- Everything coming from the other side is wrapped and marked untrusted on both ends (prompt-injection hygiene).
- No secrets in the repo. All secrets are Worker secrets;
.dev.vars,.env*, and header files are gitignored. Logs record only event names, paths, and status codes (no bodies, headers, tokens, or query strings). - Retention: messages are kept 30 days, at most 200 per thread and 5000 total.
Limitations
- Single user, single queue (one Durable Object instance named
main). - The OAuth
redirect_uriis hard-coded to Claude.ai's connector callback, so other MCP clients (e.g. Claude Code, which uses a localhost callback) needCLAUDE_REDIRECT_URIinsrc/auth-github.tsadjusted before they can connect. - Asynchronous: a reply usually takes 1-3 minutes (agent wake-up time), and Claude has to keep polling with
wait_for_reply. - In-memory rate-limit counters reset when the Durable Object is evicted.
- The consent and error pages are in Korean.
- Webhook payload format is fixed (
{source, pending}); your agent must fetch the actual messages from/chief/inbox.
License
MIT, see LICENSE.
한국어 소개
Claude Bridge는 Cloudflare Workers 위에서 동작하는 원격 MCP 서버로, Claude(Claude.ai 커넥터)와 Grok Bot 에이전트("Chief")가 서로 직접 메시지를 주고받게 해 줍니다.
- Claude가
send_to_chief로 메시지를 보내면 Durable Object 큐에 저장되고, webhook이 Grok Bot 에이전트를 깨웁니다. - 에이전트는 API 키로 보호된
/chief/*엔드포인트에서 메시지를 읽고 답장하며, Claude는wait_for_reply로 답을 받습니다. - GitHub OAuth로 소유자 한 명만 연결할 수 있고, 모든 비밀값은
wrangler secret put으로만 설정합니다.
설치 방법은 위 Setup 섹션을 참고하세요.