{"record":{"id":"ab16b142e2c2d645","repo":"PrefectHQ/fastmcp","slug":"error-calling-tool-name-r","errorCode":null,"errorMessage":"Error calling tool {name!r}","messagePattern":"Error calling tool (.+?)","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/server.py","lineNumber":1554,"sourceCode":"                            \"Tool %r requires a client capability the client did \"\n                            \"not declare\",\n                            name,\n                        )\n                        raise\n                    logger.exception(f\"Error calling tool {name!r}\")\n                    # Handle actionable errors that should reach the LLM\n                    # even when masking is enabled\n                    if get_http_status_code(e) == 429:\n                        raise ToolError(\n                            \"Rate limited by upstream API, please retry later\"\n                        ) from e\n                    if is_timeout_error(e):\n                        raise ToolError(\n                            \"Upstream request timed out, please retry\"\n                        ) from e\n                    # Standard masking logic\n                    if self._mask_error_details:\n                        raise ToolError(f\"Error calling tool {name!r}\") from e\n                    raise ToolError(f\"Error calling tool {name!r}: {e}\") from e\n\n    async def read_resource(\n        self,\n        uri: str,\n        *,\n        version: VersionSpec | None = None,\n        run_middleware: bool = True,\n    ) -> ResourceResult:\n        \"\"\"Read a resource by URI.\n\n        This is the public API for reading resources. By default, middleware is applied.\n        Checks concrete resources first, then templates.\n\n        Args:\n            uri: The resource URI\n            version: Specific version to read. If None, reads highest version.\n            run_middleware: If True (default), apply the middleware chain.","sourceCodeStart":1536,"sourceCodeEnd":1572,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/server.py#L1536-L1572","documentation":"When a tool raises an unexpected exception and the server has error masking enabled (_mask_error_details=True), call_tool raises ToolError(f'Error calling tool {name!r}') with the real cause hidden but chained (from e). This protects internal details (stack traces, messages, URLs) from reaching the client while still logging the full exception server-side.","triggerScenarios":"Any unhandled exception inside a tool handler while error masking is on — a bug in the tool function, an unhandled upstream error that is neither 429 nor a timeout, a TypeError from bad internal state.","commonSituations":"Production deployments with masking enabled; debugging why a tool 'mysteriously' fails — the client only sees the generic message; unexpected input shapes slipping past validation.","solutions":["Check the server logs — logger.exception records the real traceback","Temporarily disable error masking (mask_error_details=False) in a dev environment to see the underlying message","Fix the bug in the tool handler that raised the original exception","Add explicit handling/validation in the tool for the failing input","Raise FastMCPError/ToolError inside the tool deliberately — these pass through unmasked"],"exampleFix":"// before: unexpected crash leaks nothing and helps no one\ndef my_tool(q: str) -> str:\n    return upstream.call(q)['result']\n// after: controlled error with safe detail\ndef my_tool(q: str) -> str:\n    try:\n        return upstream.call(q)['result']\n    except upstream.UpstreamError as e:\n        raise ToolError(f'Upstream lookup failed: {e}') from e","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"def is_masked_tool_error(e: BaseException) -> bool:\n    return isinstance(e, ToolError) and str(e).startswith(\"Error calling tool\")","tryCatchPattern":"try:\n    result = await client.call_tool('my_tool', args)\nexcept ToolError as e:\n    logger.error('tool failed (masked): %s', e)\n    result = degraded_response()","preventionTips":["Check server logs for the real traceback — masking hides it from clients","Test tools with masking off in dev so failures are visible early","Validate inputs in the tool signature to catch bad calls as ValidationError","Raise deliberate ToolError/FastMCPError inside tools for expected failures"],"tags":["error-masking","tools","debugging"],"backgroundTag":"error-details-masked","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}