@@ -57,11 +57,24 @@ describe("StreamableHTTPServerTransport", () => {
57
57
} ) ;
58
58
59
59
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
+
60
71
const req = createMockRequest ( {
61
- method : "GET " ,
72
+ method : "POST " ,
62
73
headers : {
63
- accept : "text/event-stream"
74
+ "content-type" : "application/json" ,
75
+ "accept" : "application/json" ,
64
76
} ,
77
+ body : JSON . stringify ( initializeMessage ) ,
65
78
} ) ;
66
79
67
80
await transport . handleRequest ( req , mockResponse ) ;
@@ -79,6 +92,7 @@ describe("StreamableHTTPServerTransport", () => {
79
92
method : "GET" ,
80
93
headers : {
81
94
"mcp-session-id" : "invalid-session-id" ,
95
+ "accept" : "text/event-stream"
82
96
} ,
83
97
} ) ;
84
98
@@ -89,13 +103,241 @@ describe("StreamableHTTPServerTransport", () => {
89
103
expect ( mockResponse . end ) . toHaveBeenCalledWith ( expect . stringContaining ( '"jsonrpc":"2.0"' ) ) ;
90
104
expect ( mockResponse . end ) . toHaveBeenCalledWith ( expect . stringContaining ( '"error"' ) ) ;
91
105
} ) ;
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
+ } ) ;
92
332
} ) ;
93
333
94
334
describe ( "Request Handling" , ( ) => {
95
335
it ( "should reject GET requests without Accept: text/event-stream header" , async ( ) => {
96
336
const req = createMockRequest ( {
97
337
method : "GET" ,
98
- headers : { } ,
338
+ headers : {
339
+ "mcp-session-id" : transport . sessionId ,
340
+ } ,
99
341
} ) ;
100
342
101
343
await transport . handleRequest ( req , mockResponse ) ;
@@ -108,7 +350,8 @@ describe("StreamableHTTPServerTransport", () => {
108
350
const req = createMockRequest ( {
109
351
method : "GET" ,
110
352
headers : {
111
- accept : "text/event-stream" ,
353
+ "accept" : "text/event-stream" ,
354
+ "mcp-session-id" : transport . sessionId ,
112
355
} ,
113
356
} ) ;
114
357
@@ -127,7 +370,7 @@ describe("StreamableHTTPServerTransport", () => {
127
370
it ( "should reject POST requests without proper Accept header" , async ( ) => {
128
371
const message : JSONRPCMessage = {
129
372
jsonrpc : "2.0" ,
130
- method : "test" ,
373
+ method : "initialize" , // Use initialize to bypass session ID check
131
374
params : { } ,
132
375
id : 1 ,
133
376
} ;
@@ -148,7 +391,7 @@ describe("StreamableHTTPServerTransport", () => {
148
391
it ( "should properly handle JSON-RPC request messages in POST requests" , async ( ) => {
149
392
const message : JSONRPCMessage = {
150
393
jsonrpc : "2.0" ,
151
- method : "test" ,
394
+ method : "initialize" , // Use initialize to bypass session ID check
152
395
params : { } ,
153
396
id : 1 ,
154
397
} ;
@@ -188,6 +431,7 @@ describe("StreamableHTTPServerTransport", () => {
188
431
headers : {
189
432
"content-type" : "application/json" ,
190
433
"accept" : "application/json, text/event-stream" ,
434
+ "mcp-session-id" : transport . sessionId ,
191
435
} ,
192
436
body : JSON . stringify ( notification ) ,
193
437
} ) ;
@@ -212,6 +456,7 @@ describe("StreamableHTTPServerTransport", () => {
212
456
headers : {
213
457
"content-type" : "application/json" ,
214
458
"accept" : "application/json" ,
459
+ "mcp-session-id" : transport . sessionId ,
215
460
} ,
216
461
body : JSON . stringify ( batchMessages ) ,
217
462
} ) ;
@@ -231,6 +476,7 @@ describe("StreamableHTTPServerTransport", () => {
231
476
headers : {
232
477
"content-type" : "text/plain" ,
233
478
"accept" : "application/json" ,
479
+ "mcp-session-id" : transport . sessionId ,
234
480
} ,
235
481
body : "test" ,
236
482
} ) ;
@@ -244,7 +490,9 @@ describe("StreamableHTTPServerTransport", () => {
244
490
it ( "should properly handle DELETE requests and close session" , async ( ) => {
245
491
const req = createMockRequest ( {
246
492
method : "DELETE" ,
247
- headers : { } ,
493
+ headers : {
494
+ "mcp-session-id" : transport . sessionId ,
495
+ } ,
248
496
} ) ;
249
497
250
498
const onCloseMock = jest . fn ( ) ;
@@ -259,11 +507,12 @@ describe("StreamableHTTPServerTransport", () => {
259
507
260
508
describe ( "Message Replay" , ( ) => {
261
509
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
263
511
const req1 = createMockRequest ( {
264
512
method : "GET" ,
265
513
headers : {
266
- "accept" : "text/event-stream"
514
+ "accept" : "text/event-stream" ,
515
+ "mcp-session-id" : transport . sessionId
267
516
} ,
268
517
} ) ;
269
518
await transport . handleRequest ( req1 , mockResponse ) ;
@@ -293,6 +542,7 @@ describe("StreamableHTTPServerTransport", () => {
293
542
headers : {
294
543
"accept" : "text/event-stream" ,
295
544
"last-event-id" : lastEventId ,
545
+ "mcp-session-id" : transport . sessionId
296
546
} ,
297
547
} ) ;
298
548
0 commit comments