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:
- Add
structured_content: Optional[Dict[str, Any]] = None to ToolResult in libs/agno/agno/tools/function.py.
- 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.
Description
Summary
agno.tools.mcp.MCPToolsdiscards thestructuredContentfield of MCPCallToolResultwhen converting the response into Agno's internalToolResult. 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 textualcontent.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
structuredContentas 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 textualcontentis for the LLM;structuredContentis 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=Truereturning structured content):Client:
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_contentfrom theToolResult.Actual Behavior
In
libs/agno/agno/utils/mcp.py, thecall_toolwrapper:result.structuredContentis never read; it's silently dropped.ToolResultitself (inlibs/agno/agno/tools/function.py) carries onlycontent,images,videos,audios,files— nostructured_content.Possible Solutions
Same shape of fix as proposed for
_metain #7658, ~3 lines:structured_content: Optional[Dict[str, Any]] = NonetoToolResultinlibs/agno/agno/tools/function.py.structured_content=result.structuredContentwhen constructingToolResultinlibs/agno/agno/utils/mcp.py.Backward compatible (default
None; existing consumers unaffected). Could be bundled with the_metafix 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
structuredContentand 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
ToolCallResultEventshould carry structured content in AG-UI itself; that's a separate AG-UI-side issue, but the Agno-side fix is needed regardless.