UAT: MCPToolResult.data typed as dict[str, Any] but MCP protocol returns content as a list — causes ValidationError with real MCP servers #3822

Open
opened 2026-04-06 06:45:25 +00:00 by freemo · 1 comment
Owner

Metadata

  • Branch: bugfix/mcp-tool-result-data-list-type
  • Commit Message: fix(mcp): change MCPToolResult.data to accept list content from MCP protocol
  • Milestone: (none — backlog)
  • Parent Epic: #397

Backlog note: This issue was discovered during autonomous operation
on milestone v3.8.0. It does not block milestone completion and has been
placed in the backlog for human review and future milestone assignment.

Background and Context

During UAT testing of MCPToolAdapter.invoke(), a type mismatch was discovered between the MCPToolResult.data field type and the actual MCP protocol response format.

The MCP 1.4.0 protocol specifies that tools/call responses return content as a list of content items:

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

The current implementation in src/cleveragents/mcp/adapter.py (line 121-136) defines MCPToolResult.data as dict[str, Any]:

class MCPToolResult(BaseModel):
    data: dict[str, Any] = Field(default_factory=dict, description="Result payload")

The adapter's invoke() method (line 528-532) does:

return MCPToolResult(
    success=True,
    data=result.get("content", result),  # Gets the list!
    duration_ms=elapsed,
)

When result["content"] is a list (standard MCP format), Pydantic raises a ValidationError:

1 validation error for MCPToolResult
data
  Input should be a valid dictionary [type=dict_type, input_value=[{'type': 'text', 'text': 'file contents here'}], input_type=list]

This means MCPToolAdapter.invoke() will crash with a Pydantic ValidationError when used with any real MCP server that returns list-format content in success responses.

Note: The error path (lines 517-526) correctly handles list content for error responses, but the success path does not.

Current Behavior

MCPToolAdapter.invoke() raises a pydantic.ValidationError when the MCP server returns a standard tools/call response with content as a list.

Steps to Reproduce:

from cleveragents.mcp.adapter import MCPToolAdapter, MCPServerConfig, MCPTransport
from typing import Any

class RealMCPStyleTransport(MCPTransport):
    def connect(self, config):
        return {'capabilities': {'tools': True}}
    def call(self, method, params):
        if method == 'tools/list':
            return {'tools': [{'name': 'read_file', 'description': 'Read a file', 'inputSchema': {}}]}
        if method == 'tools/call':
            # Real MCP protocol returns content as a list
            return {'content': [{'type': 'text', 'text': 'file contents here'}]}
        return {}

config = MCPServerConfig(name='test', transport='stdio', command='echo')
adapter = MCPToolAdapter(config=config, transport=RealMCPStyleTransport())
adapter.connect()
adapter.discover_tools()
result = adapter.invoke('read_file', {'path': '/tmp/test.txt'})
# Raises: ValidationError for MCPToolResult.data

Expected Behavior

MCPToolAdapter.invoke() should succeed and return an MCPToolResult with data containing the list of content items returned by the MCP server. The MCPToolResult.data field should accept both list (standard MCP protocol format) and dict (legacy/fallback format).

Acceptance Criteria

  • MCPToolResult.data accepts both list[dict[str, Any]] and dict[str, Any] without raising a ValidationError
  • MCPToolAdapter.invoke() succeeds when an MCP server returns list-format content in its tools/call response
  • The fix is consistent with how the error path already handles list content
  • All existing MCP adapter tests continue to pass

Supporting Information

  • Related issue: #3708 (MCP tool handler signature mismatch — same adapter file)
  • MCP 1.4.0 spec: tools/call response schema defines content as array of content objects
  • Affected file: src/cleveragents/mcp/adapter.py, lines ~121-136 (model) and ~528-532 (invoke success path)

Subtasks

  • Change MCPToolResult.data type from dict[str, Any] to Any (or list[dict[str, Any]] | dict[str, Any])
  • Update the invoke() method success path to handle list content correctly
  • Add a Behave scenario testing invoke() with real MCP list-format content
  • Verify the fix doesn't break existing tests
  • Run nox (all default sessions), fix any errors
  • Verify coverage >= 97% via nox -s coverage_report

Definition of Done

This issue is complete when:

  • MCPToolResult.data accepts both dict and list content without ValidationError
  • MCPToolAdapter.invoke() succeeds when MCP server returns list-format content
  • New Behave scenario covers the list content case
  • All existing MCP tests still pass
  • All nox stages pass
  • Coverage >= 97%
  • A Git commit is created where the first line of the commit message matches the Commit Message in Metadata exactly, followed by a blank line, then additional lines providing relevant details about the implementation.
  • The commit is pushed to the remote on the branch matching the Branch in Metadata exactly.
  • The commit is submitted as a pull request to master, reviewed, and merged before this issue is marked done.

Automated by CleverAgents Bot
Supervisor: UAT Testing | Agent: ca-new-issue-creator

## Metadata - **Branch**: `bugfix/mcp-tool-result-data-list-type` - **Commit Message**: `fix(mcp): change MCPToolResult.data to accept list content from MCP protocol` - **Milestone**: *(none — backlog)* - **Parent Epic**: #397 > **Backlog note:** This issue was discovered during autonomous operation > on milestone v3.8.0. It does not block milestone completion and has been > placed in the backlog for human review and future milestone assignment. ## Background and Context During UAT testing of `MCPToolAdapter.invoke()`, a type mismatch was discovered between the `MCPToolResult.data` field type and the actual MCP protocol response format. **The MCP 1.4.0 protocol** specifies that `tools/call` responses return content as a **list** of content items: ```json { "content": [ {"type": "text", "text": "result text"}, {"type": "image", "data": "base64...", "mimeType": "image/png"} ], "isError": false } ``` **The current implementation** in `src/cleveragents/mcp/adapter.py` (line 121-136) defines `MCPToolResult.data` as `dict[str, Any]`: ```python class MCPToolResult(BaseModel): data: dict[str, Any] = Field(default_factory=dict, description="Result payload") ``` **The adapter's `invoke()` method** (line 528-532) does: ```python return MCPToolResult( success=True, data=result.get("content", result), # Gets the list! duration_ms=elapsed, ) ``` When `result["content"]` is a list (standard MCP format), Pydantic raises a `ValidationError`: ``` 1 validation error for MCPToolResult data Input should be a valid dictionary [type=dict_type, input_value=[{'type': 'text', 'text': 'file contents here'}], input_type=list] ``` This means `MCPToolAdapter.invoke()` will **crash with a Pydantic ValidationError** when used with any real MCP server that returns list-format content in success responses. **Note:** The error path (lines 517-526) correctly handles list content for error responses, but the success path does not. ## Current Behavior `MCPToolAdapter.invoke()` raises a `pydantic.ValidationError` when the MCP server returns a standard `tools/call` response with `content` as a list. **Steps to Reproduce:** ```python from cleveragents.mcp.adapter import MCPToolAdapter, MCPServerConfig, MCPTransport from typing import Any class RealMCPStyleTransport(MCPTransport): def connect(self, config): return {'capabilities': {'tools': True}} def call(self, method, params): if method == 'tools/list': return {'tools': [{'name': 'read_file', 'description': 'Read a file', 'inputSchema': {}}]} if method == 'tools/call': # Real MCP protocol returns content as a list return {'content': [{'type': 'text', 'text': 'file contents here'}]} return {} config = MCPServerConfig(name='test', transport='stdio', command='echo') adapter = MCPToolAdapter(config=config, transport=RealMCPStyleTransport()) adapter.connect() adapter.discover_tools() result = adapter.invoke('read_file', {'path': '/tmp/test.txt'}) # Raises: ValidationError for MCPToolResult.data ``` ## Expected Behavior `MCPToolAdapter.invoke()` should succeed and return an `MCPToolResult` with `data` containing the list of content items returned by the MCP server. The `MCPToolResult.data` field should accept both `list` (standard MCP protocol format) and `dict` (legacy/fallback format). ## Acceptance Criteria - `MCPToolResult.data` accepts both `list[dict[str, Any]]` and `dict[str, Any]` without raising a `ValidationError` - `MCPToolAdapter.invoke()` succeeds when an MCP server returns list-format `content` in its `tools/call` response - The fix is consistent with how the error path already handles list content - All existing MCP adapter tests continue to pass ## Supporting Information - Related issue: #3708 (MCP tool handler signature mismatch — same adapter file) - MCP 1.4.0 spec: `tools/call` response schema defines `content` as `array` of content objects - Affected file: `src/cleveragents/mcp/adapter.py`, lines ~121-136 (model) and ~528-532 (invoke success path) ## Subtasks - [ ] Change `MCPToolResult.data` type from `dict[str, Any]` to `Any` (or `list[dict[str, Any]] | dict[str, Any]`) - [ ] Update the `invoke()` method success path to handle list content correctly - [ ] Add a Behave scenario testing `invoke()` with real MCP list-format content - [ ] Verify the fix doesn't break existing tests - [ ] Run `nox` (all default sessions), fix any errors - [ ] Verify coverage >= 97% via `nox -s coverage_report` ## Definition of Done This issue is complete when: - [ ] `MCPToolResult.data` accepts both dict and list content without `ValidationError` - [ ] `MCPToolAdapter.invoke()` succeeds when MCP server returns list-format content - [ ] New Behave scenario covers the list content case - [ ] All existing MCP tests still pass - All nox stages pass - Coverage >= 97% - A Git commit is created where the **first line** of the commit message matches the Commit Message in Metadata exactly, followed by a blank line, then additional lines providing relevant details about the implementation. - The commit is pushed to the remote on the branch matching the **Branch** in Metadata exactly. - The commit is submitted as a **pull request** to `master`, reviewed, and **merged** before this issue is marked done. --- **Automated by CleverAgents Bot** Supervisor: UAT Testing | Agent: ca-new-issue-creator
Author
Owner

Label compliance fix applied:

  • Removed conflicting labels: Priority/Medium (kept Priority/Backlog), State/In Progress (kept State/Verified)
  • Reason: Issue had two conflicting Priority/* labels and two conflicting State/* labels. State/Verified is more advanced than State/In Progress, and Priority/Backlog is the appropriate default for backlog items.

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

Label compliance fix applied: - Removed conflicting labels: `Priority/Medium` (kept `Priority/Backlog`), `State/In Progress` (kept `State/Verified`) - Reason: Issue had two conflicting Priority/* labels and two conflicting State/* labels. `State/Verified` is more advanced than `State/In Progress`, and `Priority/Backlog` is the appropriate default for backlog items. --- **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.

Blocks
#397 Epic: Server & Autonomy Infrastructure
cleveragents/cleveragents-core
Reference
cleveragents/cleveragents-core#3822
No description provided.