Skip to content

Commit db5caf1

Browse files
authored
Merge pull request #77 from zackify/feature/context-routes
Fix MCP tool response hanging issue
2 parents 464f70f + 7228ca5 commit db5caf1

File tree

2 files changed

+77
-8
lines changed

2 files changed

+77
-8
lines changed

src/routes/mcp/mcp.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ const tools = (server: McpServer, methods: Methods) => {
189189
],
190190
};
191191
} catch (e) {
192-
console.error(e);
192+
console.error("MCP setContext tool error:", e);
193193
return {
194194
content: [
195195
{
@@ -353,10 +353,33 @@ export const mcpRoute = ({ methods }: { methods: Methods }) => {
353353

354354
console.log('Handling request with transport, sessionId:', transport.sessionId);
355355

356-
// Handle the request - pass parsedBody as third parameter
357-
await transport.handleRequest(reqAdapter as any, resAdapter as any, parsedBody);
356+
// Override the transport's send method temporarily to ensure responses are sent immediately
357+
const originalSend = transport.send;
358358

359-
console.log('Request handled');
359+
transport.send = async function(message: any, options?: any) {
360+
// Call the original send method
361+
const result = await originalSend.call(this, message, options);
362+
363+
// For responses (not requests or notifications), ensure the response is sent
364+
if (message && (message.result !== undefined || message.error !== undefined)) {
365+
// Use setImmediate to allow the SDK to finish its processing
366+
setImmediate(() => {
367+
resAdapter.ensureResponseSent();
368+
});
369+
}
370+
371+
return result;
372+
};
373+
374+
try {
375+
// Handle the request - pass parsedBody as third parameter
376+
await transport.handleRequest(reqAdapter as any, resAdapter as any, parsedBody);
377+
378+
console.log('Request handled');
379+
} finally {
380+
// Restore original send method
381+
transport.send = originalSend;
382+
}
360383
} catch (error) {
361384
console.error('Error handling MCP request:', error);
362385
const errorResponse = new Response(

src/routes/mcp/mcpToBun.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,31 @@ export class BunResponseAdapter extends EventEmitter {
6161

6262
write(chunk: any): boolean {
6363
this._body += chunk;
64+
65+
// Check if this is a complete SSE event with JSON-RPC response
66+
// The MCP SDK sends SSE events in the format: "event: message\ndata: {json}\n\n"
67+
if (this._body.includes('event: message\ndata: ') && this._body.endsWith('\n\n')) {
68+
// Extract the JSON part to check if it's complete
69+
const dataMatch = this._body.match(/data: (.+)\n\n$/);
70+
if (dataMatch && dataMatch[1]) {
71+
try {
72+
const json = JSON.parse(dataMatch[1]);
73+
// Check if this is a complete JSON-RPC response
74+
if (json.jsonrpc && json.id !== undefined && (json.result !== undefined || json.error !== undefined)) {
75+
// The MCP SDK has written a complete response but isn't ending it
76+
// This is a workaround for the SDK bug where it waits for all batch responses
77+
process.nextTick(() => {
78+
if (!this._ended) {
79+
this.end();
80+
}
81+
});
82+
}
83+
} catch (e) {
84+
// Not valid JSON yet, keep accumulating
85+
}
86+
}
87+
}
88+
6489
return true;
6590
}
6691

@@ -72,7 +97,6 @@ export class BunResponseAdapter extends EventEmitter {
7297
encodingOrCb?: BufferEncoding | (() => void),
7398
cb?: () => void
7499
): this {
75-
console.log('BunResponseAdapter.end() called');
76100
let chunk: any;
77101
let callback: (() => void) | undefined;
78102

@@ -93,11 +117,21 @@ export class BunResponseAdapter extends EventEmitter {
93117

94118
if (!this._ended) {
95119
this._ended = true;
96-
97-
console.log('Creating response with status:', this._statusCode, 'body length:', this._body.length);
120+
121+
// If this is an SSE response, extract the JSON data from the SSE format
122+
let responseBody = this._body;
123+
if (this._headers['Content-Type'] === 'text/event-stream' && this._body.includes('event: message\ndata: ')) {
124+
// Parse SSE format to extract JSON
125+
const match = this._body.match(/data: (.+?)(?:\n\n|$)/);
126+
if (match && match[1]) {
127+
responseBody = match[1];
128+
// Update content type to JSON since we're extracting the JSON data
129+
this._headers['Content-Type'] = 'application/json';
130+
}
131+
}
98132

99133
// Create the Bun Response
100-
const response = new Response(this._body, {
134+
const response = new Response(responseBody, {
101135
status: this._statusCode,
102136
headers: this._headers,
103137
});
@@ -116,6 +150,18 @@ export class BunResponseAdapter extends EventEmitter {
116150

117151
return this;
118152
}
153+
154+
// Add method to ensure response is sent
155+
ensureResponseSent(): void {
156+
if (!this._ended) {
157+
this.end();
158+
}
159+
}
160+
161+
// Add method to check body length
162+
getBodyLength(): number {
163+
return this._body.length;
164+
}
119165
}
120166

121167
export // Adapter to convert Bun Request to Node.js IncomingMessage-like object

0 commit comments

Comments
 (0)