Source code for klea_utils.mcp.tool_result
#!/usr/bin/env python3
"""
Helper to map Klea dict results with an ``error`` field to MCP ``isError``.
File: klea_utils/mcp/tool_result.py
Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""
import json
import logging
from typing import Any
from fastmcp.tools import ToolResult
from mcp.types import TextContent
logger = logging.getLogger(__name__)
[docs]
def to_result(result: dict[str, Any]) -> ToolResult:
"""Return a ``ToolResult`` with ``is_error`` derived from the ``error`` field.
Klea tool implementations are framework-agnostic and signal failure via a
non-empty ``error`` (or ``Error`` for legacy repository tools) string in
their returned dict. The MCP spec requires such failures to be reported
as ``isError: true`` in the ``CallToolResult`` (see
``mcp/types.py:CallToolResult`` and the spec section
``server/tools#Error Handling``); FastMCP only sets this when the tool
explicitly returns ``ToolResult(is_error=True)`` or raises ``ToolError``
(see ``fastmcp/tools/base.py:convert_result`` and
``mcp/server/lowlevel/server.py:_make_error_result``).
This helper bridges the two: it preserves the dict as ``structured_content``
(so LLM remediation keeps full context) while marking ``is_error`` when
the ``error``/``Error`` field is truthy. It intentionally preserves the
structured payload rather than raising ``ToolError``, which would strip it.
:param result: Tool return dict containing at least an ``error`` key;
legacy tools may use ``Error`` (capitalised).
:returns: ``ToolResult`` with ``is_error`` reflecting the error field.
"""
error = result.get("error")
if error is None:
error = result.get("Error", "")
# Treat missing, empty, or whitespace-only as success.
is_error = (
bool(isinstance(error, str) and error.strip())
if isinstance(error, str)
else bool(error)
)
# Non-string truthy errors (unlikely) still count as error.
if not isinstance(error, str) and error:
is_error = True
logger.debug(f"{is_error = }\n{error = }")
# FastMCP serialises dicts via pydantic_core; ensure JSON text is safe.
try:
text = json.dumps(result, ensure_ascii=False)
except (TypeError, ValueError):
text = json.dumps(result, ensure_ascii=False, default=str)
return ToolResult(
content=[TextContent(type="text", text=text)],
structured_content=result,
is_error=is_error,
)