From dbc189b12133b5aa8084e50571f21d51eb151c40 Mon Sep 17 00:00:00 2001 From: Shivani Bhandari Date: Thu, 27 Aug 2026 11:29:15 +0530 Subject: [PATCH 1/2] Python: Prefer MCP structuredContent over duplicate content When CallToolResult includes both content and structuredContent, return only the structured payload so agents are not charged for duplicated tokens from servers that echo the same result in both fields. --- python/packages/core/agent_framework/_mcp.py | 18 ++++++-- python/packages/core/tests/core/test_mcp.py | 44 +++++++++++++++++--- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index 9abafdad277..a1e434717ae 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -630,6 +630,13 @@ def _parse_tool_result_from_mcp( ) -> list[Content]: """Parse an MCP CallToolResult into a list of Content items. + When ``structuredContent`` is present it is preferred and returned alone. + Many MCP servers (including MS Learn and DeepWiki) also echo an equivalent + serialization in ``content``, and appending both duplicates tokens for the + agent. Preferring structured output matches the more deterministic payload + and avoids that duplication (#7866). Plain ``content`` is used only when + ``structuredContent`` is absent. + If the server attached a ``_meta`` payload to the tool result (e.g. for Information Flow Control labels under the ``ifc`` key), a copy of that payload is stamped onto each produced :class:`Content` instance under @@ -647,6 +654,14 @@ def _parse_tool_result_from_mcp( # each newly constructed Content; empty when the server provided no meta. additional_kwargs: dict[str, Any] = {"additional_properties": {"_meta": meta}} if meta else {} + if mcp_type.structuredContent is not None: + return [ + Content.from_text( + json.dumps(mcp_type.structuredContent, default=str), + **additional_kwargs, + ) + ] + result: list[Content] = [] for item in mcp_type.content: match item: @@ -688,9 +703,6 @@ def _parse_tool_result_from_mcp( case _: result.append(Content.from_text(str(item), **additional_kwargs)) - if mcp_type.structuredContent is not None: - result.append(Content.from_text(json.dumps(mcp_type.structuredContent, default=str))) - if not result: result.append(Content.from_text("null", **additional_kwargs)) return result diff --git a/python/packages/core/tests/core/test_mcp.py b/python/packages/core/tests/core/test_mcp.py index 028cac027b3..388d932096d 100644 --- a/python/packages/core/tests/core/test_mcp.py +++ b/python/packages/core/tests/core/test_mcp.py @@ -473,7 +473,7 @@ def test_parse_tool_result_from_mcp_structured_content_only(): def test_parse_tool_result_from_mcp_structured_content_with_text(): - """Test that structuredContent is appended alongside regular content items.""" + """When both are present, prefer structuredContent instead of appending both (#7866).""" mcp_result = types.CallToolResult( content=[types.TextContent(type="text", text="Summary")], structuredContent={"data": [1, 2, 3]}, @@ -481,15 +481,47 @@ def test_parse_tool_result_from_mcp_structured_content_with_text(): result = _HELPER_MCP_TOOL._parse_tool_result_from_mcp(mcp_result) assert isinstance(result, list) - assert len(result) == 2 + assert len(result) == 1 assert result[0].type == "text" - assert result[0].text == "Summary" - assert result[1].type == "text" - assert result[1].text is not None - parsed = json.loads(result[1].text) + assert result[0].text is not None + parsed = json.loads(result[0].text) assert parsed == {"data": [1, 2, 3]} +def test_parse_tool_result_from_mcp_does_not_duplicate_equivalent_structured_content(): + """Regression for #7866: servers often echo the same payload in content and structuredContent.""" + text = ( + "This repository, `microsoft/agent-framework`, is a multi-language framework " + "designed for building, orchestrating, and deploying AI agents." + ) + mcp_result = types.CallToolResult( + content=[types.TextContent(type="text", text=text)], + structuredContent={"result": text}, + ) + result = _HELPER_MCP_TOOL._parse_tool_result_from_mcp(mcp_result) + + assert len(result) == 1 + assert result[0].type == "text" + assert result[0].text is not None + assert json.loads(result[0].text) == {"result": text} + + +def test_parse_tool_result_from_mcp_structured_content_stamps_meta(): + """structuredContent-preferred results must still carry server ``_meta``.""" + mcp_result = types.CallToolResult( + content=[types.TextContent(type="text", text="ignored when structured")], + structuredContent={"ok": True}, + _meta={"ifc": {"integrity": "untrusted", "confidentiality": "public"}}, + ) + result = _HELPER_MCP_TOOL._parse_tool_result_from_mcp(mcp_result) + + assert len(result) == 1 + assert result[0].additional_properties.get("_meta") == { + "ifc": {"integrity": "untrusted", "confidentiality": "public"}, + } + assert json.loads(result[0].text) == {"ok": True} + + def test_parse_tool_result_from_mcp_structured_content_none(): """Test that None structuredContent does not affect results.""" mcp_result = types.CallToolResult( From 5f46d091f34ea44be5c2a33b23ec3afa0d9d4e9d Mon Sep 17 00:00:00 2001 From: Shivani Bhandari Date: Thu, 27 Aug 2026 11:45:46 +0530 Subject: [PATCH 2/2] Python: Dedupe MCP structured echoes without dropping rich content Skip only text content that echoes structuredContent; keep images, audio, resources, and complementary summaries. Add a mixed-content regression test. --- python/packages/core/agent_framework/_mcp.py | 66 +++++++++++++++----- python/packages/core/tests/core/test_mcp.py | 37 +++++++++-- 2 files changed, 82 insertions(+), 21 deletions(-) diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index a1e434717ae..b87e99701b0 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -390,6 +390,33 @@ def _should_propagate_cancelled_error(ex: BaseException) -> bool: return task is not None and task.cancelling() > 0 +def _structured_content_contains_text(structured: Any, text: str) -> bool: + """Return whether *text* appears as a string value anywhere in *structured*.""" + if isinstance(structured, str): + return structured == text + if isinstance(structured, Mapping): + return any(_structured_content_contains_text(value, text) for value in structured.values()) + if isinstance(structured, Sequence) and not isinstance(structured, (str, bytes, bytearray)): + return any(_structured_content_contains_text(item, text) for item in structured) + return False + + +def _text_duplicates_structured_content(text: str, structured: Any, structured_json: str) -> bool: + """Return whether a text content block is an echo of ``structuredContent``. + + MCP servers often return the same payload as both a text ``content`` block and + ``structuredContent`` (for example ``{"result": ""}``). Treat those as + duplicates so agents are not charged twice. Complementary text (a human-readable + summary that is not present in the structured payload) is kept. + """ + if text == structured_json: + return True + with contextlib.suppress(json.JSONDecodeError, TypeError): + if json.loads(text) == structured: + return True + return _structured_content_contains_text(structured, text) + + # region: MCP Plugin @@ -630,12 +657,11 @@ def _parse_tool_result_from_mcp( ) -> list[Content]: """Parse an MCP CallToolResult into a list of Content items. - When ``structuredContent`` is present it is preferred and returned alone. - Many MCP servers (including MS Learn and DeepWiki) also echo an equivalent - serialization in ``content``, and appending both duplicates tokens for the - agent. Preferring structured output matches the more deterministic payload - and avoids that duplication (#7866). Plain ``content`` is used only when - ``structuredContent`` is absent. + When ``structuredContent`` is present it is emitted first. Text (or embedded + text) ``content`` blocks that merely echo that structured payload are skipped + so servers such as MS Learn / DeepWiki do not duplicate tokens (#7866). + Non-text blocks (images, audio, resources) and complementary text that is not + represented in ``structuredContent`` are retained. If the server attached a ``_meta`` payload to the tool result (e.g. for Information Flow Control labels under the ``ifc`` key), a copy of that @@ -654,18 +680,19 @@ def _parse_tool_result_from_mcp( # each newly constructed Content; empty when the server provided no meta. additional_kwargs: dict[str, Any] = {"additional_properties": {"_meta": meta}} if meta else {} - if mcp_type.structuredContent is not None: - return [ - Content.from_text( - json.dumps(mcp_type.structuredContent, default=str), - **additional_kwargs, - ) - ] - + structured = mcp_type.structuredContent + structured_json: str | None = None result: list[Content] = [] + if structured is not None: + structured_json = json.dumps(structured, default=str) + result.append(Content.from_text(structured_json, **additional_kwargs)) + for item in mcp_type.content: match item: case types.TextContent(): + if structured is not None and structured_json is not None: + if _text_duplicates_structured_content(item.text, structured, structured_json): + continue result.append(Content.from_text(item.text, **additional_kwargs)) case types.ImageContent() | types.AudioContent(): decoded = base64.b64decode(item.data) @@ -687,6 +714,11 @@ def _parse_tool_result_from_mcp( case types.EmbeddedResource(): match item.resource: case types.TextResourceContents(): + if structured is not None and structured_json is not None: + if _text_duplicates_structured_content( + item.resource.text, structured, structured_json + ): + continue result.append(Content.from_text(item.resource.text, **additional_kwargs)) case types.BlobResourceContents(): blob = item.resource.blob @@ -701,7 +733,11 @@ def _parse_tool_result_from_mcp( ) ) case _: - result.append(Content.from_text(str(item), **additional_kwargs)) + fallback = str(item) + if structured is not None and structured_json is not None: + if _text_duplicates_structured_content(fallback, structured, structured_json): + continue + result.append(Content.from_text(fallback, **additional_kwargs)) if not result: result.append(Content.from_text("null", **additional_kwargs)) diff --git a/python/packages/core/tests/core/test_mcp.py b/python/packages/core/tests/core/test_mcp.py index 388d932096d..5832f9c3251 100644 --- a/python/packages/core/tests/core/test_mcp.py +++ b/python/packages/core/tests/core/test_mcp.py @@ -473,7 +473,7 @@ def test_parse_tool_result_from_mcp_structured_content_only(): def test_parse_tool_result_from_mcp_structured_content_with_text(): - """When both are present, prefer structuredContent instead of appending both (#7866).""" + """Complementary human-readable text is kept alongside structuredContent.""" mcp_result = types.CallToolResult( content=[types.TextContent(type="text", text="Summary")], structuredContent={"data": [1, 2, 3]}, @@ -481,11 +481,12 @@ def test_parse_tool_result_from_mcp_structured_content_with_text(): result = _HELPER_MCP_TOOL._parse_tool_result_from_mcp(mcp_result) assert isinstance(result, list) - assert len(result) == 1 + assert len(result) == 2 assert result[0].type == "text" assert result[0].text is not None - parsed = json.loads(result[0].text) - assert parsed == {"data": [1, 2, 3]} + assert json.loads(result[0].text) == {"data": [1, 2, 3]} + assert result[1].type == "text" + assert result[1].text == "Summary" def test_parse_tool_result_from_mcp_does_not_duplicate_equivalent_structured_content(): @@ -507,9 +508,9 @@ def test_parse_tool_result_from_mcp_does_not_duplicate_equivalent_structured_con def test_parse_tool_result_from_mcp_structured_content_stamps_meta(): - """structuredContent-preferred results must still carry server ``_meta``.""" + """structuredContent results must still carry server ``_meta``.""" mcp_result = types.CallToolResult( - content=[types.TextContent(type="text", text="ignored when structured")], + content=[], structuredContent={"ok": True}, _meta={"ifc": {"integrity": "untrusted", "confidentiality": "public"}}, ) @@ -522,6 +523,30 @@ def test_parse_tool_result_from_mcp_structured_content_stamps_meta(): assert json.loads(result[0].text) == {"ok": True} +def test_parse_tool_result_from_mcp_keeps_rich_content_with_structured(): + """Non-text content blocks must not be dropped when structuredContent is present.""" + mcp_result = types.CallToolResult( + content=[ + types.ImageContent( + type="image", + data="ZmFrZS1pbWFnZS1ieXRlcw==", # base64 for b"fake-image-bytes" + mimeType="image/png", + ), + types.TextContent(type="text", text="caption echoed in structured"), + ], + structuredContent={"caption": "caption echoed in structured", "width": 32}, + ) + result = _HELPER_MCP_TOOL._parse_tool_result_from_mcp(mcp_result) + + assert len(result) == 2 + assert result[0].type == "text" + assert result[0].text is not None + assert json.loads(result[0].text) == {"caption": "caption echoed in structured", "width": 32} + assert result[1].type == "data" + assert result[1].media_type == "image/png" + assert "ZmFrZS1pbWFnZS1ieXRlcw==" in result[1].uri # type: ignore[operator] + + def test_parse_tool_result_from_mcp_structured_content_none(): """Test that None structuredContent does not affect results.""" mcp_result = types.CallToolResult(