PrefectHQ/fastmcp · error · ToolError

Error calling tool {name!r}: {e}

Error message

Error calling tool {name!r}: {e}

What it means

The unmasked variant of the tool-failure wrapper: with _mask_error_details=False, call_tool raises ToolError(f'Error calling tool {name!r}: {e}') including the original exception's message. This is the development/default mode where the client (and the LLM) can see why the tool failed.

Source

Thrown at fastmcp_slim/fastmcp/server/server.py:1555

                            "not declare",
                            name,
                        )
                        raise
                    logger.exception(f"Error calling tool {name!r}")
                    # Handle actionable errors that should reach the LLM
                    # even when masking is enabled
                    if get_http_status_code(e) == 429:
                        raise ToolError(
                            "Rate limited by upstream API, please retry later"
                        ) from e
                    if is_timeout_error(e):
                        raise ToolError(
                            "Upstream request timed out, please retry"
                        ) from e
                    # Standard masking logic
                    if self._mask_error_details:
                        raise ToolError(f"Error calling tool {name!r}") from e
                    raise ToolError(f"Error calling tool {name!r}: {e}") from e

    async def read_resource(
        self,
        uri: str,
        *,
        version: VersionSpec | None = None,
        run_middleware: bool = True,
    ) -> ResourceResult:
        """Read a resource by URI.

        This is the public API for reading resources. By default, middleware is applied.
        Checks concrete resources first, then templates.

        Args:
            uri: The resource URI
            version: Specific version to read. If None, reads highest version.
            run_middleware: If True (default), apply the middleware chain.
                Set to False when called from middleware to avoid re-applying.

View on GitHub (pinned to 1f02114297)

Solutions

  1. Read the '{e}' portion of the message — it names the actual failing cause and fix that directly
  2. Handle the failing condition inside the tool and raise a deliberate ToolError with a clear message
  3. Add argument validation in the tool signature so bad calls fail as ValidationError before the body runs
  4. Enable masking in production if detailed messages should not reach clients

Example fix

// client-side: capture and inspect the detail
try:
    result = await client.call_tool('my_tool', args)
except ToolError as e:
    print(e)  # Error calling tool 'my_tool': <root cause here>
Defensive patterns

Strategy: try-catch

Validate before calling

sig = await client.get_tool('my_tool')  # or inspect list_tools() inputSchema
assert set(expected_args) <= set(sig.inputSchema.get('properties', {})), 'arg mismatch'

Type guard

def is_tool_error_with_detail(e: BaseException) -> bool:
    return isinstance(e, ToolError) and ': ' in str(e)

Try / catch

try:
    result = await client.call_tool('my_tool', args)
except ToolError as e:
    root_cause = str(e).split(': ', 1)[-1]
    logger.error('tool my_tool failed: %s', root_cause)
    raise

Prevention

When it happens

Trigger: Any unhandled exception escaping tool._run while masking is disabled — bugs, bad arguments that slip past validation, upstream API errors other than 429/timeout — with the underlying message appended after the colon.

Common situations: Local development and testing; self-hosted servers that intentionally expose detailed errors; internal tools where callers are trusted developers.

Related errors


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