@@ -203,7 +203,7 @@ def test_send_request_raises_forbidden_error
203203 assert_equal ( { method : "tools/list" , params : nil } , error . request )
204204 end
205205
206- def test_send_request_raises_not_found_error
206+ def test_send_request_raises_not_found_error_on_404_without_session
207207 request = {
208208 jsonrpc : "2.0" ,
209209 id : "test_id" ,
@@ -218,11 +218,56 @@ def test_send_request_raises_not_found_error
218218 client . send_request ( request : request )
219219 end
220220
221+ refute_kind_of ( SessionExpiredError , error )
221222 assert_equal ( "The tools/list request is not found" , error . message )
222223 assert_equal ( :not_found , error . error_type )
223224 assert_equal ( { method : "tools/list" , params : nil } , error . request )
224225 end
225226
227+ def test_send_request_raises_session_expired_error_on_404_with_session
228+ stub_request ( :post , url )
229+ . to_return (
230+ status : 200 ,
231+ headers : {
232+ "Content-Type" => "application/json" ,
233+ "Mcp-Session-Id" => "session-abc" ,
234+ } ,
235+ body : { result : { protocolVersion : "2025-11-25" } } . to_json ,
236+ )
237+
238+ client . send_request ( request : { jsonrpc : "2.0" , id : "1" , method : "initialize" } )
239+
240+ stub_request ( :post , url ) . to_return ( status : 404 )
241+
242+ error = assert_raises ( SessionExpiredError ) do
243+ client . send_request ( request : { jsonrpc : "2.0" , id : "2" , method : "tools/list" } )
244+ end
245+
246+ assert_equal ( :not_found , error . error_type )
247+ end
248+
249+ def test_session_expired_error_is_a_request_handler_error
250+ stub_request ( :post , url )
251+ . to_return (
252+ status : 200 ,
253+ headers : {
254+ "Content-Type" => "application/json" ,
255+ "Mcp-Session-Id" => "session-abc" ,
256+ } ,
257+ body : { result : { protocolVersion : "2025-11-25" } } . to_json ,
258+ )
259+
260+ client . send_request ( request : { jsonrpc : "2.0" , id : "1" , method : "initialize" } )
261+
262+ stub_request ( :post , url ) . to_return ( status : 404 )
263+
264+ error = assert_raises ( RequestHandlerError ) do
265+ client . send_request ( request : { jsonrpc : "2.0" , id : "2" , method : "tools/list" } )
266+ end
267+
268+ assert_kind_of ( SessionExpiredError , error )
269+ end
270+
226271 def test_send_request_raises_unprocessable_entity_error
227272 request = {
228273 jsonrpc : "2.0" ,
@@ -413,6 +458,149 @@ def test_send_request_raises_error_for_sse_without_response
413458 assert_equal ( :parse_error , error . error_type )
414459 end
415460
461+ def test_captures_session_id_and_protocol_version_on_initialize
462+ stub_request ( :post , url )
463+ . to_return (
464+ status : 200 ,
465+ headers : {
466+ "Content-Type" => "application/json" ,
467+ "Mcp-Session-Id" => "session-abc" ,
468+ } ,
469+ body : { result : { protocolVersion : "2025-11-25" } } . to_json ,
470+ )
471+
472+ client . send_request ( request : { jsonrpc : "2.0" , id : "1" , method : "initialize" } )
473+
474+ assert_equal ( "session-abc" , client . session_id )
475+ assert_equal ( "2025-11-25" , client . protocol_version )
476+ end
477+
478+ def test_includes_session_and_protocol_version_headers_after_initialize
479+ stub_request ( :post , url )
480+ . to_return (
481+ status : 200 ,
482+ headers : {
483+ "Content-Type" => "application/json" ,
484+ "Mcp-Session-Id" => "session-abc" ,
485+ } ,
486+ body : { result : { protocolVersion : "2025-11-25" } } . to_json ,
487+ )
488+
489+ client . send_request ( request : { jsonrpc : "2.0" , id : "1" , method : "initialize" } )
490+
491+ stub_request ( :post , url )
492+ . with (
493+ headers : {
494+ "Mcp-Session-Id" => "session-abc" ,
495+ "MCP-Protocol-Version" => "2025-11-25" ,
496+ } ,
497+ )
498+ . to_return (
499+ status : 200 ,
500+ headers : { "Content-Type" => "application/json" } ,
501+ body : { result : { tools : [ ] } } . to_json ,
502+ )
503+
504+ client . send_request ( request : { jsonrpc : "2.0" , id : "2" , method : "tools/list" } )
505+ end
506+
507+ def test_does_not_send_protocol_version_header_before_initialize
508+ stub_request ( :post , url )
509+ . with { |req | !req . headers . keys . map ( &:downcase ) . include? ( "mcp-protocol-version" ) }
510+ . to_return (
511+ status : 200 ,
512+ headers : { "Content-Type" => "application/json" } ,
513+ body : { result : { protocolVersion : "2025-11-25" } } . to_json ,
514+ )
515+
516+ client . send_request ( request : { jsonrpc : "2.0" , id : "1" , method : "initialize" } )
517+ end
518+
519+ def test_ignores_empty_session_id_header
520+ stub_request ( :post , url )
521+ . to_return (
522+ status : 200 ,
523+ headers : {
524+ "Content-Type" => "application/json" ,
525+ "Mcp-Session-Id" => "" ,
526+ } ,
527+ body : { result : { protocolVersion : "2025-11-25" } } . to_json ,
528+ )
529+
530+ client . send_request ( request : { jsonrpc : "2.0" , id : "1" , method : "initialize" } )
531+
532+ assert_nil ( client . session_id )
533+ end
534+
535+ def test_session_id_not_overwritten_by_subsequent_responses
536+ stub_request ( :post , url )
537+ . to_return (
538+ status : 200 ,
539+ headers : {
540+ "Content-Type" => "application/json" ,
541+ "Mcp-Session-Id" => "original-session" ,
542+ } ,
543+ body : { result : { protocolVersion : "2025-11-25" } } . to_json ,
544+ )
545+
546+ client . send_request ( request : { jsonrpc : "2.0" , id : "1" , method : "initialize" } )
547+
548+ assert_equal ( "original-session" , client . session_id )
549+
550+ stub_request ( :post , url )
551+ . to_return (
552+ status : 200 ,
553+ headers : {
554+ "Content-Type" => "application/json" ,
555+ "Mcp-Session-Id" => "different-session" ,
556+ } ,
557+ body : { result : { tools : [ ] } } . to_json ,
558+ )
559+
560+ client . send_request ( request : { jsonrpc : "2.0" , id : "2" , method : "tools/list" } )
561+
562+ assert_equal ( "original-session" , client . session_id )
563+ end
564+
565+ def test_stateless_server_without_session_id_header
566+ stub_request ( :post , url )
567+ . to_return (
568+ status : 200 ,
569+ headers : { "Content-Type" => "application/json" } ,
570+ body : { result : { protocolVersion : "2025-11-25" } } . to_json ,
571+ )
572+
573+ client . send_request ( request : { jsonrpc : "2.0" , id : "1" , method : "initialize" } )
574+
575+ assert_nil ( client . session_id )
576+ assert_equal ( "2025-11-25" , client . protocol_version )
577+ end
578+
579+ def test_clears_session_state_on_404
580+ stub_request ( :post , url )
581+ . to_return (
582+ status : 200 ,
583+ headers : {
584+ "Content-Type" => "application/json" ,
585+ "Mcp-Session-Id" => "session-abc" ,
586+ } ,
587+ body : { result : { protocolVersion : "2025-11-25" } } . to_json ,
588+ )
589+
590+ client . send_request ( request : { jsonrpc : "2.0" , id : "1" , method : "initialize" } )
591+
592+ assert_equal ( "session-abc" , client . session_id )
593+
594+ stub_request ( :post , url ) . to_return ( status : 404 )
595+
596+ assert_raises ( SessionExpiredError ) do
597+ client . send_request ( request : { jsonrpc : "2.0" , id : "2" , method : "tools/list" } )
598+ end
599+
600+ assert_nil ( client . session_id )
601+ assert_nil ( client . protocol_version )
602+ end
603+
416604 private
417605
418606 def stub_request ( method , url )
0 commit comments