@@ -1270,16 +1270,21 @@ describe('StreamableHTTPServerTransport with resumability', () => {
12701270 let baseUrl : URL ;
12711271 let sessionId : string ;
12721272 let mcpServer : McpServer ;
1273- const storedEvents : Map < string , { eventId : string ; message : JSONRPCMessage } > = new Map ( ) ;
1273+ const storedEvents : Map < string , { eventId : string ; message : JSONRPCMessage ; streamId : string } > = new Map ( ) ;
12741274
12751275 // Simple implementation of EventStore
12761276 const eventStore : EventStore = {
12771277 async storeEvent ( streamId : string , message : JSONRPCMessage ) : Promise < string > {
12781278 const eventId = `${ streamId } _${ randomUUID ( ) } ` ;
1279- storedEvents . set ( eventId , { eventId, message } ) ;
1279+ storedEvents . set ( eventId , { eventId, message, streamId } ) ;
12801280 return eventId ;
12811281 } ,
12821282
1283+ async getStreamIdForEventId ( eventId : string ) : Promise < string | undefined > {
1284+ const event = storedEvents . get ( eventId ) ;
1285+ return event ?. streamId ;
1286+ } ,
1287+
12831288 async replayEventsAfter (
12841289 lastEventId : EventId ,
12851290 {
@@ -1288,11 +1293,11 @@ describe('StreamableHTTPServerTransport with resumability', () => {
12881293 send : ( eventId : EventId , message : JSONRPCMessage ) => Promise < void > ;
12891294 }
12901295 ) : Promise < StreamId > {
1291- const streamId = lastEventId . split ( '_' ) [ 0 ] ;
1292- // Extract stream ID from the event ID
1296+ const event = storedEvents . get ( lastEventId ) ;
1297+ const streamId = event ?. streamId || lastEventId . split ( '_' ) [ 0 ] ;
12931298 // For test simplicity, just return all events with matching streamId that aren't the lastEventId
1294- for ( const [ eventId , { message } ] of storedEvents . entries ( ) ) {
1295- if ( eventId . startsWith ( streamId ) && eventId !== lastEventId ) {
1299+ for ( const [ eventId , { message, streamId : evtStreamId } ] of storedEvents . entries ( ) ) {
1300+ if ( evtStreamId === streamId && eventId !== lastEventId ) {
12961301 await send ( eventId , message ) ;
12971302 }
12981303 }
@@ -1405,6 +1410,8 @@ describe('StreamableHTTPServerTransport with resumability', () => {
14051410
14061411 // Close the first SSE stream to simulate a disconnect
14071412 await reader ! . cancel ( ) ;
1413+ // Give the close handler time to clean up the stream mapping
1414+ await new Promise ( resolve => setTimeout ( resolve , 10 ) ) ;
14081415
14091416 // Reconnect with the Last-Event-ID to get missed messages
14101417 const reconnectResponse = await fetch ( baseUrl , {
@@ -1535,11 +1542,16 @@ describe('StreamableHTTPServerTransport POST SSE priming events', () => {
15351542 storedEvents . set ( eventId , { eventId, message, streamId } ) ;
15361543 return eventId ;
15371544 } ,
1545+ async getStreamIdForEventId ( eventId : string ) : Promise < string | undefined > {
1546+ const event = storedEvents . get ( eventId ) ;
1547+ return event ?. streamId ;
1548+ } ,
15381549 async replayEventsAfter (
15391550 lastEventId : EventId ,
15401551 { send } : { send : ( eventId : EventId , message : JSONRPCMessage ) => Promise < void > }
15411552 ) : Promise < StreamId > {
1542- const streamId = lastEventId . split ( '::' ) [ 0 ] ;
1553+ const event = storedEvents . get ( lastEventId ) ;
1554+ const streamId = event ?. streamId || lastEventId . split ( '::' ) [ 0 ] ;
15431555 const eventsToReplay : Array < [ string , { message : JSONRPCMessage } ] > = [ ] ;
15441556 for ( const [ eventId , data ] of storedEvents . entries ( ) ) {
15451557 if ( data . streamId === streamId && eventId > lastEventId ) {
@@ -1965,6 +1977,159 @@ describe('StreamableHTTPServerTransport POST SSE priming events', () => {
19651977 expect ( eventIds ) . toBeTruthy ( ) ;
19661978 expect ( eventIds ! . length ) . toBeGreaterThanOrEqual ( 3 ) ;
19671979 } ) ;
1980+
1981+ it ( 'should allow resuming multiple POST streams via separate GET streams' , async ( ) => {
1982+ const result = await createTestServer ( {
1983+ sessionIdGenerator : ( ) => randomUUID ( ) ,
1984+ eventStore : createEventStore ( ) ,
1985+ retryInterval : 1000
1986+ } ) ;
1987+ server = result . server ;
1988+ transport = result . transport ;
1989+ baseUrl = result . baseUrl ;
1990+ mcpServer = result . mcpServer ;
1991+
1992+ // Track tool execution state for two separate tools
1993+ let tool1Resolve : ( ) => void ;
1994+ let tool2Resolve : ( ) => void ;
1995+ const tool1Promise = new Promise < void > ( resolve => {
1996+ tool1Resolve = resolve ;
1997+ } ) ;
1998+ const tool2Promise = new Promise < void > ( resolve => {
1999+ tool2Resolve = resolve ;
2000+ } ) ;
2001+
2002+ // Register two tools
2003+ mcpServer . tool ( 'stream-tool-1' , 'First stream tool' , { } , async ( ) => {
2004+ await tool1Promise ;
2005+ return { content : [ { type : 'text' , text : 'Result from stream 1' } ] } ;
2006+ } ) ;
2007+ mcpServer . tool ( 'stream-tool-2' , 'Second stream tool' , { } , async ( ) => {
2008+ await tool2Promise ;
2009+ return { content : [ { type : 'text' , text : 'Result from stream 2' } ] } ;
2010+ } ) ;
2011+
2012+ // Initialize to get session ID
2013+ const initResponse = await sendPostRequest ( baseUrl , TEST_MESSAGES . initialize ) ;
2014+ sessionId = initResponse . headers . get ( 'mcp-session-id' ) as string ;
2015+ expect ( sessionId ) . toBeDefined ( ) ;
2016+
2017+ // POST tool call #1
2018+ const toolCall1 : JSONRPCMessage = {
2019+ jsonrpc : '2.0' ,
2020+ id : 301 ,
2021+ method : 'tools/call' ,
2022+ params : { name : 'stream-tool-1' , arguments : { } }
2023+ } ;
2024+ const post1Response = await fetch ( baseUrl , {
2025+ method : 'POST' ,
2026+ headers : {
2027+ 'Content-Type' : 'application/json' ,
2028+ Accept : 'text/event-stream, application/json' ,
2029+ 'mcp-session-id' : sessionId ,
2030+ 'mcp-protocol-version' : '2025-03-26'
2031+ } ,
2032+ body : JSON . stringify ( toolCall1 )
2033+ } ) ;
2034+ expect ( post1Response . status ) . toBe ( 200 ) ;
2035+
2036+ // Read priming event and extract event ID for stream 1
2037+ const reader1 = post1Response . body ?. getReader ( ) ;
2038+ const { value : priming1 } = await reader1 ! . read ( ) ;
2039+ const priming1Text = new TextDecoder ( ) . decode ( priming1 ) ;
2040+ const priming1Match = priming1Text . match ( / i d : ( [ ^ \n ] + ) / ) ;
2041+ expect ( priming1Match ) . toBeTruthy ( ) ;
2042+ const eventId1 = priming1Match ! [ 1 ] ;
2043+
2044+ // POST tool call #2
2045+ const toolCall2 : JSONRPCMessage = {
2046+ jsonrpc : '2.0' ,
2047+ id : 302 ,
2048+ method : 'tools/call' ,
2049+ params : { name : 'stream-tool-2' , arguments : { } }
2050+ } ;
2051+ const post2Response = await fetch ( baseUrl , {
2052+ method : 'POST' ,
2053+ headers : {
2054+ 'Content-Type' : 'application/json' ,
2055+ Accept : 'text/event-stream, application/json' ,
2056+ 'mcp-session-id' : sessionId ,
2057+ 'mcp-protocol-version' : '2025-03-26'
2058+ } ,
2059+ body : JSON . stringify ( toolCall2 )
2060+ } ) ;
2061+ expect ( post2Response . status ) . toBe ( 200 ) ;
2062+
2063+ // Read priming event and extract event ID for stream 2
2064+ const reader2 = post2Response . body ?. getReader ( ) ;
2065+ const { value : priming2 } = await reader2 ! . read ( ) ;
2066+ const priming2Text = new TextDecoder ( ) . decode ( priming2 ) ;
2067+ const priming2Match = priming2Text . match ( / i d : ( [ ^ \n ] + ) / ) ;
2068+ expect ( priming2Match ) . toBeTruthy ( ) ;
2069+ const eventId2 = priming2Match ! [ 1 ] ;
2070+
2071+ // Verify we have two different stream IDs
2072+ const streamId1 = eventId1 . split ( '::' ) [ 0 ] ;
2073+ const streamId2 = eventId2 . split ( '::' ) [ 0 ] ;
2074+ expect ( streamId1 ) . not . toBe ( streamId2 ) ;
2075+
2076+ // Close both streams
2077+ transport . closeSSEStream ( 301 ) ;
2078+ transport . closeSSEStream ( 302 ) ;
2079+
2080+ // Verify both streams are closed
2081+ const { done : done1 } = await reader1 ! . read ( ) ;
2082+ const { done : done2 } = await reader2 ! . read ( ) ;
2083+ expect ( done1 ) . toBe ( true ) ;
2084+ expect ( done2 ) . toBe ( true ) ;
2085+
2086+ // Complete both tools while disconnected
2087+ tool1Resolve ! ( ) ;
2088+ tool2Resolve ! ( ) ;
2089+ await new Promise ( resolve => setTimeout ( resolve , 50 ) ) ;
2090+
2091+ // Resume BOTH streams via GET - they should work concurrently (no 409)
2092+ const [ reconnect1Response , reconnect2Response ] = await Promise . all ( [
2093+ fetch ( baseUrl , {
2094+ method : 'GET' ,
2095+ headers : {
2096+ Accept : 'text/event-stream' ,
2097+ 'mcp-session-id' : sessionId ,
2098+ 'mcp-protocol-version' : '2025-03-26' ,
2099+ 'last-event-id' : eventId1
2100+ }
2101+ } ) ,
2102+ fetch ( baseUrl , {
2103+ method : 'GET' ,
2104+ headers : {
2105+ Accept : 'text/event-stream' ,
2106+ 'mcp-session-id' : sessionId ,
2107+ 'mcp-protocol-version' : '2025-03-26' ,
2108+ 'last-event-id' : eventId2
2109+ }
2110+ } )
2111+ ] ) ;
2112+
2113+ // Both should succeed (not 409)
2114+ expect ( reconnect1Response . status ) . toBe ( 200 ) ;
2115+ expect ( reconnect2Response . status ) . toBe ( 200 ) ;
2116+
2117+ // Read results from both streams
2118+ const reconnect1Reader = reconnect1Response . body ?. getReader ( ) ;
2119+ const { value : replay1 } = await reconnect1Reader ! . read ( ) ;
2120+ const replay1Text = new TextDecoder ( ) . decode ( replay1 ) ;
2121+
2122+ const reconnect2Reader = reconnect2Response . body ?. getReader ( ) ;
2123+ const { value : replay2 } = await reconnect2Reader ! . read ( ) ;
2124+ const replay2Text = new TextDecoder ( ) . decode ( replay2 ) ;
2125+
2126+ // Each stream should have its own result
2127+ expect ( replay1Text ) . toContain ( 'Result from stream 1' ) ;
2128+ expect ( replay1Text ) . toContain ( '"id":301' ) ;
2129+
2130+ expect ( replay2Text ) . toContain ( 'Result from stream 2' ) ;
2131+ expect ( replay2Text ) . toContain ( '"id":302' ) ;
2132+ } ) ;
19682133} ) ;
19692134
19702135// Test onsessionclosed callback
0 commit comments