Skip to content

Python: Prefer MCP structuredContent over duplicate content - #7897

Closed
Shivani . (Shivani767) wants to merge 2 commits into
microsoft:mainfrom
Shivani767:fix/7866-mcp-structured-content-dedupe
Closed

Shivani . (Shivani767) wants to merge 2 commits into
microsoft:mainfrom
Shivani767:fix/7866-mcp-structured-content-dedupe

Conversation

@Shivani767

@Shivani767 Shivani . (Shivani767) commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

MCP CallToolResult may include both content and structuredContent. Many servers (MS Learn, DeepWiki) echo an equivalent payload in both fields. Agent Framework previously appended both, so agents saw duplicated text and paid ~2x tokens.

Fixes #7866

Note: Pavlo Natalenko (@Pavnat) reported this and shared a custom-parser workaround that prefers structuredContent; there was no open PR when this was started. Happy to coordinate if preferred.

Description & Review Guide

  • What are the major changes?

    • Emit structuredContent first when present.
    • Skip only text / embedded-text content blocks that demonstrably echo the structured payload (exact JSON match or string value present in the structured tree).
    • Retain complementary text and all non-text blocks (images, audio, resources).
    • Stamp server _meta onto structured-content Content items.
    • Add regression tests for DeepWiki-style duplication, complementary summary text, and image + structured mixed results.
  • What is the impact of these changes?

    • Equivalent duplicated text no longer doubles token usage.
    • Rich / complementary content is no longer dropped when structuredContent exists.
    • Callers who need different merge semantics can still supply parse_tool_results.
  • What do you want reviewers to focus on?

    • Whether treating any matching string leaf in structuredContent as an echo is the right equivalence check.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

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.
Copilot AI balanced review requested due to automatic review settings August 27, 2026 05:59
@agent-framework-automation agent-framework-automation Bot added the python Usage: [Issues, PRs], Target: Python label Aug 27, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates MCP tool-result parsing to avoid duplicated structured output while propagating server metadata.

Changes:

  • Prefer structuredContent over parallel content blocks.
  • Add regression tests for deduplication and metadata propagation.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
python/packages/core/agent_framework/_mcp.py Changes MCP result parsing precedence.
python/packages/core/tests/core/test_mcp.py Updates and adds parser regression tests.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

# 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:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — dropping all content was too aggressive.

Updated to emit structuredContent first, skip only text (or embedded text) that demonstrably echoes the structured payload, and keep complementary text plus images/audio/resources. Added a mixed image + structured regression test.

Skip only text content that echoes structuredContent; keep images,
audio, resources, and complementary summaries. Add a mixed-content
regression test.
Comment on lines +414 to +416
with contextlib.suppress(json.JSONDecodeError, TypeError):
if json.loads(text) == structured:
return True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this comparison preserve JSON type identity? Python treats 1 == True, so content='{"approved": 1}' with structuredContent={"approved": true} reaches this branch and silently drops the numeric result even though they are distinct JSON documents. Comparing canonical JSON with type-aware values, or avoiding heuristic suppression through explicit modes, would prevent the data loss.

Comment on lines +397 to +400
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we avoid rescanning the full structured tree for every text block? An MCP server controls both payloads, and this traversal runs once per content item, so a roughly 248 KB result with 10,000 leaves and 10,000 nonmatching text blocks performs 100 million comparisons and blocked the parser for about four seconds. Precomputing the string leaves once, or using a bounded selection policy, would keep one tool response from stalling the event loop.

Comment on lines +405 to +417
"""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": "<same text>"}``). 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the selection behavior be caller-controlled instead of inferred globally from payload values? Every MCP caller now inherits three hidden rules: serialized JSON equality, Python decoded equality, and equality with any nested string, while parse_tool_results requires replacing the entire rich-content and metadata parser. A result_content_mode on MCPTool, forwarded by its transport constructors, could provide the structured-first, content-first, content-only, structured-only, and both policies discussed in #7866 without making callers reimplement parsing.

@moonbox3

Copy link
Copy Markdown
Contributor

Re-open when wanting to move forward. No need to keep in a draft next time.

@Shivani767

Copy link
Copy Markdown
Contributor Author

Evan Mattson (@moonbox3) Sorry for the late response, and thanks for the detailed feedback. Could you please reopen this PR so I can address the review comments and push the required changes? I’ll update the implementation and add the requested regression tests. Thanks!

@Shivani767

Shivani . (Shivani767) commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Implemented in ab28f74.

What changed

  • Replaced inferred structured-content deduplication with an explicit result_content_mode policy.
  • Added the supported policies: structured_first, content_first, content_only, structured_only, and both.
  • Forwarded the policy through all MCP transports.
  • Eliminated the JSON type-coercion risk (for example, 1 versus true) and repeated scans of structured payload trees.

Test coverage

  • Each content-selection policy
  • Transport option forwarding
  • Invalid policy validation
  • Preservation of distinct JSON values
  • Rich-content preservation with both mode

Validation

uv run --project . pytest packages/core/tests/core/test_mcp.py -q
295 passed, 2 skipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: MCP tools return double output if CallToolResult contains both content and structuredContent

3 participants