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
- Read the '{e}' portion of the message — it names the actual failing cause and fix that directly
- Handle the failing condition inside the tool and raise a deliberate ToolError with a clear message
- Add argument validation in the tool signature so bad calls fail as ValidationError before the body runs
- 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
- Read the message after ': ' — it names the actual cause
- Match tool inputSchema exactly before calling
- Enable masking in production; keep detail only where callers are trusted
- Handle known upstream failure modes inside the tool with clear ToolError messages
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
- Error calling tool {name!r}
- Authorization failed for tool '{tool_name}': not found or no
- Ambiguous app tool {tool_name!r}: {len(matches)} components
- To decorate a classmethod, use @classmethod above @tool. See
- The function '{fn_name}' has '{params[0]}' as its first para
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/5048f294b7fd48d5.
Report an issue: GitHub.