@@ -1954,6 +1954,99 @@ describe('Zod v4', () => {
19541954 toolResolve ! ( ) ;
19551955 } ) ;
19561956
1957+ it ( 'should replay terminal response after request-scoped SSE stream is closed' , async ( ) => {
1958+ const result = await createTestServer ( {
1959+ sessionIdGenerator : ( ) => randomUUID ( ) ,
1960+ eventStore : createEventStore ( ) ,
1961+ retryInterval : 1000
1962+ } ) ;
1963+ server = result . server ;
1964+ transport = result . transport ;
1965+ baseUrl = result . baseUrl ;
1966+ mcpServer = result . mcpServer ;
1967+
1968+ let streamCloseCalled = false ;
1969+ let toolResolve : ( ) => void ;
1970+ const toolCompletePromise = new Promise < void > ( resolve => {
1971+ toolResolve = resolve ;
1972+ } ) ;
1973+
1974+ mcpServer . registerTool ( 'close-and-finish-tool' , { description : 'Closes and then finishes' } , async ctx => {
1975+ ctx . http ?. closeSSE ?.( ) ;
1976+ streamCloseCalled = true ;
1977+ await toolCompletePromise ;
1978+ return { content : [ { type : 'text' , text : 'finished after reconnect' } ] } ;
1979+ } ) ;
1980+
1981+ const initResponse = await sendPostRequest ( baseUrl , TEST_MESSAGES . initialize ) ;
1982+ sessionId = initResponse . headers . get ( 'mcp-session-id' ) as string ;
1983+ expect ( sessionId ) . toBeDefined ( ) ;
1984+
1985+ const toolCallRequest : JSONRPCMessage = {
1986+ jsonrpc : '2.0' ,
1987+ id : 101 ,
1988+ method : 'tools/call' ,
1989+ params : { name : 'close-and-finish-tool' , arguments : { } }
1990+ } ;
1991+
1992+ const postResponse = await fetch ( baseUrl , {
1993+ method : 'POST' ,
1994+ headers : {
1995+ 'Content-Type' : 'application/json' ,
1996+ Accept : 'text/event-stream, application/json' ,
1997+ 'mcp-session-id' : sessionId ,
1998+ 'mcp-protocol-version' : '2025-11-25'
1999+ } ,
2000+ body : JSON . stringify ( toolCallRequest )
2001+ } ) ;
2002+
2003+ expect ( postResponse . status ) . toBe ( 200 ) ;
2004+
2005+ const reader = postResponse . body ?. getReader ( ) ;
2006+ const priming = await reader ! . read ( ) ;
2007+ const primingText = new TextDecoder ( ) . decode ( priming . value ) ;
2008+ const idMatch = primingText . match ( / i d : ( [ ^ \n ] + ) / ) ;
2009+ expect ( idMatch ) . toBeTruthy ( ) ;
2010+ const lastEventId = idMatch ! [ 1 ] ! ;
2011+
2012+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
2013+ expect ( streamCloseCalled ) . toBe ( true ) ;
2014+
2015+ const { done } = await reader ! . read ( ) ;
2016+ expect ( done ) . toBe ( true ) ;
2017+
2018+ toolResolve ! ( ) ;
2019+ await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
2020+
2021+ const reconnectResponse = await fetch ( baseUrl , {
2022+ method : 'GET' ,
2023+ headers : {
2024+ Accept : 'text/event-stream' ,
2025+ 'mcp-session-id' : sessionId ,
2026+ 'mcp-protocol-version' : '2025-11-25' ,
2027+ 'last-event-id' : lastEventId
2028+ }
2029+ } ) ;
2030+
2031+ expect ( reconnectResponse . status ) . toBe ( 200 ) ;
2032+
2033+ const reconnectReader = reconnectResponse . body ?. getReader ( ) ;
2034+ let replayedText = '' ;
2035+ const timeout = setTimeout ( ( ) => reconnectReader ! . cancel ( ) , 2000 ) ;
2036+ try {
2037+ while ( ! replayedText . includes ( 'finished after reconnect' ) ) {
2038+ const { value, done } = await reconnectReader ! . read ( ) ;
2039+ if ( done ) break ;
2040+ replayedText += new TextDecoder ( ) . decode ( value ) ;
2041+ }
2042+ } finally {
2043+ clearTimeout ( timeout ) ;
2044+ }
2045+
2046+ expect ( replayedText ) . toContain ( 'finished after reconnect' ) ;
2047+ expect ( replayedText ) . toContain ( '"id":101' ) ;
2048+ } ) ;
2049+
19572050 it ( 'should provide closeSSEStream callback in ctx when eventStore is configured' , async ( ) => {
19582051 const result = await createTestServer ( {
19592052 sessionIdGenerator : ( ) => randomUUID ( ) ,
0 commit comments