From 76b5bbd9568337a5a6c517b4dfd060bc1188c38e Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Wed, 26 Aug 2026 16:09:08 +0000 Subject: [PATCH] fix(kagent-adk): compact MCP App tool results only when the result carries a UI resource compact_mcp_app_response collapsed every result from a UI-capable tool into a terminal notice, classifying purely from the tool's definition (_meta.ui.resourceUri at ListTools time) rather than the specific call result. A tool that sometimes returns plain data and sometimes renders UI (e.g. an Atlassian search tool with a UI mode) had every result blanked for headless agents (A2A, kagent invoke), even ones with no UI resource at all. Add _result_has_ui_resource, mirroring the existing _meta.ui.resourceUri / _meta["ui/resourceUri"] parsing kagent already uses to classify a tool definition (go/adk/pkg/mcp/mcp_ui.go), applied to the result instead. compact_mcp_app_response now passes a result through unchanged unless it itself declares a UI resource. Scoped to the Python ADK runtime per the issue; go/adk/pkg/agent/mcp_apps.go has the identical gap and is left as a follow-up. Fixes #2519 Signed-off-by: Amir Fathi --- .../kagent-adk/src/kagent/adk/_mcp_apps.py | 22 ++++++++++++++++- .../tests/unittests/test_mcp_apps.py | 24 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/python/packages/kagent-adk/src/kagent/adk/_mcp_apps.py b/python/packages/kagent-adk/src/kagent/adk/_mcp_apps.py index 4ae1f6288..53d25aef6 100644 --- a/python/packages/kagent-adk/src/kagent/adk/_mcp_apps.py +++ b/python/packages/kagent-adk/src/kagent/adk/_mcp_apps.py @@ -54,19 +54,39 @@ def __len__(self) -> int: return len(self._names) +def _result_has_ui_resource(response: dict) -> bool: + """Whether this specific result, not just the tool's definition, carries a UI resource. + + Mirrors the ``_meta.ui.resourceUri`` / ``_meta["ui/resourceUri"]`` parsing already + used to classify a tool's definition (``go/adk/pkg/mcp/mcp_ui.go:parseMCPUIMetadata``). + """ + meta = response.get("_meta") + if not isinstance(meta, dict): + return False + ui = meta.get("ui") + if isinstance(ui, dict) and ui.get("resourceUri"): + return True + return bool(meta.get("ui/resourceUri")) + + def compact_mcp_app_response(response: dict) -> dict: """Rewrite an MCP App tool result (a JSON ``CallToolResult``) for the model. On error, keep the content so the model can diagnose/recover but drop the heavy structured payload. On success, collapse the render payload into a terminal directive so the model stops re-invoking the rendering tool, - preserving ``_meta`` (e.g. resourceUri) for any downstream tooling. + preserving ``_meta`` (e.g. resourceUri) for any downstream tooling. A tool + can be UI-capable by definition yet return a plain result for a given + call; only compact when this result itself carries a UI resource. """ if response.get("isError") is True or response.get("error") is True: compacted = dict(response) compacted.pop("structuredContent", None) return compacted + if not _result_has_ui_resource(response): + return response + compacted: dict = {"content": [{"type": "text", "text": MCP_APP_RENDERED_NOTICE}]} if "_meta" in response: compacted["_meta"] = response["_meta"] diff --git a/python/packages/kagent-adk/tests/unittests/test_mcp_apps.py b/python/packages/kagent-adk/tests/unittests/test_mcp_apps.py index 8b0db4298..9644f66eb 100644 --- a/python/packages/kagent-adk/tests/unittests/test_mcp_apps.py +++ b/python/packages/kagent-adk/tests/unittests/test_mcp_apps.py @@ -31,6 +31,24 @@ def test_compact_success_replaces_payload_with_notice(): assert compacted["_meta"] == response["_meta"] +def test_compact_skips_data_only_result_from_a_ui_capable_tool(): + # A UI-capable tool's result with no UI resource of its own. + response = { + "content": [{"type": "text", "text": '{"issues": [{"key": "GF-3687"}]}'}], + "structuredContent": {"issues": [{"key": "GF-3687"}]}, + "_meta": {}, + } + compacted = compact_mcp_app_response(response) + assert compacted == response + + +def test_compact_skips_result_with_no_meta_at_all(): + # Servers that never set _meta on a call result (as opposed to an empty one). + response = {"content": [{"type": "text", "text": "hello"}], "structuredContent": {"x": 1}} + compacted = compact_mcp_app_response(response) + assert compacted == response + + def test_compact_error_keeps_content_drops_structured(): response = { "content": [{"type": "text", "text": "boom"}], @@ -48,7 +66,11 @@ def test_callback_compacts_only_app_tool_responses(): app_tool_names.add("show-weather-dashboard") callback = make_mcp_app_model_result_callback(app_tool_names) - weather = {"structuredContent": {"temperature": 36}, "content": [{"type": "text", "text": "36C"}]} + weather = { + "structuredContent": {"temperature": 36}, + "content": [{"type": "text", "text": "36C"}], + "_meta": {"ui": {"resourceUri": "ui://server-everything/weather-dashboard"}}, + } echo = {"content": [{"type": "text", "text": "hello"}]} request = LlmRequest( contents=[