UAT: MCPToolResult.data typed as dict[str, Any] but MCP tools/call response content is an array — type mismatch causes silent data loss #2576

Closed
opened 2026-04-03 18:58:32 +00:00 by freemo · 1 comment
Owner

Bug Report

Feature Area

Standards Alignment — MCP (Model Context Protocol) / Tool Invocation

What Was Tested

Code-level analysis of src/cleveragents/mcp/adapter.py — the MCPToolResult model and invoke() method's result handling.

Expected Behavior (from spec)

The MCP specification defines the tools/call response as:

{
  "content": [
    {"type": "text", "text": "result text"},
    {"type": "image", "data": "base64...", "mimeType": "image/png"},
    {"type": "resource", "resource": {...}}
  ],
  "isError": false
}

The content field is an array of content items, each with a type field and type-specific fields (text, data, mimeType, resource).

Actual Behavior (from code)

In src/cleveragents/mcp/adapter.py:

MCPToolResult.data is typed as dict[str, Any]:

class MCPToolResult(BaseModel):
    success: bool
    data: dict[str, Any] = Field(default_factory=dict, description="Result payload")
    error: str | None = None
    duration_ms: float = 0.0

invoke() assigns the content array directly to data:

return MCPToolResult(
    success=True,
    data=result.get("content", result),  # ← content is a list, not a dict!
    duration_ms=elapsed,
)

When result["content"] is a list (as per MCP spec), data=result.get("content", result) assigns a list to a field typed as dict[str, Any]. Pydantic will either coerce this (losing type safety) or raise a validation error.

Additionally, the error handling for isError is incomplete:

if result.get("isError"):
    return MCPToolResult(
        success=False,
        error=f"MCP server error: {result.get('error', 'unknown error')}",
        # ← MCP spec uses result["content"] for error details, not result["error"]
    )

Per MCP spec, when isError=true, the error details are in content[0].text, not in a top-level error field.

Impact

  • Type mismatch: MCPToolResult.data receives a list when MCP returns content, but is typed as dict. This breaks any downstream code that treats data as a dict.
  • Silent data loss: When result.get("content", result) falls back to result (the full response dict), the actual content is lost.
  • Error details lost: When isError=true, the error message is read from result.get('error') which doesn't exist per MCP spec — the error text is in content[0].text.
  • Multi-content responses broken: MCP tools can return multiple content items (text + image). The dict type cannot represent this.

Code Location

  • src/cleveragents/mcp/adapter.pyMCPToolResult.data: dict[str, Any] and invoke() method (lines ~516-525)

Steps to Reproduce

# Simulate an MCP tools/call response with text content:
mcp_response = {
    "content": [{"type": "text", "text": "Hello from MCP tool"}],
    "isError": False
}
# The invoke() method does:
data = mcp_response.get("content", mcp_response)
# data = [{"type": "text", "text": "Hello from MCP tool"}]  ← a list, not a dict
result = MCPToolResult(success=True, data=data)
# Pydantic may coerce or raise; either way, data is not a proper dict

Fix Required

  1. Change MCPToolResult.data to list[dict[str, Any]] | dict[str, Any] or create a proper MCPContent model.
  2. Fix invoke() to properly extract content:
    content = result.get("content", [])
    return MCPToolResult(success=True, data={"content": content}, duration_ms=elapsed)
    
  3. Fix error handling to read error text from content[0]["text"] when isError=true.

Automated by CleverAgents Bot
Supervisor: UAT Testing | Agent: ca-uat-tester

## Bug Report ### Feature Area Standards Alignment — MCP (Model Context Protocol) / Tool Invocation ### What Was Tested Code-level analysis of `src/cleveragents/mcp/adapter.py` — the `MCPToolResult` model and `invoke()` method's result handling. ### Expected Behavior (from spec) The MCP specification defines the `tools/call` response as: ```json { "content": [ {"type": "text", "text": "result text"}, {"type": "image", "data": "base64...", "mimeType": "image/png"}, {"type": "resource", "resource": {...}} ], "isError": false } ``` The `content` field is an **array** of content items, each with a `type` field and type-specific fields (`text`, `data`, `mimeType`, `resource`). ### Actual Behavior (from code) In `src/cleveragents/mcp/adapter.py`: **`MCPToolResult.data` is typed as `dict[str, Any]`:** ```python class MCPToolResult(BaseModel): success: bool data: dict[str, Any] = Field(default_factory=dict, description="Result payload") error: str | None = None duration_ms: float = 0.0 ``` **`invoke()` assigns the content array directly to `data`:** ```python return MCPToolResult( success=True, data=result.get("content", result), # ← content is a list, not a dict! duration_ms=elapsed, ) ``` When `result["content"]` is a list (as per MCP spec), `data=result.get("content", result)` assigns a `list` to a field typed as `dict[str, Any]`. Pydantic will either coerce this (losing type safety) or raise a validation error. Additionally, the error handling for `isError` is incomplete: ```python if result.get("isError"): return MCPToolResult( success=False, error=f"MCP server error: {result.get('error', 'unknown error')}", # ← MCP spec uses result["content"] for error details, not result["error"] ) ``` Per MCP spec, when `isError=true`, the error details are in `content[0].text`, not in a top-level `error` field. ### Impact - **Type mismatch**: `MCPToolResult.data` receives a `list` when MCP returns content, but is typed as `dict`. This breaks any downstream code that treats `data` as a dict. - **Silent data loss**: When `result.get("content", result)` falls back to `result` (the full response dict), the actual content is lost. - **Error details lost**: When `isError=true`, the error message is read from `result.get('error')` which doesn't exist per MCP spec — the error text is in `content[0].text`. - **Multi-content responses broken**: MCP tools can return multiple content items (text + image). The dict type cannot represent this. ### Code Location - `src/cleveragents/mcp/adapter.py` — `MCPToolResult.data: dict[str, Any]` and `invoke()` method (lines ~516-525) ### Steps to Reproduce ```python # Simulate an MCP tools/call response with text content: mcp_response = { "content": [{"type": "text", "text": "Hello from MCP tool"}], "isError": False } # The invoke() method does: data = mcp_response.get("content", mcp_response) # data = [{"type": "text", "text": "Hello from MCP tool"}] ← a list, not a dict result = MCPToolResult(success=True, data=data) # Pydantic may coerce or raise; either way, data is not a proper dict ``` ### Fix Required 1. Change `MCPToolResult.data` to `list[dict[str, Any]] | dict[str, Any]` or create a proper `MCPContent` model. 2. Fix `invoke()` to properly extract content: ```python content = result.get("content", []) return MCPToolResult(success=True, data={"content": content}, duration_ms=elapsed) ``` 3. Fix error handling to read error text from `content[0]["text"]` when `isError=true`. --- **Automated by CleverAgents Bot** Supervisor: UAT Testing | Agent: ca-uat-tester
Author
Owner

Closing as duplicate of #2152. Both issues describe the same bug: MCPToolResult.data typed as dict[str, Any] but MCP protocol returns content as an array. Issue #2152 was filed first.


Automated by CleverAgents Bot
Supervisor: Backlog Grooming | Agent: ca-backlog-groomer

Closing as duplicate of #2152. Both issues describe the same bug: `MCPToolResult.data` typed as `dict[str, Any]` but MCP protocol returns `content` as an array. Issue #2152 was filed first. --- **Automated by CleverAgents Bot** Supervisor: Backlog Grooming | Agent: ca-backlog-groomer
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#2576
No description provided.