SwarmCode is an MCP server that lets two Claude Code instances on different machines message each other, so I stop copy-pasting between two terminal windows when one agent needs to hand work to another. Repo: github.com/spranab/swarmcode. npm: swarmcode-mcp. Neither the name nor the architecture it shipped with is the one it started with.
Claude Code has no native "wake me up when X happens" primitive that plays nicely with a human still typing in the same window. There's no interrupt. Anything that feels like push has to be built by bending some other piece of the toolset into the shape of one. On 2026-03-30, pairing with Claude Opus 4.6, I bent six different things into that shape and kept each one just long enough to find out how it broke.
The evening
28c8822 at 14:51 is the initial commit: "Initial release: MCP server for cross-machine Claude Code communication," named agent-bridge, published as mcp-agent-bridge.
The first push attempt was the crudest. By 16:55 (28daad0) the Stop hook was returning decision:block, which forces Claude to process the inbox before it's allowed to end its turn. A hook built for something else, hijacked.
At 17:17 (1d371fa) I switched to MCP sampling, createMessage, the protocol's own mechanism for a server to request a completion from the client. Between 17:26 and 17:44 I added per-workspace Redis channels plus a broadcast channel, then fixed a same-session bug where publish and subscribe were sharing one connection (e4a743b).
Then e8919c5 at 18:19: a one-shot Redis listener. Spawn it as a background Bash task; when a message arrives the listener exits; the exit fires a VS Code task notification; Claude reads it, replies, starts a new listener. Nine minutes on, ee51ded tagged that v1.2.0, "Real-time channel + background listener for true push in VS Code."
a1c4757 at 19:19 is labelled "v1.3.0: Clean architecture," which undersells it. The body opens "Breaking: removed SSE server (server.js rewritten as CLI router)," and that single commit deleted the SSE server, tools.js, redis.js, check-inbox.js, the Dockerfile, .dockerignore, the K8s bridge and ingress manifests, and the express dependency. An entire transport layer, dropped in one go. Thirty-nine seconds later 532d66f pulled the ghcr.io build out of CI, since there was no longer a Dockerfile to build.
21:56, 0ef29bf: a persistent listener plus a queue file, to make message delivery uninterrupted. Let the listener run forever and write incoming messages to a local file (.agent-bridge-inbox) that a hook reads each turn. No restart, no gap between listeners. It survived nineteen minutes. ac1ae4f at 22:16: "Revert to one-shot listener pattern (proven real-time)."
0cadbb7 at 22:23 went the other way entirely, with no client-side process at all. Swap the plain Server for McpServer with experimental task support and register the listener as a real MCP task:
mcp.experimental.tasks.registerToolTask(
"bridge_listen",
{
description: "Wait for the next message from another workspace. ...",
execution: { taskSupport: "required" },
},
{
createTask: async (extra) => {
const task = await extra.taskStore.createTask({
ttl: 600000, // 10 min
pollInterval: 1000, // poll every 1 second
});
waitForMessage().then(async (msg) => {
// ...
await extra.taskStore.storeTaskResult(task.taskId, "completed", result);
});
return { task };
},
}
);
Two patches followed before 22:36. 41eb1dd dropped taskSupport from required to optional, because the VS Code extension doesn't advertise required task support. 162dd35 handed the server a task store of its own:
+import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental";
...
+const taskStore = new InMemoryTaskStore();
const mcp = new McpServer(
{ name: "agent-bridge", version: "1.0.0" },
{
+ taskStore,
Commit message: "VS Code extension doesn't provide a taskStore, but we can provide our own."
By 23:19 (7b73909) I'd walked away from that too. The commit calls its replacement "The proven pattern," and its diff to src/channel.js (the instructions string the MCP server injects into Claude's context) goes back to the background-listener process from five hours earlier and puts a blocking read on top of it:
Step 1: Start the background listener:
Bash(run_in_background=true, timeout=600000): AGENT_BRIDGE_REDIS_URL=... npx -y mcp-agent-bridge listen
Step 2: Block-wait for the message:
TaskOutput(task_id=<id from step 1>, block=true, timeout=600000)
When a message arrives, TaskOutput returns the message. ...
This creates a true real-time event loop with zero user intervention.
The same diff demotes bridge_listen, the task-protocol tool from fifty-six minutes earlier, to "(experimental, may not work in all environments)" in the tools list.
Thirty-one minutes after that, aa40a2d at 23:51: "Use task-notification pattern (non-blocking) instead of TaskOutput(block)." Reason given: "TaskOutput blocks all user interaction." Same instructions string, rewritten again:
The listener runs in the background. When a message arrives, you get a task-notification.
When you see the notification:
1. Read the task output file to see the message
2. Call bridge_receive() to mark messages as read
3. Reply with bridge_send(to: "sender", type: "answer", content: "...")
4. Start a new listener (same command as above)
This keeps you free to interact with the user while listening for messages.
"Zero user intervention" at 23:19; "keeps you free to interact with the user" at 23:51. From the first push attempt at 16:55 to that revert is six hours and fifty-six minutes, and what came out the other end — background listener, exits on message, fires a notification, gets restarted — is functionally what already existed at 18:28.
Except that isn't where it stopped
At 00:13:12, twenty-two minutes later, past midnight but the same sitting, 64dbd7d: "Add CronCreate backup polling to MCP instructions."
Instructions now tell Claude to set up both:
- Background listener (real-time push)
- CronCreate every 5 min (backup polling)
Belt and suspenders — real-time when listener is active, 5-min fallback when it times out or misses.
The listener runs on a ten-minute Bash timeout, and the only thing that restarts it is Claude working through to step 4 of an instructions blob. The commit's stated reasons for the cron are that the listener times out or misses. So what shipped isn't push. It's push with a five-minute floor underneath, and the floor is the half that doesn't depend on a model remembering a numbered list.
That's still how it works. The live src/channel.js opens with "CRITICAL: Do BOTH of these at the start of every conversation," then the listener, then CronCreate(cron="*/5 * * * *", ...).
On MCP's task primitives
registerToolTask looks purpose-built for this problem: a server-side wait that completes when something happens, nothing to babysit on the client. I'd still not reach for it first, and my reason is those two patches. Getting the primitive to exist at all in the environment I was targeting meant finding out that VS Code doesn't advertise required task support, then that it ships no task store, then shipping my own. Twelve minutes of patching before the thing could run, all of it spent on the gap between what the spec describes and what the client implements. That gap is mine to fill every time, and it moves.
That is not an argument about what killed the next attempt. TaskOutput(block=true) freezing the user's session is Claude Code Bash behaviour, and by then I'd already dropped the task protocol; the same diff that introduced the blocking read is the one that demoted bridge_listen to experimental. Two unrelated failures that happened to land within an hour of each other. The registerToolTask code is still in src/channel.js today, renamed swarm_listen, still registered, just no longer mentioned in the instructions anyone reads.
The rename, and a name one hyphen away
e5cd49d, 2026-03-31 07:52:26: "Rename to SwarmCode — the missing networking layer for Claude Code." agent-bridge became swarmcode, tools went bridge_* to swarm_*, env vars AGENT_BRIDGE_* to SWARMCODE_*. At 08:03:19, b131fcc: "npm package name: swarmcode-mcp (swarmcode was too similar to swarm-code)."
npm blocked the bare name for similarity, not because it was taken; swarmcode sits unregistered to this day. The package it was too similar to is real and actively published: swarm-code, "Open-source swarm-native coding agent orchestrator — spawns parallel coding agents in isolated git worktrees," keyworded swarm, coding-agent, orchestrator, llm, cli, agent. I had picked a name one hyphen off a tool in the same corner of the same problem space, and npm's similarity check noticed before I did. The project is swarmcode everywhere in the source and swarmcode-mcp on npm.
The bug, thirty-five hours in
1745bae, 2026-04-01 02:04:31: "Fix hook performance: use global binary, add Redis timeout." The UserPromptSubmit hook that checks the inbox was shelling out to npx -y swarmcode-mcp check on every single prompt. npx resolves the package, and sometimes downloads it, before running anything: 5-10 seconds, against roughly 0.5 for a globally installed binary. Every prompt paid it. Chats were hanging.
try {
const { default: Redis } = await import("ioredis");
- const redis = new Redis(REDIS_URL, { keyPrefix: "swarmcode:" });
+ const redis = new Redis(REDIS_URL, {
+ keyPrefix: "swarmcode:",
+ connectTimeout: 2000,
+ commandTimeout: 2000,
+ maxRetriesPerRequest: 1,
+ retryStrategy: () => null, // don't retry — fail fast
+ });
const raw = await redis.lrange(`inbox:${WORKSPACE_ID}`, 0, -1);
messages = raw.map((m) => JSON.parse(m)).reverse();
await redis.quit();
- } catch {}
+ } catch {
+ // Redis unavailable — silently skip
+ process.exit(0);
+ }
The fix pointed the hook at the installed swarmcode check binary, added 2-second Redis connect and command timeouts with retries off, cut the hook's own timeout to 3 seconds, and made an unreachable Redis fail silently rather than visibly.
The name that won't die
Four and a half months on, 7d05b94 (2026-08-18): "Add mcp-agent-bridge migration guide and lead with the problem." From the commit message: "The legacy npm name still gets ~6x the downloads of swarmcode-mcp and its npm page links here, so this README is where those users land." It added a migration banner, a bridge_* to swarm_* mapping table, and kept the old env vars working as a fallback so a half-migrated set of machines could still talk to each other mid-switch.
mcp-agent-bridge has gone 143, 118, 76 downloads per month across 2026-08-31, 09-07 and 09-14. swarmcode-mcp went 19, 31, 31. The 6x from August is about 2.4x now. The dead name is fading and still pulls more than twice the traffic of the live one.
Line 112 of that same README, today: "No polling. No cron. True event-driven push in VS Code." Line 122 is a section headed "Backup polling," and it says a 5-minute CronCreate runs alongside the listener as a safety net. Ten lines apart, in the file I rewrote to lead with the problem. I've known about the cron since 00:13 on March 31 and I still haven't fixed the sentence above it.
Pranab Sarkar, Independent Researcher
Top comments (0)