Skip to content

Commit f6422b2

Browse files
committed
fix: bootstrapping bug with streamable http transport
1 parent f34b844 commit f6422b2

File tree

7 files changed

+81
-67
lines changed

7 files changed

+81
-67
lines changed

examples/email-mcp/src/index.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,21 @@ export default {
1111
_ctx: ExecutionContext,
1212
): Promise<Response> {
1313
const url = new URL(request.url);
14-
let sessionIdStr = url.searchParams.get("sessionId");
1514

16-
const id = sessionIdStr
17-
? env.EMAIL_MCP_SERVER.idFromString(sessionIdStr)
18-
: env.EMAIL_MCP_SERVER.idFromName("default-email-session");
15+
// Dynamically generate sessionId if it isn't provided to allocate a session
16+
const sessionId =
17+
request.headers.get("mcp-session-id") ?? crypto.randomUUID();
1918

20-
url.searchParams.set("sessionId", id.toString());
19+
const reqClone = request.clone();
20+
21+
const json = await request.json();
22+
23+
console.log("Request:", { headers: request.headers, json });
24+
25+
const id = env.EMAIL_MCP_SERVER.idFromName(sessionId);
2126

2227
return env.EMAIL_MCP_SERVER.get(id).fetch(
23-
new Request(url.toString(), request.clone()),
28+
new Request(url.toString(), reqClone),
2429
);
2530
},
2631
};

examples/email-mcp/test/email-mcp-client.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ describe("Email MCP Client Integration Tests", () => {
6969

7070
const transport = createTransport(ctx);
7171
await client.connect(transport);
72+
console.log("Connected to transport");
7273

7374
await waitOnExecutionContext(ctx);
7475
console.log(`Client connection test passed!`);

examples/email-mcp/wrangler.jsonc

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,29 @@
11
{
2-
"$schema": "https://raw.githubusercontent.com/cloudflare/workers-sdk/main/packages/wrangler/config-schema.json",
2+
"$schema": "./node_modules/wrangler/config-schema.json",
33
"name": "email-mcp",
44
"main": "src/index.ts",
55
"compatibility_date": "2025-03-10",
66
"account_id": "7541b433fbcea05edcdd0b86dbdd41fc",
7+
"compatibility_flags": ["nodejs_compat"],
78

89
"durable_objects": {
9-
"bindings": [
10-
{ "name": "EMAIL_MCP_SERVER", "class_name": "EmailMcpServer" }
11-
]
10+
"bindings": [{ "name": "EMAIL_MCP_SERVER", "class_name": "EmailMcpServer" }]
1211
},
1312

1413
// Register the Durable Object class with SQLite support
15-
"migrations": [
16-
{ "tag": "v1", "new_sqlite_classes": ["EmailMcpServer"] }
17-
],
14+
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["EmailMcpServer"] }],
1815

1916
// Cloudflare Email Workers "Send Email" binding
20-
"send_email": [
21-
{ "name": "SEND_EMAIL" }
22-
],
17+
"send_email": [{ "name": "SEND_EMAIL" }],
2318

2419
"vars": {
2520
"MAIL_FROM": "no-reply@nullshot.ai",
2621
"ALLOWED_RECIPIENTS": "raymond@xavalabs.com,@nullshot.ai"
2722
},
2823

29-
// logs/traces
24+
// logs/traces
3025
"observability": {
3126
"enabled": true
3227
}
33-
}
28+
}
29+

packages/mcp/AGENTS.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# AGENTS.md - MCP Package
2+
3+
## Build/Run Commands
4+
- Build: `pnpm run build` (uses tsc)
5+
- Dev mode: `pnpm run dev` (wrangler dev)
6+
- Run tests: `pnpm run test` (vitest)
7+
- Run single test: `pnpm run test src/mcp/sse-transport.test.ts`
8+
- Typecheck: `pnpm run type-check` (tsc --noEmit)
9+
- Deploy: `pnpm run deploy` (wrangler deploy)
10+
11+
## Code Style Guidelines
12+
- Use double quotes for strings, no semicolons
13+
- Use tabs for indentation, strict TypeScript
14+
- No unused locals/parameters, exact optional property types
15+
- Use ES2021 target and ES2022 modules for Cloudflare Workers
16+
- Import paths use relative paths for local imports
17+
18+
## Naming Conventions
19+
- Files: kebab-case, Types: PascalCase
20+
- Functions/variables: camelCase, Constants: UPPER_SNAKE_CASE
21+
- Test files: *.test.ts, Tools/Resources: snake_case
22+
23+
## Error Handling & Testing
24+
- Use try/catch for async operations with meaningful messages
25+
- Use vitest with Cloudflare Workers pool, tests alongside source files
26+
- Tests run with isolated storage disabled for Durable Objects
27+
28+
## Development Practices
29+
- Follow MCP patterns from ../../.cursor/rules/mcp-development.mdc
30+
- Extend McpHonoServerDO, use Zod schemas with .describe()
31+
- Always call super.setupRoutes() when overriding setupRoutes()

packages/mcp/src/mcp/hono-server.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Hono } from 'hono';
22
import { McpServerDO, SSE_MESSAGE_ENDPOINT, WEBSOCKET_ENDPOINT, MCP_SUBPROTOCOL } from './server';
3+
import { cors } from 'hono/cors';
34

45
// Support both Cloudflare and Hono environments
56
export abstract class McpHonoServerDO<Env extends Record<string, any> = Record<string, any>> extends McpServerDO<Env> {
@@ -8,6 +9,13 @@ export abstract class McpHonoServerDO<Env extends Record<string, any> = Record<s
89
public constructor(ctx: DurableObjectState, env: Env) {
910
super(ctx, env);
1011
this.app = new Hono<{ Bindings: Env }>();
12+
this.app.use(
13+
cors({
14+
origin: '*',
15+
exposeHeaders: ['mcp-session-id', 'mcp-protocol-version'],
16+
allowHeaders: ['Content-Type', 'mcp-session-id', 'mcp-protocol-version'],
17+
}),
18+
);
1119
this.setupRoutes(this.app);
1220
}
1321

packages/mcp/src/mcp/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ interface WebSocketAttachment {
2121

2222
/**
2323
* McpDurableServer is a Durable Object implementation of an MCP server.
24-
* It supports SSE connections for event streaming and WebSocket connections with hibernation.
24+
* It supports all transport connections for event streaming and WebSocket connections with hibernation.
2525
*/
2626
export abstract class McpServerDO<Env = unknown> extends DurableObject<Env> {
2727
private server: IMcpServer;

packages/test-utils/src/mcp/WorkerStreamableHTTPClientTransport.ts

Lines changed: 21 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ export class WorkerStreamableHTTPClientTransport extends StreamableHTTPClientTra
1616
fetchUrl: RequestInfo | URL,
1717
fetchInit: RequestInit = {},
1818
) => {
19-
console.log(`[Debug] Fetching from: ${fetchUrl}`);
19+
console.log(
20+
`[Debug] Fetching from: ${fetchUrl}`,
21+
JSON.stringify(fetchInit, null, 2),
22+
);
2023
// add auth headers
2124
const workerOptions = {
2225
...fetchInit,
2326
headers: {
24-
...fetchInit?.headers,
27+
...fetchInit.headers,
2528
"Content-Type": "application/json",
2629
Accept: "application/json, text/event-stream",
2730
},
@@ -32,58 +35,28 @@ export class WorkerStreamableHTTPClientTransport extends StreamableHTTPClientTra
3235
const request = new Request(fetchUrl.toString(), workerOptions);
3336

3437
// Pass the Request object to the worker.fetch method
35-
return await SELF.fetch(request);
38+
const response = await SELF.fetch(request);
39+
const resClone = response.clone();
40+
console.log("Response:", {
41+
headers: response.headers,
42+
json: await response.json(),
43+
});
44+
return resClone;
3645
};
3746

3847
// Initialize the parent StreamableHTTPClientTransport with our custom fetch
3948
super(url, { fetch: fetchOverride });
4049
this.ctx = ctx;
4150
}
4251

43-
/**
44-
* Override the send method to direct requests to our worker
45-
*/
46-
async send(message: JSONRPCMessage): Promise<void> {
47-
console.log(
48-
`[Debug] Sending message to worker: ${JSON.stringify(message)}`,
49-
);
50-
// Call the internal method to get the endpoint
51-
// @ts-ignore
52-
const endpoint = this._url;
53-
54-
if (!endpoint) {
55-
throw new Error("Not connected");
56-
}
57-
58-
try {
59-
// Set up headers - we would normally get these from _commonHeaders
60-
// but we can't access it due to it being private
61-
const headers = new Headers();
62-
headers.set("content-type", "application/json");
63-
headers.set("accept", "application/json, text/event-stream");
64-
65-
const init = {
66-
method: "POST",
67-
headers,
68-
body: JSON.stringify(message),
69-
};
70-
71-
console.log(`Sending message to worker: ${JSON.stringify(message)}`);
72-
73-
// Use our worker fetch instead of regular fetch
74-
const request = new Request(endpoint.toString(), init);
75-
76-
const response = await SELF.fetch(request);
77-
78-
if (!response.ok) {
79-
const text = await response.text().catch(() => null);
80-
throw new Error(
81-
`Error POSTing to endpoint (HTTP ${response.status}): ${text}`,
82-
);
83-
}
84-
} catch (error) {
85-
this.onerror?.(error as Error);
86-
throw error;
87-
}
52+
async send(
53+
message: JSONRPCMessage | JSONRPCMessage[],
54+
options?: {
55+
resumptionToken?: string;
56+
onresumptiontoken?: (token: string) => void;
57+
},
58+
): Promise<void> {
59+
console.log("Session:", { sessionId: this.sessionId });
60+
await super.send(message, options);
8861
}
8962
}

0 commit comments

Comments
 (0)