the Room Protocol · transport
MCP as transport: stateless HTTP over long-lived SSE
A coordination protocol is only as useful as the number of agents that can speak it. Riding on MCP meant every capable agent could join with zero custom client code — but only if I got the transport and the tool surface right.
The four earlier parts of this series were about what the room is: the primitives, the concurrency, the single-store architecture, the typed context. This one is about how an agent actually reaches it. That question has a boring-sounding answer with a lot riding on it, because the transport decision is really a distribution decision. A coordination substrate that agents can't easily connect to is a coordination substrate nobody uses.
The distribution problem
Set aside the internals and ask the plain question: how does a Claude Code instance call read_plan? The honest options were three.
MCP wins because it's the layer agents already speak for tool use. Discovery and typed schemas come with the protocol, so the agent learns what tools exist and how to call them by asking the server, not by reading my documentation. The moment roomd — the reference server — is in an agent's config, all 19 coordination tools are available to it with no further work. That's the difference between “integrate our SDK” and “paste this into settings.”
// the entire client-side integration — two lines of real config
{
"mcpServers": {
"room-protocol": {
"type": "http",
"url": "https://roomd.sh/mcp",
"headers": { "Authorization": "Bearer YOUR_SECRET" }
}
}
}The payoff is zero client lock-in and near-zero integration cost. It drops into whatever agent stack a team already runs, which is the cheapest possible path to adoption: you never ask anyone to change their tools, you ride the ones they already have.
Choosing MCP settles the “what protocol” question. It leaves a sharper one: MCP can be spoken over more than one transport, and the obvious default is the wrong one here.
The transport most examples reach for
The familiar way to run an MCP server is a long-lived connection: the client opens a Server-Sent Events stream, a session is established, and it stays open with per-connection state held in the server's memory. It's the default in a lot of examples. It also quietly reintroduces every problem that statelessness was chosen to avoid: sessions pinned to one instance, sticky routing, connections dropped on every deploy, session state to migrate when scaling out.
What the Room Protocol uses instead
roomd uses the modern streamable-HTTP transport in stateless mode. There is no session to establish and no stream to keep alive. Every tool call is an independent HTTP request that builds a fresh server, does its work against Redis, and returns:
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless: no session id, no pinning
});
const server = createMcpServer(keyCtx); // fresh per request
await server.connect(transport);
return transport.handleRequest(c.req.raw);This is the transport-level twin of the architecture bet. A stateless server over a stateless transport talking to a store over stateless HTTP requests is one coherent design from top to bottom. It's also why the client config says "type": "http" and not "sse"— a small line that reflects the whole stance. The cost is that the server can't push to the client, so agents poll for updates. For the current design that's an acceptable trade, and it's the honest limitation I'll come back to.
Tools designed for how agents actually work
With MCP, the tools arethe interface an agent sees. So their design is product design, not just API surface. Two tools exist purely because of how agents behave, and they're the clearest examples of treating the tool list as UX.
An agent frequently starts cold — a fresh session with no memory of the room. The naive path is five calls: read the plan, get my tasks, get unread events, count context, check presence. So there's one tool, get_my_summary, that returns all of it in a single round trip. It exists because a cold start is the most common and most latency-sensitive moment an agent has, and collapsing five calls into one is a real UX win for a machine just as much as for a person.
The second is get_unread_events, which reads from a per-agent cursor and advances it, so each agent gets every event exactly once without tracking anything client-side. The protocol encodes the intended usage pattern directly into the tool, rather than leaving the agent to implement “what have I already seen” itself. Good protocol design pushes the fiddly bookkeeping into the server where it's done once and correctly.
Under the hood each tool is registered with a description and a typed input schema, and every handler runs the room-access check before doing anything, so access control isn't a separate middleware an agent could route around — it's baked into every operation on shared state:
server.registerTool(
"read_plan",
{ description: "Read the current plan (tasks, status, owners) for a room",
inputSchema: readPlanInput.shape },
async (input) => {
await assertRoomAccess(input.roomId, keyCtx); // ownership enforced here
const result = await readPlan(input);
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
},
);What riding on MCP costs
The honest tradeoffs. MCP is young and still moving, so building on it means tracking a spec that changes — the shift away from the older SSE transport is exactly that kind of churn, and there will be more. Stateless transport means no server-initiated push yet, so “real-time” is really “polling on a short interval,” and instant notifications are a roadmap item that will likely want a pub/sub layer. And a growing tool count is its own risk: 19 is comfortable, but a coordination surface can sprawl, and every tool is something an agent has to understand. Keeping the set small and purposeful is ongoing work, not a solved thing.
None of that outweighs the core win. By building on the protocol agents already speak, the Room Protocol asks for essentially nothing from the people adopting it: no SDK, no new client, no tools to abandon. Two lines of config and an agent is in the room. The lowest-friction path wins almost every time, and “change nothing” is the easiest sell there is.
— shreyas
more in this series
01Coordinating agents through shared state · the idea
02Concurrency control for a shared plan · locks and cursors
03A stateless server on a single Redis · architecture
04Typed context over prose and vector search · context model
05MCP as transport · you are here
06Multi-tenancy on one Redis · isolation and access