Topology
End-to-end flow
When Claude Code spawns the MCP server over stdio, main() first probes GET /health. If the broker isn't up, it shells out bun broker.ts with proc.unref() so the daemon outlives this MCP process, then polls for up to six seconds until /health answers. Then it gathers context (cwd, git_root via git rev-parse --show-toplevel, parent tty) and POST /registers with the OS pid. The broker generates an 8-char alphanumeric ID and returns it.
show source · server.ts
// server.ts — startup async function main() { await ensureBroker(); // spawn bun broker.ts if /health is dead myCwd = process.cwd(); myGitRoot = await getGitRoot(myCwd); const reg = await brokerFetch<RegisterResponse>("/register", { pid: process.pid, cwd: myCwd, git_root: myGitRoot, tty, summary: initialSummary, }); myId = reg.id; // e.g. "k3p9wf2x" }
Best-effort, non-blocking. If OPENAI_API_KEY is set, the server asks the OpenAI Chat Completions API (model name in source: gpt-5.4-nano) for a 1–2 sentence guess at what this developer is doing, computed from cwd, current git branch, files in git diff --name-only HEAD, and the last five commits. Startup races the summary against a 3 s timeout — if it loses, it registers with an empty summary and patches it in via /set-summary later. Other peers see this string when they call list_peers.
show source · shared/summarize.ts
// shared/summarize.ts export async function generateSummary(context) { const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) return null; // silent fallback const res = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, ... }, body: JSON.stringify({ model: "gpt-5.4-nano", messages: [ { role: "system", content: "..." }, ... ], max_tokens: 100, }), signal: AbortSignal.timeout(5000), }); ... }
When Claude asks "who else is here?" it invokes the list_peers tool with a scope of machine, directory, or repo. The MCP server forwards to POST /list-peers, sending its own cwd, git_root, and exclude_id. The broker filters the SQLite peers table by the scope, then liveness-checks each row with process.kill(pid, 0) — any PID that no longer exists is deleted on the spot, so the returned list is never stale. The MCP server formats peers into a text block for the model.
show source · broker.ts
// broker.ts — handleListPeers switch (body.scope) { case "machine": peers = selectAllPeers.all(); break; case "directory": peers = selectPeersByDirectory.all(body.cwd); break; case "repo": peers = body.git_root ? selectPeersByGitRoot.all(body.git_root) : selectPeersByDirectory.all(body.cwd); } if (body.exclude_id) peers = peers.filter(p => p.id !== body.exclude_id); // liveness probe — drops dead PIDs eagerly return peers.filter(p => { try { process.kill(p.pid, 0); return true; } catch { deletePeer.run(p.id); return false; } });
Outbound is the easy half. Claude A calls send_message({ to_id, message }). The MCP server POSTs /send-message with { from_id: myId, to_id, text }. The broker verifies the target peer still exists in the peers table, then inserts a row into messages with delivered = 0. That row is now Peer B's outbox — the broker does not proactively notify B. Delivery happens on B's next poll tick.
show source · broker.ts
// broker.ts — handleSendMessage const target = db.query("SELECT id FROM peers WHERE id = ?").get(body.to_id); if (!target) return { ok: false, error: `Peer ${body.to_id} not found` }; insertMessage.run(body.from_id, body.to_id, body.text, new Date().toISOString()); // delivered defaults to 0 — Peer B will claim it on its next /poll-messages return { ok: true };
This is the heart of the two-way story. Every Peer B's MCP server runs a setInterval(pollAndPushMessages, 1000) from main(). Each tick it POSTs /poll-messages with its own ID. The broker returns every row with to_id = myId AND delivered = 0, then marks them delivered = 1 in the same transaction so they're consumed exactly once. For each returned message, the MCP server enriches it by looking up the sender's summary and cwd, then emits a channel notification (next step).
show source · server.ts
// server.ts — pollAndPushMessages const result = await brokerFetch<PollMessagesResponse>("/poll-messages", { id: myId }); for (const msg of result.messages) { // look up sender context to attach to the channel notification const peers = await brokerFetch<Peer[]>("/list-peers", { scope: "machine", ... }); const sender = peers.find(p => p.id === msg.from_id); await mcp.notification({ method: "notifications/claude/channel", params: { content: msg.text, meta: { from_id: msg.from_id, from_summary: sender?.summary ?? "", from_cwd: sender?.cwd ?? "", sent_at: msg.sent_at, }, }, }); }
This is what "messages appear instantly inside Claude" actually means. The MCP server's capability handshake declares experimental: { "claude/channel": {} } alongside the standard tools capability. When Claude Code is launched with --dangerously-load-development-channels server:claude-peers, it watches the MCP server's outbound JSON-RPC for notifications/claude/channel and, when one arrives, splices a <channel source="claude-peers" ...> block into the model's running context. The model sees a new message the same way it sees a user turn — no tool call required. The pre-loaded instructions string tells the model to respond immediately, treating peer messages like a coworker tap on the shoulder.
Without that --dangerously-load-development-channels flag, the tool surface still works — but channel notifications get ignored, so the model only learns about new messages when it (or the user) explicitly calls check_messages.
show source · server.ts
// server.ts — MCP capability handshake const mcp = new Server( { name: "claude-peers", version: "0.1.0" }, { capabilities: { experimental: { "claude/channel": {} }, // ← opts into the channel protocol tools: {}, }, instructions: `...When you receive a <channel source="claude-peers" ...> message, RESPOND IMMEDIATELY. Treat incoming peer messages like a coworker tapping you on the shoulder...`, } ); // later, in the poll loop, every undelivered row becomes: mcp.notification({ method: "notifications/claude/channel", params: { content: msg.text, meta: { from_id, from_summary, from_cwd, sent_at } }, });
Three overlapping liveness mechanisms keep the address book honest. Each MCP server POSTs /heartbeat every 15 s, updating its last_seen timestamp. The broker runs cleanStalePeers() on startup and every 30 s, calling process.kill(pid, 0) on every registered peer and deleting any whose PID has disappeared (and dropping their undelivered messages). On SIGINT/SIGTERM, the MCP server politely POSTs /unregister. Re-registration with the same pid deletes the old row first, so a restarted Claude Code gets a fresh peer ID rather than two.
show source · broker.ts
// broker.ts — cleanStalePeers function cleanStalePeers() { const peers = db.query("SELECT id, pid FROM peers").all(); for (const p of peers) { try { process.kill(p.pid, 0); } // signal 0 = "are you alive?" catch { db.run("DELETE FROM peers WHERE id = ?", [p.id]); db.run("DELETE FROM messages WHERE to_id = ? AND delivered = 0", [p.id]); } } } setInterval(cleanStalePeers, 30_000);