@clementvial

Developer from Canada 🇨🇦
Product, infrastructure, AI, and web3.
Mostly on AWS and Cloudflare.

All notes

A Remote MCP Server on Workers Needs Somewhere to Put the Session

My first remote MCP server worked flawlessly against wrangler dev and fell apart the moment it was deployed. Tools listed fine, then calling one returned nothing at all.

The SSE transport is the reason. It uses two endpoints: the client opens a long-lived GET /sse for the server to push messages down, and sends every request as a separate POST /messages. Those are two different HTTP requests that must agree about one session.

Locally there’s a single process, so a module-level Map of sessions works. In production each request can land in a different isolate, in a different city. The POST arrives somewhere that has never heard of the stream it’s supposed to answer on.

Give the session an address

Durable Objects solve exactly this, and the agents package wires it up so you don’t hand-roll the routing:

src/index.ts
export class MyMCP extends McpAgent {
server = new McpServer({ name: 'notes', version: '1.0.0' });
async init() {
this.server.tool(
'search_notes',
'Search published notes by keyword',
{ query: z.string() },
async ({ query }) => ({ content: [{ type: 'text', text: await search(query) }] }),
);
}
}
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const { pathname } = new URL(request.url);
return pathname === '/sse'
? MyMCP.serveSSE('/sse').fetch(request, env, ctx)
: MyMCP.serve('/mcp').fetch(request, env, ctx);
},
};

Each session becomes a Durable Object, so both requests reach the same place regardless of which isolate they hit.

Mount both transports

Streamable HTTP replaced SSE in the spec. It uses a single endpoint, and a server whose tools carry no session state can run stateless on a plain Worker with no Durable Object at all.

Client support still lags the spec, though, and it lags unevenly across editors and versions. Serving /sse and /mcp side by side costs one line and saves you from debugging someone else’s client.

If you take one thing from this: decide early whether your tools need session state. That answer, not the transport, is what determines the architecture.