@@ -57,11 +57,24 @@ describe("StreamableHTTPServerTransport", () => {
5757 } ) ;
5858
5959 it ( "should include session ID in response headers" , async ( ) => {
60+ // Use POST with initialize method to avoid session ID requirement
61+ const initializeMessage : JSONRPCMessage = {
62+ jsonrpc : "2.0" ,
63+ method : "initialize" ,
64+ params : {
65+ clientInfo : { name : "test-client" , version : "1.0" } ,
66+ protocolVersion : "2025-03-26"
67+ } ,
68+ id : "init-1" ,
69+ } ;
70+
6071 const req = createMockRequest ( {
61- method : "GET " ,
72+ method : "POST " ,
6273 headers : {
63- accept : "text/event-stream"
74+ "content-type" : "application/json" ,
75+ "accept" : "application/json" ,
6476 } ,
77+ body : JSON . stringify ( initializeMessage ) ,
6578 } ) ;
6679
6780 await transport . handleRequest ( req , mockResponse ) ;
@@ -79,6 +92,7 @@ describe("StreamableHTTPServerTransport", () => {
7992 method : "GET" ,
8093 headers : {
8194 "mcp-session-id" : "invalid-session-id" ,
95+ "accept" : "text/event-stream"
8296 } ,
8397 } ) ;
8498
@@ -89,13 +103,241 @@ describe("StreamableHTTPServerTransport", () => {
89103 expect ( mockResponse . end ) . toHaveBeenCalledWith ( expect . stringContaining ( '"jsonrpc":"2.0"' ) ) ;
90104 expect ( mockResponse . end ) . toHaveBeenCalledWith ( expect . stringContaining ( '"error"' ) ) ;
91105 } ) ;
106+
107+ it ( "should reject non-initialization requests without session ID with 400 Bad Request" , async ( ) => {
108+ const req = createMockRequest ( {
109+ method : "GET" ,
110+ headers : {
111+ accept : "text/event-stream" ,
112+ // No mcp-session-id header
113+ } ,
114+ } ) ;
115+
116+ await transport . handleRequest ( req , mockResponse ) ;
117+
118+ expect ( mockResponse . writeHead ) . toHaveBeenCalledWith ( 400 ) ;
119+ expect ( mockResponse . end ) . toHaveBeenCalledWith ( expect . stringContaining ( '"jsonrpc":"2.0"' ) ) ;
120+ expect ( mockResponse . end ) . toHaveBeenCalledWith ( expect . stringContaining ( '"message":"Bad Request: Mcp-Session-Id header is required"' ) ) ;
121+ } ) ;
122+
123+ it ( "should always include session ID in initialization response even in stateless mode" , async ( ) => {
124+ // Create a stateless transport for this test
125+ const statelessTransport = new StreamableHTTPServerTransport ( endpoint , { enableSessionManagement : false } ) ;
126+
127+ // Create an initialization request
128+ const initializeMessage : JSONRPCMessage = {
129+ jsonrpc : "2.0" ,
130+ method : "initialize" ,
131+ params : {
132+ clientInfo : { name : "test-client" , version : "1.0" } ,
133+ protocolVersion : "2025-03-26"
134+ } ,
135+ id : "init-1" ,
136+ } ;
137+
138+ const req = createMockRequest ( {
139+ method : "POST" ,
140+ headers : {
141+ "content-type" : "application/json" ,
142+ "accept" : "application/json" ,
143+ } ,
144+ body : JSON . stringify ( initializeMessage ) ,
145+ } ) ;
146+
147+ await statelessTransport . handleRequest ( req , mockResponse ) ;
148+
149+ // In stateless mode, session ID should also be included for initialize responses
150+ expect ( mockResponse . writeHead ) . toHaveBeenCalledWith (
151+ 200 ,
152+ expect . objectContaining ( {
153+ "mcp-session-id" : statelessTransport . sessionId ,
154+ } )
155+ ) ;
156+ } ) ;
157+ } ) ;
158+
159+ describe ( "Stateless Mode" , ( ) => {
160+ let statelessTransport : StreamableHTTPServerTransport ;
161+ let mockResponse : jest . Mocked < ServerResponse > ;
162+
163+ beforeEach ( ( ) => {
164+ statelessTransport = new StreamableHTTPServerTransport ( endpoint , { enableSessionManagement : false } ) ;
165+ mockResponse = createMockResponse ( ) ;
166+ } ) ;
167+
168+ it ( "should not include session ID in response headers when in stateless mode" , async ( ) => {
169+ // Use a non-initialization request
170+ const message : JSONRPCMessage = {
171+ jsonrpc : "2.0" ,
172+ method : "test" ,
173+ params : { } ,
174+ id : 1 ,
175+ } ;
176+
177+ const req = createMockRequest ( {
178+ method : "POST" ,
179+ headers : {
180+ "content-type" : "application/json" ,
181+ "accept" : "application/json" ,
182+ } ,
183+ body : JSON . stringify ( message ) ,
184+ } ) ;
185+
186+ await statelessTransport . handleRequest ( req , mockResponse ) ;
187+
188+ expect ( mockResponse . writeHead ) . toHaveBeenCalled ( ) ;
189+ // Extract the headers from writeHead call
190+ const headers = mockResponse . writeHead . mock . calls [ 0 ] [ 1 ] ;
191+ expect ( headers ) . not . toHaveProperty ( "mcp-session-id" ) ;
192+ } ) ;
193+
194+ it ( "should not validate session ID in stateless mode" , async ( ) => {
195+ const req = createMockRequest ( {
196+ method : "GET" ,
197+ headers : {
198+ accept : "text/event-stream" ,
199+ "mcp-session-id" : "invalid-session-id" , // This would cause a 404 in stateful mode
200+ } ,
201+ } ) ;
202+
203+ await statelessTransport . handleRequest ( req , mockResponse ) ;
204+
205+ // Should still get 200 OK, not 404 Not Found
206+ expect ( mockResponse . writeHead ) . toHaveBeenCalledWith (
207+ 200 ,
208+ expect . not . objectContaining ( {
209+ "mcp-session-id" : expect . anything ( ) ,
210+ } )
211+ ) ;
212+ } ) ;
213+
214+ it ( "should handle POST requests without session validation in stateless mode" , async ( ) => {
215+ const message : JSONRPCMessage = {
216+ jsonrpc : "2.0" ,
217+ method : "test" ,
218+ params : { } ,
219+ id : 1 ,
220+ } ;
221+
222+ const req = createMockRequest ( {
223+ method : "POST" ,
224+ headers : {
225+ "content-type" : "application/json" ,
226+ "accept" : "application/json" ,
227+ "mcp-session-id" : "non-existent-session-id" , // This would be rejected in stateful mode
228+ } ,
229+ body : JSON . stringify ( message ) ,
230+ } ) ;
231+
232+ const onMessageMock = jest . fn ( ) ;
233+ statelessTransport . onmessage = onMessageMock ;
234+
235+ await statelessTransport . handleRequest ( req , mockResponse ) ;
236+
237+ // Message should be processed despite invalid session ID
238+ expect ( onMessageMock ) . toHaveBeenCalledWith ( message ) ;
239+ } ) ;
240+
241+ it ( "should work with a mix of requests with and without session IDs in stateless mode" , async ( ) => {
242+ // First request without session ID
243+ const req1 = createMockRequest ( {
244+ method : "GET" ,
245+ headers : {
246+ accept : "text/event-stream" ,
247+ } ,
248+ } ) ;
249+
250+ await statelessTransport . handleRequest ( req1 , mockResponse ) ;
251+ expect ( mockResponse . writeHead ) . toHaveBeenCalledWith (
252+ 200 ,
253+ expect . objectContaining ( {
254+ "Content-Type" : "text/event-stream" ,
255+ } )
256+ ) ;
257+
258+ // Reset mock for second request
259+ mockResponse . writeHead . mockClear ( ) ;
260+
261+ // Second request with a session ID (which would be invalid in stateful mode)
262+ const req2 = createMockRequest ( {
263+ method : "GET" ,
264+ headers : {
265+ accept : "text/event-stream" ,
266+ "mcp-session-id" : "some-random-session-id" ,
267+ } ,
268+ } ) ;
269+
270+ await statelessTransport . handleRequest ( req2 , mockResponse ) ;
271+
272+ // Should still succeed
273+ expect ( mockResponse . writeHead ) . toHaveBeenCalledWith (
274+ 200 ,
275+ expect . objectContaining ( {
276+ "Content-Type" : "text/event-stream" ,
277+ } )
278+ ) ;
279+ } ) ;
280+
281+ it ( "should handle initialization requests properly in both modes" , async ( ) => {
282+ // Initialize message that would typically be sent during initialization
283+ const initializeMessage : JSONRPCMessage = {
284+ jsonrpc : "2.0" ,
285+ method : "initialize" ,
286+ params : {
287+ clientInfo : { name : "test-client" , version : "1.0" } ,
288+ protocolVersion : "2025-03-26"
289+ } ,
290+ id : "init-1" ,
291+ } ;
292+
293+ // Test stateful transport (default)
294+ const statefulReq = createMockRequest ( {
295+ method : "POST" ,
296+ headers : {
297+ "content-type" : "application/json" ,
298+ "accept" : "application/json" ,
299+ } ,
300+ body : JSON . stringify ( initializeMessage ) ,
301+ } ) ;
302+
303+ await transport . handleRequest ( statefulReq , mockResponse ) ;
304+
305+ // In stateful mode, session ID should be included in the response header
306+ expect ( mockResponse . writeHead ) . toHaveBeenCalledWith (
307+ 200 ,
308+ expect . objectContaining ( {
309+ "mcp-session-id" : transport . sessionId ,
310+ } )
311+ ) ;
312+
313+ // Reset mocks for stateless test
314+ mockResponse . writeHead . mockClear ( ) ;
315+
316+ // Test stateless transport
317+ const statelessReq = createMockRequest ( {
318+ method : "POST" ,
319+ headers : {
320+ "content-type" : "application/json" ,
321+ "accept" : "application/json" ,
322+ } ,
323+ body : JSON . stringify ( initializeMessage ) ,
324+ } ) ;
325+
326+ await statelessTransport . handleRequest ( statelessReq , mockResponse ) ;
327+
328+ // In stateless mode, session ID should also be included for initialize responses
329+ const headers = mockResponse . writeHead . mock . calls [ 0 ] [ 1 ] ;
330+ expect ( headers ) . toHaveProperty ( "mcp-session-id" , statelessTransport . sessionId ) ;
331+ } ) ;
92332 } ) ;
93333
94334 describe ( "Request Handling" , ( ) => {
95335 it ( "should reject GET requests without Accept: text/event-stream header" , async ( ) => {
96336 const req = createMockRequest ( {
97337 method : "GET" ,
98- headers : { } ,
338+ headers : {
339+ "mcp-session-id" : transport . sessionId ,
340+ } ,
99341 } ) ;
100342
101343 await transport . handleRequest ( req , mockResponse ) ;
@@ -108,7 +350,8 @@ describe("StreamableHTTPServerTransport", () => {
108350 const req = createMockRequest ( {
109351 method : "GET" ,
110352 headers : {
111- accept : "text/event-stream" ,
353+ "accept" : "text/event-stream" ,
354+ "mcp-session-id" : transport . sessionId ,
112355 } ,
113356 } ) ;
114357
@@ -127,7 +370,7 @@ describe("StreamableHTTPServerTransport", () => {
127370 it ( "should reject POST requests without proper Accept header" , async ( ) => {
128371 const message : JSONRPCMessage = {
129372 jsonrpc : "2.0" ,
130- method : "test" ,
373+ method : "initialize" , // Use initialize to bypass session ID check
131374 params : { } ,
132375 id : 1 ,
133376 } ;
@@ -148,7 +391,7 @@ describe("StreamableHTTPServerTransport", () => {
148391 it ( "should properly handle JSON-RPC request messages in POST requests" , async ( ) => {
149392 const message : JSONRPCMessage = {
150393 jsonrpc : "2.0" ,
151- method : "test" ,
394+ method : "initialize" , // Use initialize to bypass session ID check
152395 params : { } ,
153396 id : 1 ,
154397 } ;
@@ -188,6 +431,7 @@ describe("StreamableHTTPServerTransport", () => {
188431 headers : {
189432 "content-type" : "application/json" ,
190433 "accept" : "application/json, text/event-stream" ,
434+ "mcp-session-id" : transport . sessionId ,
191435 } ,
192436 body : JSON . stringify ( notification ) ,
193437 } ) ;
@@ -212,6 +456,7 @@ describe("StreamableHTTPServerTransport", () => {
212456 headers : {
213457 "content-type" : "application/json" ,
214458 "accept" : "application/json" ,
459+ "mcp-session-id" : transport . sessionId ,
215460 } ,
216461 body : JSON . stringify ( batchMessages ) ,
217462 } ) ;
@@ -231,6 +476,7 @@ describe("StreamableHTTPServerTransport", () => {
231476 headers : {
232477 "content-type" : "text/plain" ,
233478 "accept" : "application/json" ,
479+ "mcp-session-id" : transport . sessionId ,
234480 } ,
235481 body : "test" ,
236482 } ) ;
@@ -244,7 +490,9 @@ describe("StreamableHTTPServerTransport", () => {
244490 it ( "should properly handle DELETE requests and close session" , async ( ) => {
245491 const req = createMockRequest ( {
246492 method : "DELETE" ,
247- headers : { } ,
493+ headers : {
494+ "mcp-session-id" : transport . sessionId ,
495+ } ,
248496 } ) ;
249497
250498 const onCloseMock = jest . fn ( ) ;
@@ -259,11 +507,12 @@ describe("StreamableHTTPServerTransport", () => {
259507
260508 describe ( "Message Replay" , ( ) => {
261509 it ( "should replay messages after specified Last-Event-ID" , async ( ) => {
262- // Establish first connection with Accept header
510+ // Establish first connection with Accept header and session ID
263511 const req1 = createMockRequest ( {
264512 method : "GET" ,
265513 headers : {
266- "accept" : "text/event-stream"
514+ "accept" : "text/event-stream" ,
515+ "mcp-session-id" : transport . sessionId
267516 } ,
268517 } ) ;
269518 await transport . handleRequest ( req1 , mockResponse ) ;
@@ -293,6 +542,7 @@ describe("StreamableHTTPServerTransport", () => {
293542 headers : {
294543 "accept" : "text/event-stream" ,
295544 "last-event-id" : lastEventId ,
545+ "mcp-session-id" : transport . sessionId
296546 } ,
297547 } ) ;
298548
0 commit comments