Description
RetryMiddleware doesn't work for tool calls because FastMCP's call_tool() wraps raw exceptions as ToolError before control returns to the middleware. The middleware's _should_retry() checks isinstance(error, retry_exceptions) but receives ToolError, not the original ConnectionError/TimeoutError.
The ErrorHandlingMiddleware handles this correctly by checking error.__cause__, but RetryMiddleware doesn't.
Reproduction
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.server.middleware.error_handling import RetryMiddleware
call_count = 0
mcp = FastMCP('test')
mcp.add_middleware(RetryMiddleware(max_retries=3, base_delay=0.01, retry_exceptions=(ConnectionError,)))
@mcp.tool
def always_fail() -> str:
global call_count
call_count += 1
raise ConnectionError('connection refused')
async with Client(mcp) as client:
try:
await client.call_tool('always_fail')
except:
pass
print(f'Tool called {call_count} times')
# Prints: 1 (expected 4 with 3 retries)
Root Cause
In call_tool() (server.py line 1169-1185), all non-FastMCPError exceptions are caught and re-raised as ToolError(...) from e. This happens inside the middleware chain, so the middleware receives ToolError with __cause__=ConnectionError. The _should_retry method only checks the top-level error type.
Suggested Fix
Update RetryMiddleware._should_retry to also check error.__cause__:
def _should_retry(self, error: Exception) -> bool:
if isinstance(error, self.retry_exceptions):
return True
if error.__cause__ is not None and isinstance(error.__cause__, self.retry_exceptions):
return True
return False
Note: ErrorHandlingMiddleware._transform_error already handles this correctly (line 92: error_type = type(error.__cause__) if error.__cause__ else type(error)).
Description
RetryMiddlewaredoesn't work for tool calls because FastMCP'scall_tool()wraps raw exceptions asToolErrorbefore control returns to the middleware. The middleware's_should_retry()checksisinstance(error, retry_exceptions)but receivesToolError, not the originalConnectionError/TimeoutError.The
ErrorHandlingMiddlewarehandles this correctly by checkingerror.__cause__, butRetryMiddlewaredoesn't.Reproduction
Root Cause
In
call_tool()(server.py line 1169-1185), all non-FastMCPError exceptions are caught and re-raised asToolError(...) from e. This happens inside the middleware chain, so the middleware receivesToolErrorwith__cause__=ConnectionError. The_should_retrymethod only checks the top-level error type.Suggested Fix
Update
RetryMiddleware._should_retryto also checkerror.__cause__:Note:
ErrorHandlingMiddleware._transform_erroralready handles this correctly (line 92:error_type = type(error.__cause__) if error.__cause__ else type(error)).