@@ -31,6 +31,10 @@ use reqwest::Error as ReqwestError;
3131use tokio:: io:: AsyncBufReadExt ;
3232use tokio:: io:: BufReader ;
3333use tokio:: process:: Command ;
34+
35+ const INITIALIZE_RETRY_BASE_DELAY_MS : u64 = 200 ;
36+ const INITIALIZE_RETRY_MAX_DELAY_MS : u64 = 1_600 ;
37+ const INITIALIZE_MAX_RETRIES : usize = 3 ;
3438use tokio:: sync:: Mutex ;
3539use tokio:: time;
3640use tracing:: info;
@@ -183,9 +187,9 @@ impl RmcpClient {
183187 Ok ( service) => break service,
184188 Err ( err) => {
185189 let err = annotate_phase_error ( Phase :: Initialize , err) ;
186- if should_retry_initialize ( & err, attempt) {
190+ if let Some ( delay ) = retry_delay_for_initialize ( & err, attempt) {
187191 attempt += 1 ;
188- time:: sleep ( Duration :: from_millis ( 250 ) ) . await ;
192+ time:: sleep ( delay ) . await ;
189193 transport = build_streamable_http_transport ( & url, bearer_token. as_deref ( ) ) ;
190194 continue ;
191195 }
@@ -299,12 +303,12 @@ fn annotate_phase_error(phase: Phase, err: anyhow::Error) -> anyhow::Error {
299303 err. context ( format ! ( "phase={}" , phase. as_str( ) ) )
300304}
301305
302- fn should_retry_initialize ( err : & anyhow:: Error , attempt : usize ) -> bool {
303- if attempt != 0 {
304- return false ;
306+ fn retry_delay_for_initialize ( err : & anyhow:: Error , attempt : usize ) -> Option < Duration > {
307+ if attempt >= INITIALIZE_MAX_RETRIES {
308+ return None ;
305309 }
306310
307- for source in err. chain ( ) {
311+ let retryable = err. chain ( ) . any ( |source| {
308312 if let Some ( reqwest_err) = source. downcast_ref :: < ReqwestError > ( ) {
309313 if reqwest_err. is_timeout ( ) || reqwest_err. is_connect ( ) {
310314 return true ;
@@ -314,14 +318,32 @@ fn should_retry_initialize(err: &anyhow::Error, attempt: usize) -> bool {
314318 if let Some ( io_err) = source. downcast_ref :: < io:: Error > ( ) {
315319 if matches ! (
316320 io_err. kind( ) ,
317- io:: ErrorKind :: TimedOut | io:: ErrorKind :: ConnectionRefused
321+ io:: ErrorKind :: TimedOut
322+ | io:: ErrorKind :: ConnectionRefused
323+ | io:: ErrorKind :: ConnectionReset
324+ | io:: ErrorKind :: BrokenPipe
325+ | io:: ErrorKind :: NotConnected
326+ | io:: ErrorKind :: WouldBlock ,
318327 ) {
319328 return true ;
320329 }
321330 }
331+
332+ source. downcast_ref :: < HandshakeTimeoutError > ( ) . is_some ( )
333+ } ) ;
334+
335+ if retryable {
336+ Some ( initialize_retry_delay ( attempt) )
337+ } else {
338+ None
322339 }
340+ }
323341
324- false
342+ fn initialize_retry_delay ( attempt : usize ) -> Duration {
343+ let capped_attempt = attempt. min ( 4 ) ;
344+ let multiplier = 1u64 << capped_attempt;
345+ let delay = INITIALIZE_RETRY_BASE_DELAY_MS . saturating_mul ( multiplier) ;
346+ Duration :: from_millis ( delay. min ( INITIALIZE_RETRY_MAX_DELAY_MS ) )
325347}
326348
327349fn build_streamable_http_transport (
@@ -365,6 +387,7 @@ impl StdError for HandshakeTimeoutError {}
365387mod tests {
366388 use super :: * ;
367389 use anyhow:: anyhow;
390+ use std:: time:: Duration ;
368391
369392 #[ test]
370393 fn mcp_schema_version_is_well_formed ( ) {
@@ -388,12 +411,41 @@ mod tests {
388411 }
389412
390413 #[ test]
391- fn should_retry_initialize_detects_transient_errors ( ) {
392- let timeout_err = anyhow ! ( io:: Error :: new( io:: ErrorKind :: TimedOut , "timed out" ) ) ;
393- assert ! ( should_retry_initialize( & timeout_err, 0 ) ) ;
394- assert ! ( !should_retry_initialize( & timeout_err, 1 ) ) ;
414+ fn retry_delay_for_initialize_detects_transient_errors ( ) {
415+ let timeout_err = annotate_phase_error (
416+ Phase :: Initialize ,
417+ anyhow ! ( io:: Error :: new( io:: ErrorKind :: TimedOut , "timed out" ) ) ,
418+ ) ;
419+ assert_eq ! (
420+ retry_delay_for_initialize( & timeout_err, 0 ) ,
421+ Some ( Duration :: from_millis( INITIALIZE_RETRY_BASE_DELAY_MS ) )
422+ ) ;
423+ assert_eq ! ( retry_delay_for_initialize( & timeout_err, INITIALIZE_MAX_RETRIES ) , None ) ;
424+
425+ let mismatch_err = annotate_phase_error ( Phase :: Initialize , anyhow ! ( "protocol mismatch" ) ) ;
426+ assert_eq ! ( retry_delay_for_initialize( & mismatch_err, 0 ) , None ) ;
427+ }
428+
429+ #[ test]
430+ fn retry_delay_handles_handshake_timeout ( ) {
431+ let err = annotate_phase_error (
432+ Phase :: Initialize ,
433+ handshake_timeout_error ( Duration :: from_secs ( 1 ) ) ,
434+ ) ;
435+ assert ! ( retry_delay_for_initialize( & err, 0 ) . is_some( ) ) ;
436+ }
395437
396- let mismatch_err = anyhow ! ( "protocol mismatch" ) ;
397- assert ! ( !should_retry_initialize( & mismatch_err, 0 ) ) ;
438+ #[ test]
439+ fn initialize_retry_delay_exponential_and_capped ( ) {
440+ let first = initialize_retry_delay ( 0 ) ;
441+ let second = initialize_retry_delay ( 1 ) ;
442+ let capped = initialize_retry_delay ( 10 ) ;
443+
444+ assert_eq ! ( first, Duration :: from_millis( INITIALIZE_RETRY_BASE_DELAY_MS ) ) ;
445+ assert_eq ! ( second, Duration :: from_millis( INITIALIZE_RETRY_BASE_DELAY_MS * 2 ) ) ;
446+ assert_eq ! (
447+ capped,
448+ Duration :: from_millis( INITIALIZE_RETRY_MAX_DELAY_MS )
449+ ) ;
398450 }
399451}
0 commit comments