Skip to content

Commit 3d4455c

Browse files
committed
add error handling for onprogress disconnects (stop server crashing)
1 parent e743740 commit 3d4455c

File tree

2 files changed

+63
-19
lines changed

2 files changed

+63
-19
lines changed

src/examples/server/disconnectTestServer.ts

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,68 @@ import { CallToolResult, isInitializeRequest } from '../../types.js';
66
import { randomUUID } from 'node:crypto';
77
import * as z from 'zod/v4';
88

9+
// Usage: npx tsx disconnectTestServer.ts [--abort]
10+
const useAbort = process.argv.includes('--abort');
11+
console.log(`Abort on disconnect: ${useAbort ? 'enabled' : 'disabled'}`);
12+
913
const server = new McpServer(
1014
{ name: 'disconnect-test', version: '1.0.0' },
1115
{ capabilities: { logging: {} } }
1216
);
1317

14-
server.tool('slow-task', 'Task with progress notifications', { steps: z.number() },
18+
server.server.onerror = err => console.log('[onerror]', err.message);
19+
20+
server.registerTool(
21+
'slow-task',
22+
{
23+
description: 'Task with progress notifications',
24+
inputSchema: { steps: z.number() }
25+
},
1526
async ({ steps }, extra): Promise<CallToolResult> => {
16-
for (let i = 1; i <= steps; i++) {
17-
console.log(`Sending notification ${i}/${steps}`);
27+
// SIMULATING A PROXY: create abort controller for "upstream" request
28+
const abortController = new AbortController();
29+
if (extra.sessionId) {
30+
sessionAbortControllers[extra.sessionId] = abortController;
31+
}
1832

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-
}
33+
try {
34+
for (let i = 1; i <= steps; i++) {
35+
// Check if aborted before each step
36+
if (abortController.signal.aborted) {
37+
console.log('Upstream request aborted - stopping work');
38+
break;
39+
}
3040

31-
await new Promise(r => setTimeout(r, 1000));
41+
console.log(`Sending notification ${i}/${steps}`);
42+
43+
// SIMULATING A PROXY RELAY: onprogress forwards with same progress token
44+
const progressToken = extra._meta?.progressToken;
45+
if (progressToken !== undefined) {
46+
server.server.notification(
47+
{
48+
method: 'notifications/progress',
49+
params: { progressToken, progress: i, total: steps }
50+
},
51+
{ relatedRequestId: extra.requestId }
52+
);
53+
}
54+
55+
await new Promise(r => setTimeout(r, 1000));
56+
}
57+
return { content: [{ type: 'text', text: 'SUCCESS' }] };
58+
} finally {
59+
// Cleanup abort controller
60+
if (extra.sessionId) {
61+
delete sessionAbortControllers[extra.sessionId];
62+
}
3263
}
33-
return { content: [{ type: 'text', text: 'SUCCESS' }] };
3464
}
3565
);
3666

3767
const app = createMcpExpressApp();
3868
const transports: Record<string, StreamableHTTPServerTransport> = {};
69+
// SIMULATING A PROXY: track abort controllers for upstream requests per session
70+
const sessionAbortControllers: Record<string, AbortController> = {};
3971

4072
app.post('/mcp', async (req: Request, res: Response) => {
4173
const sessionId = req.headers['mcp-session-id'] as string | undefined;
@@ -62,7 +94,16 @@ app.post('/mcp', async (req: Request, res: Response) => {
6294
res.on('finish', () => { finished = true; });
6395
res.on('close', () => {
6496
if (!finished) {
65-
console.log('Client disconnected - closing transport to reclaim resources');
97+
console.log('Client disconnected unexpectedly');
98+
if (useAbort) {
99+
// Abort any in-flight upstream requests for this session
100+
const abortController = sessionAbortControllers[transport!.sessionId!];
101+
if (abortController) {
102+
console.log('Aborting upstream request');
103+
abortController.abort();
104+
delete sessionAbortControllers[transport!.sessionId!];
105+
}
106+
}
66107
transport!.close();
67108
}
68109
});

src/shared/protocol.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,9 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
854854
}
855855
}
856856

857-
handler(params);
857+
Promise.resolve()
858+
.then(() => handler(params))
859+
.catch(error => this._onerror(new Error(`Uncaught error in progress handler: ${error}`)));
858860
}
859861

860862
private _onresponse(response: JSONRPCResponse | JSONRPCErrorResponse): void {
@@ -1277,7 +1279,8 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
12771279
*/
12781280
async notification(notification: SendNotificationT, options?: NotificationOptions): Promise<void> {
12791281
if (!this._transport) {
1280-
throw new Error('Not connected');
1282+
this._onerror(new Error('Not connected'));
1283+
return;
12811284
}
12821285

12831286
this.assertNotificationCapability(notification.method);

0 commit comments

Comments
 (0)