@@ -6,36 +6,68 @@ import { CallToolResult, isInitializeRequest } from '../../types.js';
66import { randomUUID } from 'node:crypto' ;
77import * 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+
913const 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
3767const app = createMcpExpressApp ( ) ;
3868const transports : Record < string , StreamableHTTPServerTransport > = { } ;
69+ // SIMULATING A PROXY: track abort controllers for upstream requests per session
70+ const sessionAbortControllers : Record < string , AbortController > = { } ;
3971
4072app . 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 } ) ;
0 commit comments