@@ -958,4 +958,148 @@ describe('MCP Tool Execution', () => {
958958 expect ( result . error ) . toContain ( 'Network error' )
959959 expect ( result . timing ) . toBeDefined ( )
960960 } )
961+
962+ describe ( 'Tool request retries' , ( ) => {
963+ function makeJsonResponse (
964+ status : number ,
965+ body : unknown ,
966+ extraHeaders ?: Record < string , string >
967+ ) : any {
968+ const headers = new Headers ( { 'content-type' : 'application/json' , ...( extraHeaders ?? { } ) } )
969+ return {
970+ ok : status >= 200 && status < 300 ,
971+ status,
972+ statusText : status >= 200 && status < 300 ? 'OK' : 'Error' ,
973+ headers,
974+ json : ( ) => Promise . resolve ( body ) ,
975+ text : ( ) => Promise . resolve ( typeof body === 'string' ? body : JSON . stringify ( body ) ) ,
976+ arrayBuffer : ( ) => Promise . resolve ( new ArrayBuffer ( 0 ) ) ,
977+ blob : ( ) => Promise . resolve ( new Blob ( ) ) ,
978+ }
979+ }
980+
981+ it ( 'retries on 5xx responses for http_request' , async ( ) => {
982+ global . fetch = Object . assign (
983+ vi
984+ . fn ( )
985+ . mockResolvedValueOnce ( makeJsonResponse ( 500 , { error : 'nope' } ) )
986+ . mockResolvedValueOnce ( makeJsonResponse ( 200 , { ok : true } ) ) ,
987+ { preconnect : vi . fn ( ) }
988+ ) as typeof fetch
989+
990+ const result = await executeTool ( 'http_request' , {
991+ url : '/api/test' ,
992+ method : 'GET' ,
993+ retries : 2 ,
994+ retryDelayMs : 0 ,
995+ retryMaxDelayMs : 0 ,
996+ } )
997+
998+ expect ( global . fetch ) . toHaveBeenCalledTimes ( 2 )
999+ expect ( result . success ) . toBe ( true )
1000+ expect ( ( result . output as any ) . status ) . toBe ( 200 )
1001+ } )
1002+
1003+ it ( 'stops retrying after max attempts for http_request' , async ( ) => {
1004+ global . fetch = Object . assign (
1005+ vi . fn ( ) . mockResolvedValue ( makeJsonResponse ( 502 , { error : 'bad gateway' } ) ) ,
1006+ { preconnect : vi . fn ( ) }
1007+ ) as typeof fetch
1008+
1009+ const result = await executeTool ( 'http_request' , {
1010+ url : '/api/test' ,
1011+ method : 'GET' ,
1012+ retries : 2 ,
1013+ retryDelayMs : 0 ,
1014+ retryMaxDelayMs : 0 ,
1015+ } )
1016+
1017+ expect ( global . fetch ) . toHaveBeenCalledTimes ( 3 )
1018+ expect ( result . success ) . toBe ( false )
1019+ } )
1020+
1021+ it ( 'does not retry on 4xx responses for http_request' , async ( ) => {
1022+ global . fetch = Object . assign (
1023+ vi . fn ( ) . mockResolvedValue ( makeJsonResponse ( 400 , { error : 'bad request' } ) ) ,
1024+ { preconnect : vi . fn ( ) }
1025+ ) as typeof fetch
1026+
1027+ const result = await executeTool ( 'http_request' , {
1028+ url : '/api/test' ,
1029+ method : 'GET' ,
1030+ retries : 5 ,
1031+ retryDelayMs : 0 ,
1032+ retryMaxDelayMs : 0 ,
1033+ } )
1034+
1035+ expect ( global . fetch ) . toHaveBeenCalledTimes ( 1 )
1036+ expect ( result . success ) . toBe ( false )
1037+ } )
1038+
1039+ it ( 'does not retry POST by default (non-idempotent)' , async ( ) => {
1040+ global . fetch = Object . assign (
1041+ vi
1042+ . fn ( )
1043+ . mockResolvedValueOnce ( makeJsonResponse ( 500 , { error : 'nope' } ) )
1044+ . mockResolvedValueOnce ( makeJsonResponse ( 200 , { ok : true } ) ) ,
1045+ { preconnect : vi . fn ( ) }
1046+ ) as typeof fetch
1047+
1048+ const result = await executeTool ( 'http_request' , {
1049+ url : '/api/test' ,
1050+ method : 'POST' ,
1051+ retries : 2 ,
1052+ retryDelayMs : 0 ,
1053+ retryMaxDelayMs : 0 ,
1054+ } )
1055+
1056+ expect ( global . fetch ) . toHaveBeenCalledTimes ( 1 )
1057+ expect ( result . success ) . toBe ( false )
1058+ } )
1059+
1060+ it ( 'retries POST when retryNonIdempotent is enabled' , async ( ) => {
1061+ global . fetch = Object . assign (
1062+ vi
1063+ . fn ( )
1064+ . mockResolvedValueOnce ( makeJsonResponse ( 500 , { error : 'nope' } ) )
1065+ . mockResolvedValueOnce ( makeJsonResponse ( 200 , { ok : true } ) ) ,
1066+ { preconnect : vi . fn ( ) }
1067+ ) as typeof fetch
1068+
1069+ const result = await executeTool ( 'http_request' , {
1070+ url : '/api/test' ,
1071+ method : 'POST' ,
1072+ retries : 1 ,
1073+ retryNonIdempotent : true ,
1074+ retryDelayMs : 0 ,
1075+ retryMaxDelayMs : 0 ,
1076+ } )
1077+
1078+ expect ( global . fetch ) . toHaveBeenCalledTimes ( 2 )
1079+ expect ( result . success ) . toBe ( true )
1080+ expect ( ( result . output as any ) . status ) . toBe ( 200 )
1081+ } )
1082+
1083+ it ( 'retries on timeout errors for http_request' , async ( ) => {
1084+ const abortError = Object . assign ( new Error ( 'Aborted' ) , { name : 'AbortError' } )
1085+ global . fetch = Object . assign (
1086+ vi
1087+ . fn ( )
1088+ . mockRejectedValueOnce ( abortError )
1089+ . mockResolvedValueOnce ( makeJsonResponse ( 200 , { ok : true } ) ) ,
1090+ { preconnect : vi . fn ( ) }
1091+ ) as typeof fetch
1092+
1093+ const result = await executeTool ( 'http_request' , {
1094+ url : '/api/test' ,
1095+ method : 'GET' ,
1096+ retries : 1 ,
1097+ retryDelayMs : 0 ,
1098+ retryMaxDelayMs : 0 ,
1099+ } )
1100+
1101+ expect ( global . fetch ) . toHaveBeenCalledTimes ( 2 )
1102+ expect ( result . success ) . toBe ( true )
1103+ } )
1104+ } )
9611105} )
0 commit comments