@@ -29,6 +29,7 @@ interface TestServerConfig {
29
29
enableJsonResponse ?: boolean ;
30
30
customRequestHandler ?: ( req : IncomingMessage , res : ServerResponse , parsedBody ?: unknown ) => Promise < void > ;
31
31
eventStore ?: EventStore ;
32
+ onsessionclosed ?: ( sessionId : string ) => void ;
32
33
}
33
34
34
35
/**
@@ -57,7 +58,8 @@ async function createTestServer(config: TestServerConfig = { sessionIdGenerator:
57
58
const transport = new StreamableHTTPServerTransport ( {
58
59
sessionIdGenerator : config . sessionIdGenerator ,
59
60
enableJsonResponse : config . enableJsonResponse ?? false ,
60
- eventStore : config . eventStore
61
+ eventStore : config . eventStore ,
62
+ onsessionclosed : config . onsessionclosed
61
63
} ) ;
62
64
63
65
await mcpServer . connect ( transport ) ;
@@ -111,7 +113,8 @@ async function createTestAuthServer(config: TestServerConfig = { sessionIdGenera
111
113
const transport = new StreamableHTTPServerTransport ( {
112
114
sessionIdGenerator : config . sessionIdGenerator ,
113
115
enableJsonResponse : config . enableJsonResponse ?? false ,
114
- eventStore : config . eventStore
116
+ eventStore : config . eventStore ,
117
+ onsessionclosed : config . onsessionclosed
115
118
} ) ;
116
119
117
120
await mcpServer . connect ( transport ) ;
@@ -1504,6 +1507,165 @@ describe("StreamableHTTPServerTransport in stateless mode", () => {
1504
1507
} ) ;
1505
1508
} ) ;
1506
1509
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
+
1507
1669
// Test DNS rebinding protection
1508
1670
describe ( "StreamableHTTPServerTransport DNS rebinding protection" , ( ) => {
1509
1671
let server : Server ;
0 commit comments