Skip to content

Commit e743740

Browse files
committed
proxy crash with no abort controller
1 parent 06a4fd2 commit e743740

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Client } from '../../client/index.js';
2+
import { StreamableHTTPClientTransport } from '../../client/streamableHttp.js';
3+
import { CallToolResultSchema } from '../../types.js';
4+
5+
const client = new Client({ name: 'disconnect-test-client', version: '1.0.0' });
6+
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'));
7+
8+
let progressCount = 0;
9+
10+
client.onerror = e => console.error('Client error:', e);
11+
12+
(async () => {
13+
await client.connect(transport);
14+
console.log('Connected, calling slow-task with steps=10...');
15+
16+
try {
17+
const result = await client.request(
18+
{ method: 'tools/call', params: { name: 'slow-task', arguments: { steps: 10 } } },
19+
CallToolResultSchema,
20+
{
21+
onprogress: progress => {
22+
console.log(`Progress ${++progressCount}: ${progress.progress}/${progress.total}`);
23+
if (progressCount === 5) {
24+
console.log('Abruptly killing process after 5 progress updates...');
25+
process.exit(1);
26+
}
27+
}
28+
}
29+
);
30+
console.log('Result:', result);
31+
} catch (e) {
32+
console.log('Request aborted (expected):', (e as Error).message);
33+
}
34+
})();
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Request, Response } from 'express';
2+
import { McpServer } from '../../server/mcp.js';
3+
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
4+
import { createMcpExpressApp } from '../../server/express.js';
5+
import { CallToolResult, isInitializeRequest } from '../../types.js';
6+
import { randomUUID } from 'node:crypto';
7+
import * as z from 'zod/v4';
8+
9+
const server = new McpServer(
10+
{ name: 'disconnect-test', version: '1.0.0' },
11+
{ capabilities: { logging: {} } }
12+
);
13+
14+
server.tool('slow-task', 'Task with progress notifications', { steps: z.number() },
15+
async ({ steps }, extra): Promise<CallToolResult> => {
16+
for (let i = 1; i <= steps; i++) {
17+
console.log(`Sending notification ${i}/${steps}`);
18+
19+
// SIMULATING A PROXY RELAY: onprogress forwards with same progress token
20+
const progressToken = extra._meta?.progressToken;
21+
if (progressToken !== undefined) {
22+
server.server.notification(
23+
{
24+
method: 'notifications/progress',
25+
params: { progressToken, progress: i, total: steps }
26+
},
27+
{ relatedRequestId: extra.requestId }
28+
);
29+
}
30+
31+
await new Promise(r => setTimeout(r, 1000));
32+
}
33+
return { content: [{ type: 'text', text: 'SUCCESS' }] };
34+
}
35+
);
36+
37+
const app = createMcpExpressApp();
38+
const transports: Record<string, StreamableHTTPServerTransport> = {};
39+
40+
app.post('/mcp', async (req: Request, res: Response) => {
41+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
42+
let transport = sessionId ? transports[sessionId] : undefined;
43+
44+
if (!transport && isInitializeRequest(req.body)) {
45+
transport = new StreamableHTTPServerTransport({
46+
sessionIdGenerator: () => randomUUID(),
47+
onsessioninitialized: id => {
48+
console.log(`Session initialized: ${id}`);
49+
transports[id] = transport!;
50+
}
51+
});
52+
transport.onclose = () => {
53+
console.log(`Transport closed for session: ${transport!.sessionId}`);
54+
delete transports[transport!.sessionId!];
55+
};
56+
await server.connect(transport);
57+
}
58+
59+
if (transport) {
60+
// Track if response finished normally
61+
let finished = false;
62+
res.on('finish', () => { finished = true; });
63+
res.on('close', () => {
64+
if (!finished) {
65+
console.log('Client disconnected - closing transport to reclaim resources');
66+
transport!.close();
67+
}
68+
});
69+
await transport.handleRequest(req, res, req.body);
70+
} else {
71+
res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad request' }, id: null });
72+
}
73+
});
74+
75+
// Return 405 for GET - we don't support standalone SSE stream
76+
app.get('/mcp', (_req, res) => res.status(405).send('Method not allowed'));
77+
78+
app.listen(3000, () => console.log('Disconnect test server listening on :3000'));

0 commit comments

Comments
 (0)