@@ -18,10 +18,14 @@ import {
1818 registerSessionResource ,
1919} from '../resources/session.js' ;
2020import { registerFileResources } from '../resources/files.js' ;
21+ import {
22+ SubscribeRequestSchema ,
23+ } from '@modelcontextprotocol/sdk/types.js' ;
2124import {
2225 setSubscriptionHandlers ,
2326 beginSimulatedResourceUpdates ,
2427 stopSimulatedResourceUpdates ,
28+ removeSubscriber ,
2529} from '../resources/subscriptions.js' ;
2630
2731describe ( 'Resource Templates' , ( ) => {
@@ -298,10 +302,87 @@ describe('Subscriptions', () => {
298302 } ) ;
299303 } ) ;
300304
305+ describe ( 'removeSubscriber' , ( ) => {
306+ const testUri = 'demo://resource/dynamic/text/1' ;
307+ const sessionId = 'disconnect-test-session' ;
308+
309+ let subscribeHandler : (
310+ request : { params : { uri : string } } ,
311+ extra : { sessionId : string }
312+ ) => Promise < unknown > ;
313+
314+ beforeEach ( ( ) => {
315+ const handlers = new Map < unknown , typeof subscribeHandler > ( ) ;
316+ const mockServer = {
317+ server : {
318+ setRequestHandler : vi . fn ( ( schema , handler ) => {
319+ handlers . set ( schema , handler ) ;
320+ } ) ,
321+ notification : vi . fn ( ) ,
322+ } ,
323+ sendLoggingMessage : vi . fn ( ) ,
324+ } as unknown as McpServer ;
325+
326+ setSubscriptionHandlers ( mockServer ) ;
327+ subscribeHandler = handlers . get ( SubscribeRequestSchema ) ! ;
328+ } ) ;
329+
330+ afterEach ( ( ) => {
331+ stopSimulatedResourceUpdates ( sessionId ) ;
332+ removeSubscriber ( sessionId ) ;
333+ } ) ;
334+
335+ it ( 'should drop a disconnected session from all subscriptions' , async ( ) => {
336+ const notification = vi . fn ( ) ;
337+ const mockServer = {
338+ server : {
339+ notification,
340+ } ,
341+ } as unknown as McpServer ;
342+
343+ await subscribeHandler ( { params : { uri : testUri } } , { sessionId } ) ;
344+
345+ beginSimulatedResourceUpdates ( mockServer , sessionId ) ;
346+ expect ( notification ) . toHaveBeenCalled ( ) ;
347+
348+ notification . mockClear ( ) ;
349+ removeSubscriber ( sessionId ) ;
350+ stopSimulatedResourceUpdates ( sessionId ) ;
351+
352+ beginSimulatedResourceUpdates ( mockServer , sessionId ) ;
353+ expect ( notification ) . not . toHaveBeenCalled ( ) ;
354+ } ) ;
355+
356+ it ( 'should not affect other sessions subscribed to the same URI' , async ( ) => {
357+ const otherSessionId = 'other-session' ;
358+ const notification = vi . fn ( ) ;
359+ const mockServer = {
360+ server : {
361+ notification,
362+ } ,
363+ } as unknown as McpServer ;
364+
365+ await subscribeHandler ( { params : { uri : testUri } } , { sessionId } ) ;
366+ await subscribeHandler (
367+ { params : { uri : testUri } } ,
368+ { sessionId : otherSessionId }
369+ ) ;
370+
371+ removeSubscriber ( sessionId ) ;
372+
373+ beginSimulatedResourceUpdates ( mockServer , otherSessionId ) ;
374+ expect ( notification ) . toHaveBeenCalled ( ) ;
375+
376+ stopSimulatedResourceUpdates ( otherSessionId ) ;
377+ removeSubscriber ( otherSessionId ) ;
378+ } ) ;
379+ } ) ;
380+
301381 describe ( 'simulated resource updates lifecycle' , ( ) => {
302382 afterEach ( ( ) => {
303383 // Clean up any intervals
304384 stopSimulatedResourceUpdates ( 'lifecycle-test-session' ) ;
385+ removeSubscriber ( 'lifecycle-test-session' ) ;
305386 } ) ;
306387
307388 it ( 'should start and stop updates without errors' , ( ) => {
0 commit comments