@@ -480,6 +480,7 @@ describe('StreamableHTTPClientTransport', () => {
480480 it ( 'should always send specified custom headers' , async ( ) => {
481481 const requestInit = {
482482 headers : {
483+ Authorization : 'Bearer test-token' ,
483484 'X-Custom-Header' : 'CustomValue'
484485 }
485486 } ;
@@ -497,6 +498,7 @@ describe('StreamableHTTPClientTransport', () => {
497498 await transport . start ( ) ;
498499
499500 await transport [ '_startOrAuthSse' ] ( { } ) ;
501+ expect ( ( actualReqInit . headers as Headers ) . get ( 'authorization' ) ) . toBe ( 'Bearer test-token' ) ;
500502 expect ( ( actualReqInit . headers as Headers ) . get ( 'x-custom-header' ) ) . toBe ( 'CustomValue' ) ;
501503
502504 requestInit . headers [ 'X-Custom-Header' ] = 'SecondCustomValue' ;
@@ -510,6 +512,7 @@ describe('StreamableHTTPClientTransport', () => {
510512 it ( 'should always send specified custom headers (Headers class)' , async ( ) => {
511513 const requestInit = {
512514 headers : new Headers ( {
515+ Authorization : 'Bearer test-token' ,
513516 'X-Custom-Header' : 'CustomValue'
514517 } )
515518 } ;
@@ -527,6 +530,7 @@ describe('StreamableHTTPClientTransport', () => {
527530 await transport . start ( ) ;
528531
529532 await transport [ '_startOrAuthSse' ] ( { } ) ;
533+ expect ( ( actualReqInit . headers as Headers ) . get ( 'authorization' ) ) . toBe ( 'Bearer test-token' ) ;
530534 expect ( ( actualReqInit . headers as Headers ) . get ( 'x-custom-header' ) ) . toBe ( 'CustomValue' ) ;
531535
532536 ( requestInit . headers as Headers ) . set ( 'X-Custom-Header' , 'SecondCustomValue' ) ;
@@ -537,6 +541,30 @@ describe('StreamableHTTPClientTransport', () => {
537541 expect ( global . fetch ) . toHaveBeenCalledTimes ( 2 ) ;
538542 } ) ;
539543
544+ it ( 'should always send specified custom headers (array of tuples)' , async ( ) => {
545+ transport = new StreamableHTTPClientTransport ( new URL ( 'http://localhost:1234/mcp' ) , {
546+ requestInit : {
547+ headers : [
548+ [ 'Authorization' , 'Bearer test-token' ] ,
549+ [ 'X-Custom-Header' , 'CustomValue' ]
550+ ]
551+ }
552+ } ) ;
553+
554+ let actualReqInit : RequestInit = { } ;
555+
556+ ( global . fetch as Mock ) . mockImplementation ( async ( _url , reqInit ) => {
557+ actualReqInit = reqInit ;
558+ return new Response ( null , { status : 200 , headers : { 'content-type' : 'text/event-stream' } } ) ;
559+ } ) ;
560+
561+ await transport . start ( ) ;
562+
563+ await transport [ '_startOrAuthSse' ] ( { } ) ;
564+ expect ( ( actualReqInit . headers as Headers ) . get ( 'authorization' ) ) . toBe ( 'Bearer test-token' ) ;
565+ expect ( ( actualReqInit . headers as Headers ) . get ( 'x-custom-header' ) ) . toBe ( 'CustomValue' ) ;
566+ } ) ;
567+
540568 it ( 'should have exponential backoff with configurable maxRetries' , ( ) => {
541569 // This test verifies the maxRetries and backoff calculation directly
542570
@@ -866,6 +894,112 @@ describe('StreamableHTTPClientTransport', () => {
866894 expect ( reconnectHeaders . get ( 'last-event-id' ) ) . toBe ( 'event-123' ) ;
867895 } ) ;
868896
897+ it ( 'should NOT reconnect a POST stream when response was received' , async ( ) => {
898+ // ARRANGE
899+ transport = new StreamableHTTPClientTransport ( new URL ( 'http://localhost:1234/mcp' ) , {
900+ reconnectionOptions : {
901+ initialReconnectionDelay : 10 ,
902+ maxRetries : 1 ,
903+ maxReconnectionDelay : 1000 ,
904+ reconnectionDelayGrowFactor : 1
905+ }
906+ } ) ;
907+
908+ // Create a stream that sends:
909+ // 1. Priming event with ID (enables potential reconnection)
910+ // 2. The actual response (should prevent reconnection)
911+ // 3. Then closes
912+ const streamWithResponse = new ReadableStream ( {
913+ start ( controller ) {
914+ // Priming event with ID
915+ controller . enqueue ( new TextEncoder ( ) . encode ( 'id: priming-123\ndata: \n\n' ) ) ;
916+ // The actual response to the request
917+ controller . enqueue (
918+ new TextEncoder ( ) . encode ( 'id: response-456\ndata: {"jsonrpc":"2.0","result":{"tools":[]},"id":"request-1"}\n\n' )
919+ ) ;
920+ // Stream closes normally
921+ controller . close ( ) ;
922+ }
923+ } ) ;
924+
925+ const fetchMock = global . fetch as Mock ;
926+ fetchMock . mockResolvedValueOnce ( {
927+ ok : true ,
928+ status : 200 ,
929+ headers : new Headers ( { 'content-type' : 'text/event-stream' } ) ,
930+ body : streamWithResponse
931+ } ) ;
932+
933+ const requestMessage : JSONRPCRequest = {
934+ jsonrpc : '2.0' ,
935+ method : 'tools/list' ,
936+ id : 'request-1' ,
937+ params : { }
938+ } ;
939+
940+ // ACT
941+ await transport . start ( ) ;
942+ await transport . send ( requestMessage ) ;
943+ await vi . advanceTimersByTimeAsync ( 50 ) ;
944+
945+ // ASSERT
946+ // THE KEY ASSERTION: Fetch was called ONCE only - no reconnection!
947+ // The response was received, so no need to reconnect.
948+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 ) ;
949+ expect ( fetchMock . mock . calls [ 0 ] [ 1 ] ?. method ) . toBe ( 'POST' ) ;
950+ } ) ;
951+
952+ it ( 'should not attempt reconnection after close() is called' , async ( ) => {
953+ // ARRANGE
954+ transport = new StreamableHTTPClientTransport ( new URL ( 'http://localhost:1234/mcp' ) , {
955+ reconnectionOptions : {
956+ initialReconnectionDelay : 100 ,
957+ maxRetries : 3 ,
958+ maxReconnectionDelay : 1000 ,
959+ reconnectionDelayGrowFactor : 1
960+ }
961+ } ) ;
962+
963+ // Stream with priming event + notification (no response) that closes
964+ // This triggers reconnection scheduling
965+ const streamWithPriming = new ReadableStream ( {
966+ start ( controller ) {
967+ controller . enqueue (
968+ new TextEncoder ( ) . encode ( 'id: event-123\ndata: {"jsonrpc":"2.0","method":"notifications/test","params":{}}\n\n' )
969+ ) ;
970+ controller . close ( ) ;
971+ }
972+ } ) ;
973+
974+ const fetchMock = global . fetch as Mock ;
975+
976+ // POST request returns streaming response
977+ fetchMock . mockResolvedValueOnce ( {
978+ ok : true ,
979+ status : 200 ,
980+ headers : new Headers ( { 'content-type' : 'text/event-stream' } ) ,
981+ body : streamWithPriming
982+ } ) ;
983+
984+ // ACT
985+ await transport . start ( ) ;
986+ await transport . send ( { jsonrpc : '2.0' , method : 'test' , id : '1' , params : { } } ) ;
987+
988+ // Wait a tick to let stream processing complete and schedule reconnection
989+ await vi . advanceTimersByTimeAsync ( 10 ) ;
990+
991+ // Now close() - reconnection timeout is pending (scheduled for 100ms)
992+ await transport . close ( ) ;
993+
994+ // Advance past reconnection delay
995+ await vi . advanceTimersByTimeAsync ( 200 ) ;
996+
997+ // ASSERT
998+ // Only 1 call: the initial POST. No reconnection attempts after close().
999+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 ) ;
1000+ expect ( fetchMock . mock . calls [ 0 ] [ 1 ] ?. method ) . toBe ( 'POST' ) ;
1001+ } ) ;
1002+
8691003 it ( 'should not throw JSON parse error on priming events with empty data' , async ( ) => {
8701004 transport = new StreamableHTTPClientTransport ( new URL ( 'http://localhost:1234/mcp' ) ) ;
8711005
0 commit comments