From 2025aaf5f2bc2a15f03d0440009a392360a108da Mon Sep 17 00:00:00 2001 From: Zamil Majdy Date: Wed, 11 Feb 2026 23:13:42 +0400 Subject: [PATCH] fix(backend/chat): Preserve full MCP tool output for frontend widgets The SDK CLI truncates large tool results (writing them to disk), which breaks frontend widget rendering (e.g., find_block's block list cards). Stash the full MCP tool output before the SDK sees it, then use the stash in the response adapter so the frontend always receives the complete JSON for proper widget parsing. --- .../api/features/chat/sdk/response_adapter.py | 15 ++++++++-- .../api/features/chat/sdk/tool_adapter.py | 30 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/autogpt_platform/backend/backend/api/features/chat/sdk/response_adapter.py b/autogpt_platform/backend/backend/api/features/chat/sdk/response_adapter.py index 8db556bbd2..f15c27565e 100644 --- a/autogpt_platform/backend/backend/api/features/chat/sdk/response_adapter.py +++ b/autogpt_platform/backend/backend/api/features/chat/sdk/response_adapter.py @@ -34,7 +34,10 @@ from backend.api.features.chat.response_model import ( StreamToolInputStart, StreamToolOutputAvailable, ) -from backend.api.features.chat.sdk.tool_adapter import MCP_TOOL_PREFIX +from backend.api.features.chat.sdk.tool_adapter import ( + MCP_TOOL_PREFIX, + pop_pending_tool_output, +) logger = logging.getLogger(__name__) @@ -114,7 +117,15 @@ class SDKResponseAdapter: if isinstance(block, ToolResultBlock) and block.tool_use_id: tool_info = self.current_tool_calls.get(block.tool_use_id, {}) tool_name = tool_info.get("name", "unknown") - output = _extract_tool_output(block.content) + + # Prefer the stashed full output over the SDK's + # (potentially truncated) ToolResultBlock content. + # The SDK truncates large results, writing them to disk, + # which breaks frontend widget parsing. + output = pop_pending_tool_output(tool_name) or ( + _extract_tool_output(block.content) + ) + responses.append( StreamToolOutputAvailable( toolCallId=block.tool_use_id, diff --git a/autogpt_platform/backend/backend/api/features/chat/sdk/tool_adapter.py b/autogpt_platform/backend/backend/api/features/chat/sdk/tool_adapter.py index 5c536eac1e..c34350bb94 100644 --- a/autogpt_platform/backend/backend/api/features/chat/sdk/tool_adapter.py +++ b/autogpt_platform/backend/backend/api/features/chat/sdk/tool_adapter.py @@ -33,6 +33,13 @@ _current_tool_call_id: ContextVar[str | None] = ContextVar( "current_tool_call_id", default=None ) +# Stash for MCP tool outputs before the SDK potentially truncates them. +# Keyed by tool_name → full output string. Consumed (popped) by the +# response adapter when it builds StreamToolOutputAvailable. +_pending_tool_outputs: ContextVar[dict[str, str]] = ContextVar( + "pending_tool_outputs", default=None # type: ignore[arg-type] +) + def set_execution_context( user_id: str | None, @@ -47,6 +54,7 @@ def set_execution_context( _current_user_id.set(user_id) _current_session.set(session) _current_tool_call_id.set(tool_call_id) + _pending_tool_outputs.set({}) def get_execution_context() -> tuple[str | None, ChatSession | None, str | None]: @@ -58,6 +66,22 @@ def get_execution_context() -> tuple[str | None, ChatSession | None, str | None] ) +def pop_pending_tool_output(tool_name: str) -> str | None: + """Pop and return the stashed full output for *tool_name*. + + The SDK CLI may truncate large tool results (writing them to disk and + replacing the content with a file reference). This stash keeps the + original MCP output so the response adapter can forward it to the + frontend for proper widget rendering. + + Returns ``None`` if nothing was stashed for *tool_name*. + """ + pending = _pending_tool_outputs.get(None) + if pending is None: + return None + return pending.pop(tool_name, None) + + def create_tool_handler(base_tool: BaseTool): """Create an async handler function for a BaseTool. @@ -103,6 +127,12 @@ def create_tool_handler(base_tool: BaseTool): else json.dumps(result.output) ) + # Stash the full output before the SDK potentially truncates it. + # The response adapter will pop this for frontend widget rendering. + pending = _pending_tool_outputs.get(None) + if pending is not None: + pending[base_tool.name] = text + return { "content": [{"type": "text", "text": text}], "isError": not result.success,