PrefectHQ/fastmcp · error · MCPError

-32000

-32000

Error message

Tool '{self.name}' execution timed out after {self.timeout}s

What it means

When a tool execution exceeds its configured timeout, _run_body catches the anyio TimeoutError and re-raises it as an MCPError with code -32000 and a message naming the tool and the timeout value. This turns a low-level cancellation into a protocol-level error clients can recognize.

Solutions

  1. Increase tool.timeout to accommodate realistic execution time
  2. Optimize the tool body (cache, pagination, async I/O instead of blocking calls)
  3. Run the call with task=True as a background task for long-running operations
  4. Handle MCPError code -32000 client-side with retry/backoff if the operation is transiently slow

Example fix

// before
FunctionTool.from_function(slow_api_call, timeout=2.0)
// after
FunctionTool.from_function(slow_api_call, timeout=30.0)
// or for long ops:
await client.call_tool("slow_api_call", {...}, task=True)
Defensive patterns

Strategy: try-catch

Validate before calling

# before calling, ensure the operation fits the budget
import time
t0 = time.monotonic()
probe = quick_health_check()  # optional precheck
if time.monotonic() - t0 > tool.timeout * 0.5:
    raise RuntimeError('downstream too slow for tool timeout')

Try / catch

from mcp.types import McpError
try:
    result = await client.call_tool('my_tool', args)
except McpError as e:
    if e.error.code == -32000 and 'timed out' in e.error.message:
        result = await retry_with_backoff(lambda: client.call_tool('my_tool', args))

Prevention

When it happens

Trigger: Calling tool.run()/calling the tool with arguments whose execution time exceeds tool.timeout; sync functions dispatched to a worker thread (or async functions) that block or take longer than the configured timeout.

Common situations: Slow downstream HTTP calls or DB queries inside a tool; large inputs causing long computation; timeout set optimistically low; forgetting the suggested task=True pattern for long-running work.

Understand the failure class

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/f934a1f35cba4626. Report an issue: GitHub.

Appendix: source

Thrown at fastmcp_slim/fastmcp/tools/function_tool.py:425

        arguments: dict[str, Any],
        *,
        strict: bool,
    ) -> Any:
        """Validate arguments and execute the body, applying any timeout."""
        try:
            if self.timeout is not None:
                try:
                    with anyio.fail_after(self.timeout):
                        result = await self._execute(
                            type_adapter, exec_is_async, arguments, strict=strict
                        )
                except TimeoutError:
                    logger.warning(
                        f"Tool '{self.name}' timed out after {self.timeout}s. "
                        f"Consider using task=True for long-running operations. "
                        f"See https://gofastmcp.com/servers/tasks"
                    )
                    raise MCPError(
                        code=-32000,
                        message=f"Tool '{self.name}' execution timed out after {self.timeout}s",
                    ) from None
            else:
                result = await self._execute(
                    type_adapter, exec_is_async, arguments, strict=strict
                )
        except PydanticValidationError as e:
            # Body errors are re-raised as _ToolBodyError, so a bare pydantic
            # ValidationError here is an argument-validation failure (a bad call).
            # Convert it to fastmcp's ValidationError so the middleware chain and
            # downstream error taxonomy (e.g. Sentry filters) can treat it as a
            # client error rather than a server bug.
            raise ValidationError(str(e), log_level=logging.WARNING) from e
        except _ToolBodyError as e:
            # The tool's own body raised a pydantic ValidationError. Surface the
            # original so it is treated as a server-side error, hiding the
            # internal sentinel while preserving the error's own chained cause.

View on GitHub (pinned to 1f02114297)