@@ -29,6 +29,7 @@ interface TestServerConfig {
2929 enableJsonResponse ?: boolean ;
3030 customRequestHandler ?: ( req : IncomingMessage , res : ServerResponse , parsedBody ?: unknown ) => Promise < void > ;
3131 eventStore ?: EventStore ;
32+ onsessionclosed ?: ( sessionId : string ) => void ;
3233}
3334
3435/**
@@ -57,7 +58,8 @@ async function createTestServer(config: TestServerConfig = { sessionIdGenerator:
5758 const transport = new StreamableHTTPServerTransport ( {
5859 sessionIdGenerator : config . sessionIdGenerator ,
5960 enableJsonResponse : config . enableJsonResponse ?? false ,
60- eventStore : config . eventStore
61+ eventStore : config . eventStore ,
62+ onsessionclosed : config . onsessionclosed
6163 } ) ;
6264
6365 await mcpServer . connect ( transport ) ;
@@ -111,7 +113,8 @@ async function createTestAuthServer(config: TestServerConfig = { sessionIdGenera
111113 const transport = new StreamableHTTPServerTransport ( {
112114 sessionIdGenerator : config . sessionIdGenerator ,
113115 enableJsonResponse : config . enableJsonResponse ?? false ,
114- eventStore : config . eventStore
116+ eventStore : config . eventStore ,
117+ onsessionclosed : config . onsessionclosed
115118 } ) ;
116119
117120 await mcpServer . connect ( transport ) ;
@@ -1504,6 +1507,165 @@ describe("StreamableHTTPServerTransport in stateless mode", () => {
15041507 } ) ;
15051508} ) ;
15061509
1510+ // Test onsessionclosed callback
1511+ describe ( "StreamableHTTPServerTransport onsessionclosed callback" , ( ) => {
1512+ it ( "should call onsessionclosed callback when session is closed via DELETE" , async ( ) => {
1513+ const mockCallback = jest . fn ( ) ;
1514+
1515+ // Create server with onsessionclosed callback
1516+ const result = await createTestServer ( {
1517+ sessionIdGenerator : ( ) => randomUUID ( ) ,
1518+ onsessionclosed : mockCallback ,
1519+ } ) ;
1520+
1521+ const tempServer = result . server ;
1522+ const tempUrl = result . baseUrl ;
1523+
1524+ // Initialize to get a session ID
1525+ const initResponse = await sendPostRequest ( tempUrl , TEST_MESSAGES . initialize ) ;
1526+ const tempSessionId = initResponse . headers . get ( "mcp-session-id" ) ;
1527+ expect ( tempSessionId ) . toBeDefined ( ) ;
1528+
1529+ // DELETE the session
1530+ const deleteResponse = await fetch ( tempUrl , {
1531+ method : "DELETE" ,
1532+ headers : {
1533+ "mcp-session-id" : tempSessionId || "" ,
1534+ "mcp-protocol-version" : "2025-03-26" ,
1535+ } ,
1536+ } ) ;
1537+
1538+ expect ( deleteResponse . status ) . toBe ( 200 ) ;
1539+ expect ( mockCallback ) . toHaveBeenCalledWith ( tempSessionId ) ;
1540+ expect ( mockCallback ) . toHaveBeenCalledTimes ( 1 ) ;
1541+
1542+ // Clean up
1543+ tempServer . close ( ) ;
1544+ } ) ;
1545+
1546+ it ( "should not call onsessionclosed callback when not provided" , async ( ) => {
1547+ // Create server without onsessionclosed callback
1548+ const result = await createTestServer ( {
1549+ sessionIdGenerator : ( ) => randomUUID ( ) ,
1550+ } ) ;
1551+
1552+ const tempServer = result . server ;
1553+ const tempUrl = result . baseUrl ;
1554+
1555+ // Initialize to get a session ID
1556+ const initResponse = await sendPostRequest ( tempUrl , TEST_MESSAGES . initialize ) ;
1557+ const tempSessionId = initResponse . headers . get ( "mcp-session-id" ) ;
1558+
1559+ // DELETE the session - should not throw error
1560+ const deleteResponse = await fetch ( tempUrl , {
1561+ method : "DELETE" ,
1562+ headers : {
1563+ "mcp-session-id" : tempSessionId || "" ,
1564+ "mcp-protocol-version" : "2025-03-26" ,
1565+ } ,
1566+ } ) ;
1567+
1568+ expect ( deleteResponse . status ) . toBe ( 200 ) ;
1569+
1570+ // Clean up
1571+ tempServer . close ( ) ;
1572+ } ) ;
1573+
1574+ it ( "should not call onsessionclosed callback for invalid session DELETE" , async ( ) => {
1575+ const mockCallback = jest . fn ( ) ;
1576+
1577+ // Create server with onsessionclosed callback
1578+ const result = await createTestServer ( {
1579+ sessionIdGenerator : ( ) => randomUUID ( ) ,
1580+ onsessionclosed : mockCallback ,
1581+ } ) ;
1582+
1583+ const tempServer = result . server ;
1584+ const tempUrl = result . baseUrl ;
1585+
1586+ // Initialize to get a valid session
1587+ await sendPostRequest ( tempUrl , TEST_MESSAGES . initialize ) ;
1588+
1589+ // Try to DELETE with invalid session ID
1590+ const deleteResponse = await fetch ( tempUrl , {
1591+ method : "DELETE" ,
1592+ headers : {
1593+ "mcp-session-id" : "invalid-session-id" ,
1594+ "mcp-protocol-version" : "2025-03-26" ,
1595+ } ,
1596+ } ) ;
1597+
1598+ expect ( deleteResponse . status ) . toBe ( 404 ) ;
1599+ expect ( mockCallback ) . not . toHaveBeenCalled ( ) ;
1600+
1601+ // Clean up
1602+ tempServer . close ( ) ;
1603+ } ) ;
1604+
1605+ it ( "should call onsessionclosed callback with correct session ID when multiple sessions exist" , async ( ) => {
1606+ const mockCallback = jest . fn ( ) ;
1607+
1608+ // Create first server
1609+ const result1 = await createTestServer ( {
1610+ sessionIdGenerator : ( ) => randomUUID ( ) ,
1611+ onsessionclosed : mockCallback ,
1612+ } ) ;
1613+
1614+ const server1 = result1 . server ;
1615+ const url1 = result1 . baseUrl ;
1616+
1617+ // Create second server
1618+ const result2 = await createTestServer ( {
1619+ sessionIdGenerator : ( ) => randomUUID ( ) ,
1620+ onsessionclosed : mockCallback ,
1621+ } ) ;
1622+
1623+ const server2 = result2 . server ;
1624+ const url2 = result2 . baseUrl ;
1625+
1626+ // Initialize both servers
1627+ const initResponse1 = await sendPostRequest ( url1 , TEST_MESSAGES . initialize ) ;
1628+ const sessionId1 = initResponse1 . headers . get ( "mcp-session-id" ) ;
1629+
1630+ const initResponse2 = await sendPostRequest ( url2 , TEST_MESSAGES . initialize ) ;
1631+ const sessionId2 = initResponse2 . headers . get ( "mcp-session-id" ) ;
1632+
1633+ expect ( sessionId1 ) . toBeDefined ( ) ;
1634+ expect ( sessionId2 ) . toBeDefined ( ) ;
1635+ expect ( sessionId1 ) . not . toBe ( sessionId2 ) ;
1636+
1637+ // DELETE first session
1638+ const deleteResponse1 = await fetch ( url1 , {
1639+ method : "DELETE" ,
1640+ headers : {
1641+ "mcp-session-id" : sessionId1 || "" ,
1642+ "mcp-protocol-version" : "2025-03-26" ,
1643+ } ,
1644+ } ) ;
1645+
1646+ expect ( deleteResponse1 . status ) . toBe ( 200 ) ;
1647+ expect ( mockCallback ) . toHaveBeenCalledWith ( sessionId1 ) ;
1648+ expect ( mockCallback ) . toHaveBeenCalledTimes ( 1 ) ;
1649+
1650+ // DELETE second session
1651+ const deleteResponse2 = await fetch ( url2 , {
1652+ method : "DELETE" ,
1653+ headers : {
1654+ "mcp-session-id" : sessionId2 || "" ,
1655+ "mcp-protocol-version" : "2025-03-26" ,
1656+ } ,
1657+ } ) ;
1658+
1659+ expect ( deleteResponse2 . status ) . toBe ( 200 ) ;
1660+ expect ( mockCallback ) . toHaveBeenCalledWith ( sessionId2 ) ;
1661+ expect ( mockCallback ) . toHaveBeenCalledTimes ( 2 ) ;
1662+
1663+ // Clean up
1664+ server1 . close ( ) ;
1665+ server2 . close ( ) ;
1666+ } ) ;
1667+ } ) ;
1668+
15071669// Test DNS rebinding protection
15081670describe ( "StreamableHTTPServerTransport DNS rebinding protection" , ( ) => {
15091671 let server : Server ;
0 commit comments