@@ -12,7 +12,9 @@ import {
1212 EventStore ,
1313 EventId ,
1414 StreamId ,
15- AuthenticatedRequest
15+ AuthenticatedRequest ,
16+ SessionStore ,
17+ SessionState
1618} from './fetchStreamableHttpServerTransport.js' ;
1719import { McpServer } from '../../server/mcp.js' ;
1820import { CallToolResult , JSONRPCMessage } from '../../types.js' ;
@@ -130,6 +132,7 @@ interface TestServerConfig {
130132 sessionIdGenerator : ( ( ) => string ) | undefined ;
131133 enableJsonResponse ?: boolean ;
132134 eventStore ?: EventStore ;
135+ sessionStore ?: SessionStore ;
133136 onsessioninitialized ?: ( sessionId : string ) => void | Promise < void > ;
134137 onsessionclosed ?: ( sessionId : string ) => void | Promise < void > ;
135138 retryInterval ?: number ;
@@ -250,6 +253,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
250253 sessionIdGenerator : config . sessionIdGenerator ,
251254 enableJsonResponse : config . enableJsonResponse ?? false ,
252255 eventStore : config . eventStore ,
256+ sessionStore : config . sessionStore ,
253257 onsessioninitialized : config . onsessioninitialized ,
254258 onsessionclosed : config . onsessionclosed ,
255259 retryInterval : config . retryInterval ,
@@ -2231,4 +2235,262 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
22312235 } ) ;
22322236 } ) ;
22332237 } ) ;
2238+
2239+ /**
2240+ * Tests for SessionStore functionality (distributed/serverless mode)
2241+ */
2242+ describe ( 'FetchStreamableHTTPServerTransport with SessionStore' , ( ) => {
2243+ let server : Server ;
2244+ let transport : FetchStreamableHTTPServerTransport ;
2245+ let baseUrl : URL ;
2246+
2247+ afterEach ( async ( ) => {
2248+ if ( server && transport ) {
2249+ await stopTestServer ( { server, transport } ) ;
2250+ }
2251+ } ) ;
2252+
2253+ /**
2254+ * Creates an in-memory session store for testing
2255+ */
2256+ function createInMemorySessionStore ( ) : SessionStore & { sessions : Map < string , SessionState > } {
2257+ const sessions = new Map < string , SessionState > ( ) ;
2258+ return {
2259+ sessions,
2260+ get : async ( sessionId : string ) => sessions . get ( sessionId ) ,
2261+ save : async ( sessionId : string , state : SessionState ) => {
2262+ sessions . set ( sessionId , state ) ;
2263+ } ,
2264+ delete : async ( sessionId : string ) => {
2265+ sessions . delete ( sessionId ) ;
2266+ }
2267+ } ;
2268+ }
2269+
2270+ it ( 'should save session state to store on initialization' , async ( ) => {
2271+ const sessionStore = createInMemorySessionStore ( ) ;
2272+ const result = await createTestServer ( {
2273+ sessionIdGenerator : ( ) => 'test-session-123' ,
2274+ sessionStore
2275+ } ) ;
2276+ server = result . server ;
2277+ transport = result . transport ;
2278+ baseUrl = result . baseUrl ;
2279+
2280+ // Initialize the session
2281+ const response = await fetch ( baseUrl , {
2282+ method : 'POST' ,
2283+ headers : {
2284+ 'Content-Type' : 'application/json' ,
2285+ Accept : 'application/json, text/event-stream'
2286+ } ,
2287+ body : JSON . stringify ( TEST_MESSAGES . initialize )
2288+ } ) ;
2289+
2290+ expect ( response . status ) . toBe ( 200 ) ;
2291+
2292+ // Verify session was saved to store
2293+ const savedSession = await sessionStore . get ( 'test-session-123' ) ;
2294+ expect ( savedSession ) . toBeDefined ( ) ;
2295+ expect ( savedSession ?. initialized ) . toBe ( true ) ;
2296+ expect ( savedSession ?. protocolVersion ) . toBeDefined ( ) ;
2297+ expect ( savedSession ?. createdAt ) . toBeGreaterThan ( 0 ) ;
2298+ } ) ;
2299+
2300+ it ( 'should validate session from store for subsequent requests' , async ( ) => {
2301+ const sessionStore = createInMemorySessionStore ( ) ;
2302+ const result = await createTestServer ( {
2303+ sessionIdGenerator : ( ) => 'test-session-456' ,
2304+ sessionStore
2305+ } ) ;
2306+ server = result . server ;
2307+ transport = result . transport ;
2308+ baseUrl = result . baseUrl ;
2309+
2310+ // Initialize the session
2311+ const initResponse = await fetch ( baseUrl , {
2312+ method : 'POST' ,
2313+ headers : {
2314+ 'Content-Type' : 'application/json' ,
2315+ Accept : 'application/json, text/event-stream'
2316+ } ,
2317+ body : JSON . stringify ( TEST_MESSAGES . initialize )
2318+ } ) ;
2319+ expect ( initResponse . status ) . toBe ( 200 ) ;
2320+ const sessionId = initResponse . headers . get ( 'mcp-session-id' ) ;
2321+
2322+ // Make a subsequent request with valid session ID
2323+ const listResponse = await fetch ( baseUrl , {
2324+ method : 'POST' ,
2325+ headers : {
2326+ 'Content-Type' : 'application/json' ,
2327+ Accept : 'application/json, text/event-stream' ,
2328+ 'mcp-session-id' : sessionId !
2329+ } ,
2330+ body : JSON . stringify ( TEST_MESSAGES . toolsList )
2331+ } ) ;
2332+ expect ( listResponse . status ) . toBe ( 200 ) ;
2333+ } ) ;
2334+
2335+ it ( 'should reject requests with invalid session ID when using session store' , async ( ) => {
2336+ const sessionStore = createInMemorySessionStore ( ) ;
2337+ const result = await createTestServer ( {
2338+ sessionIdGenerator : ( ) => 'test-session-789' ,
2339+ sessionStore
2340+ } ) ;
2341+ server = result . server ;
2342+ transport = result . transport ;
2343+ baseUrl = result . baseUrl ;
2344+
2345+ // Initialize the session first
2346+ const initResponse = await fetch ( baseUrl , {
2347+ method : 'POST' ,
2348+ headers : {
2349+ 'Content-Type' : 'application/json' ,
2350+ Accept : 'application/json, text/event-stream'
2351+ } ,
2352+ body : JSON . stringify ( TEST_MESSAGES . initialize )
2353+ } ) ;
2354+ expect ( initResponse . status ) . toBe ( 200 ) ;
2355+
2356+ // Try to make a request with invalid session ID
2357+ const response = await fetch ( baseUrl , {
2358+ method : 'POST' ,
2359+ headers : {
2360+ 'Content-Type' : 'application/json' ,
2361+ Accept : 'application/json, text/event-stream' ,
2362+ 'mcp-session-id' : 'invalid-session-id'
2363+ } ,
2364+ body : JSON . stringify ( TEST_MESSAGES . toolsList )
2365+ } ) ;
2366+
2367+ expect ( response . status ) . toBe ( 404 ) ;
2368+ const body = await response . json ( ) ;
2369+ expect ( body . error . message ) . toBe ( 'Session not found' ) ;
2370+ } ) ;
2371+
2372+ it ( 'should delete session from store on DELETE request' , async ( ) => {
2373+ const sessionStore = createInMemorySessionStore ( ) ;
2374+ const result = await createTestServer ( {
2375+ sessionIdGenerator : ( ) => 'test-session-delete' ,
2376+ sessionStore
2377+ } ) ;
2378+ server = result . server ;
2379+ transport = result . transport ;
2380+ baseUrl = result . baseUrl ;
2381+
2382+ // Initialize the session
2383+ const initResponse = await fetch ( baseUrl , {
2384+ method : 'POST' ,
2385+ headers : {
2386+ 'Content-Type' : 'application/json' ,
2387+ Accept : 'application/json, text/event-stream'
2388+ } ,
2389+ body : JSON . stringify ( TEST_MESSAGES . initialize )
2390+ } ) ;
2391+ expect ( initResponse . status ) . toBe ( 200 ) ;
2392+ const sessionId = initResponse . headers . get ( 'mcp-session-id' ) ;
2393+
2394+ // Verify session exists in store
2395+ expect ( await sessionStore . get ( sessionId ! ) ) . toBeDefined ( ) ;
2396+
2397+ // Delete the session
2398+ const deleteResponse = await fetch ( baseUrl , {
2399+ method : 'DELETE' ,
2400+ headers : {
2401+ 'mcp-session-id' : sessionId !
2402+ }
2403+ } ) ;
2404+ expect ( deleteResponse . status ) . toBe ( 200 ) ;
2405+
2406+ // Verify session was deleted from store
2407+ expect ( await sessionStore . get ( sessionId ! ) ) . toBeUndefined ( ) ;
2408+ } ) ;
2409+
2410+ it ( 'should allow new transport instances to validate existing sessions (serverless mode)' , async ( ) => {
2411+ // This test simulates serverless behavior where each request
2412+ // is handled by a fresh transport instance
2413+ const sessionStore = createInMemorySessionStore ( ) ;
2414+
2415+ // First, initialize using one transport instance
2416+ const result1 = await createTestServer ( {
2417+ sessionIdGenerator : ( ) => 'serverless-session-123' ,
2418+ sessionStore
2419+ } ) ;
2420+ server = result1 . server ;
2421+ transport = result1 . transport ;
2422+ baseUrl = result1 . baseUrl ;
2423+
2424+ const initResponse = await fetch ( baseUrl , {
2425+ method : 'POST' ,
2426+ headers : {
2427+ 'Content-Type' : 'application/json' ,
2428+ Accept : 'application/json, text/event-stream'
2429+ } ,
2430+ body : JSON . stringify ( TEST_MESSAGES . initialize )
2431+ } ) ;
2432+ expect ( initResponse . status ) . toBe ( 200 ) ;
2433+ const sessionId = initResponse . headers . get ( 'mcp-session-id' ) ;
2434+
2435+ // Stop the first server
2436+ await stopTestServer ( { server, transport } ) ;
2437+
2438+ // Create a NEW transport instance with same sessionStore (simulates new serverless invocation)
2439+ const result2 = await createTestServer ( {
2440+ sessionIdGenerator : ( ) => crypto . randomUUID ( ) , // Different generator, doesn't matter
2441+ sessionStore // Same session store
2442+ } ) ;
2443+ server = result2 . server ;
2444+ transport = result2 . transport ;
2445+ baseUrl = result2 . baseUrl ;
2446+
2447+ // The new transport should be able to validate the existing session from the store
2448+ const listResponse = await fetch ( baseUrl , {
2449+ method : 'POST' ,
2450+ headers : {
2451+ 'Content-Type' : 'application/json' ,
2452+ Accept : 'application/json, text/event-stream' ,
2453+ 'mcp-session-id' : sessionId !
2454+ } ,
2455+ body : JSON . stringify ( TEST_MESSAGES . toolsList )
2456+ } ) ;
2457+
2458+ expect ( listResponse . status ) . toBe ( 200 ) ;
2459+ } ) ;
2460+
2461+ it ( 'should work with GET SSE stream when session is hydrated from store' , async ( ) => {
2462+ const sessionStore = createInMemorySessionStore ( ) ;
2463+ const result = await createTestServer ( {
2464+ sessionIdGenerator : ( ) => 'sse-session-123' ,
2465+ sessionStore
2466+ } ) ;
2467+ server = result . server ;
2468+ transport = result . transport ;
2469+ baseUrl = result . baseUrl ;
2470+
2471+ // Initialize session
2472+ const initResponse = await fetch ( baseUrl , {
2473+ method : 'POST' ,
2474+ headers : {
2475+ 'Content-Type' : 'application/json' ,
2476+ Accept : 'application/json, text/event-stream'
2477+ } ,
2478+ body : JSON . stringify ( TEST_MESSAGES . initialize )
2479+ } ) ;
2480+ expect ( initResponse . status ) . toBe ( 200 ) ;
2481+ const sessionId = initResponse . headers . get ( 'mcp-session-id' ) ;
2482+
2483+ // Open SSE stream with session ID
2484+ const sseResponse = await fetch ( baseUrl , {
2485+ method : 'GET' ,
2486+ headers : {
2487+ Accept : 'text/event-stream' ,
2488+ 'mcp-session-id' : sessionId !
2489+ }
2490+ } ) ;
2491+
2492+ expect ( sseResponse . status ) . toBe ( 200 ) ;
2493+ expect ( sseResponse . headers . get ( 'content-type' ) ) . toBe ( 'text/event-stream' ) ;
2494+ } ) ;
2495+ } ) ;
22342496} ) ;
0 commit comments