Skip to content

[Bug] MCP: CallToolResult.structuredContent is silently dropped by the tool wrapper #7686

@wjshamblin

Description

@wjshamblin

Description

Summary

agno.tools.mcp.MCPTools discards the structuredContent field of MCP CallToolResult when converting the response into Agno's internal ToolResult. This makes it impossible for agents, tool hooks, or downstream consumers (notably AG-UI / MCP Apps clients) to access the structured payload that MCP servers return alongside the textual content.

This is the same shape of bug as #7658 (which covers meta/_meta); both fields are dropped at the same call site for the same reason.

Why it matters

The MCP spec defines structuredContent as the canonical channel for machine-readable tool output: rendered UI trees (MCP Apps / Prefab generative UI), validated objects, or anything the server wants the client to consume programmatically rather than as text. The textual content is for the LLM; structuredContent is for the client app.

For MCP Apps specifically, the rendered UI JSON tree lives in structuredContent. When this is dropped, MCP Apps clients (CopilotKit, AG-UI hosts, etc.) cannot render the UI without a separate, redundant tool call to re-fetch the result.

Steps to Reproduce

Server (FastMCP, with app=True returning structured content):

from fastmcp import FastMCP

mcp = FastMCP("repro")

@mcp.tool
def get_dashboard(symbol: str) -> dict:
    # In real MCP Apps usage this would be a Prefab component tree;
    # any non-trivial dict reproduces the drop.
    return {"chart": {"type": "bar", "data": [1, 2, 3]}}

if __name__ == "__main__":
    mcp.run(transport="http", port=8765)

Client:

import asyncio, inspect
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools.mcp import MCPTools

async def dump(fn_name, fn_call, args):
    result = (
        await fn_call(**args)
        if inspect.iscoroutinefunction(fn_call)
        else fn_call(**args)
    )
    print(type(result).__name__,
          "structured_content =",
          getattr(result, "structured_content", "<missing>"))
    return result

async def main():
    async with MCPTools(
        transport="streamable-http",
        url="http://127.0.0.1:8765/mcp",
    ) as t:
        agent = Agent(
            model=OpenAIChat(id="gpt-4o-mini"),
            tools=[t],
            tool_hooks=[dump],
        )
        async for _ in agent.arun(
            "call get_dashboard with symbol=ACME",
            stream=True,
            stream_events=True,
        ):
            pass

asyncio.run(main())

Expected print: ToolResult structured_content = {'chart': {...}}
Actual print: ToolResult structured_content = <missing>

Expected Behavior

Downstream consumers (tool hooks, AG-UI emitters, MCP Apps activity renderers) should be able to read structured_content from the ToolResult.

Actual Behavior

In libs/agno/agno/utils/mcp.py, the call_tool wrapper:

result: CallToolResult = await active_session.call_tool(tool_name, kwargs)
# ... iterates result.content into response_str / images / ...
return ToolResult(
    content=response_str.strip(),
    images=images if images else None,
)

result.structuredContent is never read; it's silently dropped. ToolResult itself (in libs/agno/agno/tools/function.py) carries only content, images, videos, audios, files — no structured_content.

Possible Solutions

Same shape of fix as proposed for _meta in #7658, ~3 lines:

  1. Add structured_content: Optional[Dict[str, Any]] = None to ToolResult in libs/agno/agno/tools/function.py.
  2. Pass structured_content=result.structuredContent when constructing ToolResult in libs/agno/agno/utils/mcp.py.

Backward compatible (default None; existing consumers unaffected). Could be bundled with the _meta fix in #7658 since they live in the same call site.

Additional Context

We hit this building an MCP-Apps generative-UI flow on top of Agno → AG-UI → CopilotKit. The Prefab UI tree sits in structuredContent and was being lost at this layer. We've worked around it by re-invoking the tool from a downstream emitter to recover the field, which is wasteful — the original call already had the data.

Related: ag-ui-protocol/ag-ui#918 (closed) and the broader question of whether ToolCallResultEvent should carry structured content in AG-UI itself; that's a separate AG-UI-side issue, but the Agno-side fix is needed regardless.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions