@@ -1914,6 +1914,133 @@ def test_captures_session_id_and_protocol_version_on_initialize
19141914 assert_equal ( "2025-11-25" , client . protocol_version )
19151915 end
19161916
1917+ def test_initialize_returns_promptly_and_captures_session_id_when_sse_stream_stays_open
1918+ require "socket"
1919+
1920+ hold_seconds = 5
1921+ seen_session_ids = [ ]
1922+ WebMock . disable!
1923+ server = TCPServer . new ( "127.0.0.1" , 0 )
1924+ connection_threads = [ ]
1925+ server_thread = Thread . new do
1926+ 2 . times do |index |
1927+ socket = server . accept
1928+ connection_threads << Thread . new ( socket , index ) do |conn , conn_index |
1929+ request_headers = { }
1930+ conn . gets
1931+ while ( line = conn . gets ) && line != "\r \n "
1932+ name , value = line . split ( ": " , 2 )
1933+ request_headers [ name . downcase ] = value &.strip
1934+ end
1935+ content_length = request_headers [ "content-length" ] . to_i
1936+ conn . read ( content_length ) if content_length . positive?
1937+ seen_session_ids << request_headers [ "mcp-session-id" ]
1938+ if conn_index . zero?
1939+ event = 'data: {"jsonrpc":"2.0","id":"1","result":{"protocolVersion":"2025-11-25"}}' + "\n \n "
1940+ conn . write (
1941+ "HTTP/1.1 200 OK\r \n " \
1942+ "Content-Type: text/event-stream\r \n " \
1943+ "Mcp-Session-Id: session-held-open\r \n " \
1944+ "Connection: close\r \n \r \n " ,
1945+ )
1946+ conn . write ( event )
1947+ conn . flush
1948+ sleep ( hold_seconds )
1949+ else
1950+ body = { jsonrpc : "2.0" , result : { tools : [ ] } } . to_json
1951+ conn . write (
1952+ "HTTP/1.1 200 OK\r \n " \
1953+ "Content-Type: application/json\r \n " \
1954+ "Content-Length: #{ body . bytesize } \r \n " \
1955+ "Connection: close\r \n \r \n #{ body } " ,
1956+ )
1957+ end
1958+ conn . close
1959+ end
1960+ end
1961+ rescue IOError , SystemCallError
1962+ nil
1963+ end
1964+ begin
1965+ held_client = HTTP . new ( url : "http://127.0.0.1:#{ server . addr [ 1 ] } /" )
1966+ started = Process . clock_gettime ( Process ::CLOCK_MONOTONIC )
1967+ held_client . send_request ( request : { jsonrpc : "2.0" , id : "1" , method : "initialize" } )
1968+ elapsed = Process . clock_gettime ( Process ::CLOCK_MONOTONIC ) - started
1969+ assert_operator ( elapsed , :< , hold_seconds / 2.0 , "initialize waited for the held-open SSE stream to close" )
1970+ assert_equal ( "session-held-open" , held_client . session_id )
1971+ assert_equal ( "2025-11-25" , held_client . protocol_version )
1972+ held_client . send_request ( request : { jsonrpc : "2.0" , id : "2" , method : "tools/list" } )
1973+ server_thread . join ( 5 )
1974+ assert_equal ( "session-held-open" , seen_session_ids [ 1 ] )
1975+ ensure
1976+ connection_threads . each ( &:kill )
1977+ connection_threads . each { |thread | thread . join ( 1 ) }
1978+ server_thread . kill
1979+ server . close unless server . closed?
1980+ end
1981+ ensure
1982+ WebMock . enable!
1983+ end
1984+
1985+ def test_initialize_retries_through_oauth_when_sse_answer_is_401
1986+ require "socket"
1987+
1988+ WebMock . disable!
1989+ server = TCPServer . new ( "127.0.0.1" , 0 )
1990+ connection_threads = [ ]
1991+ server_thread = Thread . new do
1992+ 2 . times do |index |
1993+ socket = server . accept
1994+ connection_threads << Thread . new ( socket , index ) do |conn , conn_index |
1995+ conn . gets
1996+ headers = { }
1997+ while ( line = conn . gets ) && line != "\r \n "
1998+ name , value = line . split ( ": " , 2 )
1999+ headers [ name . downcase ] = value &.strip
2000+ end
2001+ length = headers [ "content-length" ] . to_i
2002+ conn . read ( length ) if length . positive?
2003+ if conn_index . zero?
2004+ event = 'data: {"jsonrpc":"2.0","id":"1","error":{"code":-32001,"message":"unauthorized"}}' + "\n \n "
2005+ conn . write (
2006+ "HTTP/1.1 401 Unauthorized\r \n " \
2007+ "Content-Type: text/event-stream\r \n " \
2008+ "Connection: close\r \n \r \n #{ event } " ,
2009+ )
2010+ else
2011+ event = 'data: {"jsonrpc":"2.0","id":"1","result":{"protocolVersion":"2025-11-25"}}' + "\n \n "
2012+ conn . write (
2013+ "HTTP/1.1 200 OK\r \n " \
2014+ "Content-Type: text/event-stream\r \n " \
2015+ "Mcp-Session-Id: session-after-retry\r \n " \
2016+ "Connection: close\r \n \r \n #{ event } " ,
2017+ )
2018+ end
2019+ conn . close
2020+ end
2021+ end
2022+ rescue IOError , SystemCallError
2023+ nil
2024+ end
2025+ begin
2026+ oauth_client = HTTP . new ( url : "http://127.0.0.1:#{ server . addr [ 1 ] } /" , oauth : Struct . new ( :access_token ) . new )
2027+ oauth_calls = 0
2028+ oauth_client . define_singleton_method ( :run_oauth_flow! ) { |unauthorized_error :| oauth_calls += 1 }
2029+ oauth_client . send_request ( request : { jsonrpc : "2.0" , id : "1" , method : "initialize" } )
2030+ server_thread . join ( 5 )
2031+ assert_equal ( 1 , oauth_calls , "the 401 must reach the OAuth retry instead of being returned as a response" )
2032+ assert_equal ( "session-after-retry" , oauth_client . session_id )
2033+ assert_equal ( "2025-11-25" , oauth_client . protocol_version )
2034+ ensure
2035+ connection_threads . each ( &:kill )
2036+ connection_threads . each { |thread | thread . join ( 1 ) }
2037+ server_thread . kill
2038+ server . close unless server . closed?
2039+ end
2040+ ensure
2041+ WebMock . enable!
2042+ end
2043+
19172044 def test_includes_session_and_protocol_version_headers_after_initialize
19182045 stub_request ( :post , url )
19192046 . to_return (
0 commit comments