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..08305eba7 100644 --- a/python/packages/kagent-adk/tests/unittests/test_mcp_apps.py +++ b/python/packages/kagent-adk/tests/unittests/test_mcp_apps.py @@ -6,6 +6,7 @@ from kagent.adk._mcp_apps import ( MCP_APP_RENDERED_NOTICE, MCPAppToolNames, + _result_has_ui_resource, compact_mcp_app_response, make_mcp_app_model_result_callback, ) @@ -18,6 +19,13 @@ def _function_response_content(name: str, response: dict) -> genai_types.Content ) +def test_result_has_ui_resource_accepts_nested_and_flat_meta_shapes(): + # _meta.ui.resourceUri, the shape the other tests exercise via compact_mcp_app_response. + assert _result_has_ui_resource({"_meta": {"ui": {"resourceUri": "ui://server/dashboard"}}}) + # _meta["ui/resourceUri"], the flat shape parseMCPUIMetadata also accepts (go/adk/pkg/mcp/mcp_ui.go). + assert _result_has_ui_resource({"_meta": {"ui/resourceUri": "ui://server/dashboard"}}) + + def test_compact_success_replaces_payload_with_notice(): response = { "content": [{"type": "text", "text": "Weather for Chicago: Rain, 36C, 82%."}], @@ -31,6 +39,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 +74,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=[