Skip to content

Commit e2c8198

Browse files
committed
Route /mcp through a Durable Object for session persistence
Effect's McpServer holds sessions in memory; Workers are stateless across requests, so initialize and tools/call landed in different isolates (404). Route all /mcp traffic to one singleton Durable Object so the session map persists across requests — the CF-native way to run MCP-over-HTTP.
1 parent 49b2b9b commit e2c8198

3 files changed

Lines changed: 37 additions & 3 deletions

File tree

‎worker/index.ts‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
import { detect } from "../src/lib/detect.ts";
2-
import { mcpHandler } from "./registry.ts";
2+
3+
export { McpDurableObject } from "./mcp-do.ts";
4+
5+
interface DurableObjectStub {
6+
fetch: (request: Request) => Promise<Response>;
7+
}
8+
interface DurableObjectNamespace {
9+
idFromName: (name: string) => unknown;
10+
get: (id: unknown) => DurableObjectStub;
11+
}
312

413
export interface Env {
514
ASSETS: { fetch: (request: Request) => Promise<Response> };
615
POSTHOG_KEY: string;
16+
MCP: DurableObjectNamespace;
717
}
818

919
const json = (body: unknown, status = 200, headers: Record<string, string> = {}) =>
@@ -28,8 +38,10 @@ export default {
2838
const url = new URL(request.url);
2939

3040
// MCP server (write-once tool registry) — point Claude/Cursor at /mcp.
41+
// Routed through a single Durable Object so the session map persists.
3142
if (url.pathname === "/mcp") {
32-
return mcpHandler(request);
43+
const stub = env.MCP.get(env.MCP.idFromName("mcp"));
44+
return stub.fetch(request);
3345
}
3446

3547
// Detection endpoint: run the full agent-readiness battery for a domain.

‎worker/mcp-do.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Durable Object that hosts the MCP server.
3+
*
4+
* Effect's McpServer keeps client sessions in an in-memory Map (created on
5+
* `initialize`, required by `tools/call`). Plain Workers are stateless across
6+
* requests, so the session is lost between calls. A Durable Object is a single
7+
* persistent instance — routing all /mcp traffic to one DO keeps the session
8+
* map alive across requests, which is how MCP-over-HTTP works on Cloudflare.
9+
*/
10+
import { mcpHandler } from "./registry.ts";
11+
12+
export class McpDurableObject {
13+
constructor(_state: unknown, _env: unknown) {}
14+
15+
fetch(request: Request): Promise<Response> {
16+
return mcpHandler(request);
17+
}
18+
}

‎wrangler.jsonc‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,9 @@
1717
],
1818
"observability": {
1919
"enabled": true
20-
}
20+
},
21+
"durable_objects": {
22+
"bindings": [{ "name": "MCP", "class_name": "McpDurableObject" }]
23+
},
24+
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["McpDurableObject"] }]
2125
}

0 commit comments

Comments
 (0)