@@ -465,7 +465,7 @@ describe("StreamableHTTPClientTransport", () => {
465
465
466
466
// Verify custom fetch was used
467
467
expect ( customFetch ) . toHaveBeenCalled ( ) ;
468
-
468
+
469
469
// Global fetch should never have been called
470
470
expect ( global . fetch ) . not . toHaveBeenCalled ( ) ;
471
471
} ) ;
@@ -589,32 +589,32 @@ describe("StreamableHTTPClientTransport", () => {
589
589
await expect ( transport . send ( message ) ) . rejects . toThrow ( UnauthorizedError ) ;
590
590
expect ( mockAuthProvider . redirectToAuthorization . mock . calls ) . toHaveLength ( 1 ) ;
591
591
} ) ;
592
-
592
+
593
593
describe ( 'Reconnection Logic' , ( ) => {
594
594
let transport : StreamableHTTPClientTransport ;
595
-
595
+
596
596
// Use fake timers to control setTimeout and make the test instant.
597
597
beforeEach ( ( ) => jest . useFakeTimers ( ) ) ;
598
598
afterEach ( ( ) => jest . useRealTimers ( ) ) ;
599
-
599
+
600
600
it ( 'should reconnect a GET-initiated notification stream that fails' , async ( ) => {
601
601
// ARRANGE
602
602
transport = new StreamableHTTPClientTransport ( new URL ( "http://localhost:1234/mcp" ) , {
603
603
reconnectionOptions : {
604
- initialReconnectionDelay : 10 ,
605
- maxRetries : 1 ,
604
+ initialReconnectionDelay : 10 ,
605
+ maxRetries : 1 ,
606
606
maxReconnectionDelay : 1000 , // Ensure it doesn't retry indefinitely
607
607
reconnectionDelayGrowFactor : 1 // No exponential backoff for simplicity
608
608
}
609
609
} ) ;
610
-
610
+
611
611
const errorSpy = jest . fn ( ) ;
612
612
transport . onerror = errorSpy ;
613
-
613
+
614
614
const failingStream = new ReadableStream ( {
615
615
start ( controller ) { controller . error ( new Error ( "Network failure" ) ) ; }
616
616
} ) ;
617
-
617
+
618
618
const fetchMock = global . fetch as jest . Mock ;
619
619
// Mock the initial GET request, which will fail.
620
620
fetchMock . mockResolvedValueOnce ( {
@@ -628,13 +628,13 @@ describe("StreamableHTTPClientTransport", () => {
628
628
headers : new Headers ( { "content-type" : "text/event-stream" } ) ,
629
629
body : new ReadableStream ( ) ,
630
630
} ) ;
631
-
631
+
632
632
// ACT
633
633
await transport . start ( ) ;
634
634
// Trigger the GET stream directly using the internal method for a clean test.
635
635
await transport [ "_startOrAuthSse" ] ( { } ) ;
636
636
await jest . advanceTimersByTimeAsync ( 20 ) ; // Trigger reconnection timeout
637
-
637
+
638
638
// ASSERT
639
639
expect ( errorSpy ) . toHaveBeenCalledWith ( expect . objectContaining ( {
640
640
message : expect . stringContaining ( 'SSE stream disconnected: Error: Network failure' ) ,
@@ -644,47 +644,47 @@ describe("StreamableHTTPClientTransport", () => {
644
644
expect ( fetchMock . mock . calls [ 0 ] [ 1 ] ?. method ) . toBe ( 'GET' ) ;
645
645
expect ( fetchMock . mock . calls [ 1 ] [ 1 ] ?. method ) . toBe ( 'GET' ) ;
646
646
} ) ;
647
-
647
+
648
648
it ( 'should NOT reconnect a POST-initiated stream that fails' , async ( ) => {
649
649
// ARRANGE
650
650
transport = new StreamableHTTPClientTransport ( new URL ( "http://localhost:1234/mcp" ) , {
651
- reconnectionOptions : {
652
- initialReconnectionDelay : 10 ,
653
- maxRetries : 1 ,
651
+ reconnectionOptions : {
652
+ initialReconnectionDelay : 10 ,
653
+ maxRetries : 1 ,
654
654
maxReconnectionDelay : 1000 , // Ensure it doesn't retry indefinitely
655
655
reconnectionDelayGrowFactor : 1 // No exponential backoff for simplicity
656
656
}
657
657
} ) ;
658
-
658
+
659
659
const errorSpy = jest . fn ( ) ;
660
660
transport . onerror = errorSpy ;
661
-
661
+
662
662
const failingStream = new ReadableStream ( {
663
663
start ( controller ) { controller . error ( new Error ( "Network failure" ) ) ; }
664
664
} ) ;
665
-
665
+
666
666
const fetchMock = global . fetch as jest . Mock ;
667
667
// Mock the POST request. It returns a streaming content-type but a failing body.
668
668
fetchMock . mockResolvedValueOnce ( {
669
669
ok : true , status : 200 ,
670
670
headers : new Headers ( { "content-type" : "text/event-stream" } ) ,
671
671
body : failingStream ,
672
672
} ) ;
673
-
673
+
674
674
// A dummy request message to trigger the `send` logic.
675
675
const requestMessage : JSONRPCRequest = {
676
676
jsonrpc : '2.0' ,
677
677
method : 'long_running_tool' ,
678
678
id : 'request-1' ,
679
679
params : { } ,
680
680
} ;
681
-
681
+
682
682
// ACT
683
683
await transport . start ( ) ;
684
684
// Use the public `send` method to initiate a POST that gets a stream response.
685
685
await transport . send ( requestMessage ) ;
686
686
await jest . advanceTimersByTimeAsync ( 20 ) ; // Advance time to check for reconnections
687
-
687
+
688
688
// ASSERT
689
689
expect ( errorSpy ) . toHaveBeenCalledWith ( expect . objectContaining ( {
690
690
message : expect . stringContaining ( 'SSE stream disconnected: Error: Network failure' ) ,
@@ -718,7 +718,9 @@ describe("StreamableHTTPClientTransport", () => {
718
718
( global . fetch as jest . Mock )
719
719
// Initial connection
720
720
. mockResolvedValueOnce ( unauthedResponse )
721
- // Resource discovery
721
+ // Resource discovery, path aware
722
+ . mockResolvedValueOnce ( unauthedResponse )
723
+ // Resource discovery, root
722
724
. mockResolvedValueOnce ( unauthedResponse )
723
725
// OAuth metadata discovery
724
726
. mockResolvedValueOnce ( {
@@ -770,7 +772,9 @@ describe("StreamableHTTPClientTransport", () => {
770
772
( global . fetch as jest . Mock )
771
773
// Initial connection
772
774
. mockResolvedValueOnce ( unauthedResponse )
773
- // Resource discovery
775
+ // Resource discovery, path aware
776
+ . mockResolvedValueOnce ( unauthedResponse )
777
+ // Resource discovery, root
774
778
. mockResolvedValueOnce ( unauthedResponse )
775
779
// OAuth metadata discovery
776
780
. mockResolvedValueOnce ( {
@@ -822,7 +826,9 @@ describe("StreamableHTTPClientTransport", () => {
822
826
( global . fetch as jest . Mock )
823
827
// Initial connection
824
828
. mockResolvedValueOnce ( unauthedResponse )
825
- // Resource discovery
829
+ // Resource discovery, path aware
830
+ . mockResolvedValueOnce ( unauthedResponse )
831
+ // Resource discovery, root
826
832
. mockResolvedValueOnce ( unauthedResponse )
827
833
// OAuth metadata discovery
828
834
. mockResolvedValueOnce ( {
@@ -888,7 +894,7 @@ describe("StreamableHTTPClientTransport", () => {
888
894
ok : false ,
889
895
status : 404
890
896
} ) ;
891
-
897
+
892
898
// Create transport instance
893
899
transport = new StreamableHTTPClientTransport ( new URL ( "http://localhost:1234/mcp" ) , {
894
900
authProvider : mockAuthProvider ,
@@ -901,14 +907,14 @@ describe("StreamableHTTPClientTransport", () => {
901
907
902
908
// Verify custom fetch was used
903
909
expect ( customFetch ) . toHaveBeenCalled ( ) ;
904
-
910
+
905
911
// Verify specific OAuth endpoints were called with custom fetch
906
912
const customFetchCalls = customFetch . mock . calls ;
907
913
const callUrls = customFetchCalls . map ( ( [ url ] ) => url . toString ( ) ) ;
908
-
914
+
909
915
// Should have called resource metadata discovery
910
916
expect ( callUrls . some ( url => url . includes ( '/.well-known/oauth-protected-resource' ) ) ) . toBe ( true ) ;
911
-
917
+
912
918
// Should have called OAuth authorization server metadata discovery
913
919
expect ( callUrls . some ( url => url . includes ( '/.well-known/oauth-authorization-server' ) ) ) . toBe ( true ) ;
914
920
@@ -966,19 +972,19 @@ describe("StreamableHTTPClientTransport", () => {
966
972
967
973
// Verify custom fetch was used
968
974
expect ( customFetch ) . toHaveBeenCalled ( ) ;
969
-
975
+
970
976
// Verify specific OAuth endpoints were called with custom fetch
971
977
const customFetchCalls = customFetch . mock . calls ;
972
978
const callUrls = customFetchCalls . map ( ( [ url ] ) => url . toString ( ) ) ;
973
-
979
+
974
980
// Should have called resource metadata discovery
975
981
expect ( callUrls . some ( url => url . includes ( '/.well-known/oauth-protected-resource' ) ) ) . toBe ( true ) ;
976
-
982
+
977
983
// Should have called OAuth authorization server metadata discovery
978
984
expect ( callUrls . some ( url => url . includes ( '/.well-known/oauth-authorization-server' ) ) ) . toBe ( true ) ;
979
985
980
986
// Should have called token endpoint for authorization code exchange
981
- const tokenCalls = customFetchCalls . filter ( ( [ url , options ] ) =>
987
+ const tokenCalls = customFetchCalls . filter ( ( [ url , options ] ) =>
982
988
url . toString ( ) . includes ( '/token' ) && options ?. method === "POST"
983
989
) ;
984
990
expect ( tokenCalls . length ) . toBeGreaterThan ( 0 ) ;
0 commit comments