Repository navigation
Labels
.NETUsage: [Issues, PRs], Target: .NetUsage: [Issues, PRs], Target: .NetagentsUsage: [Issues, PRs], Target: Single agentUsage: [Issues, PRs], Target: Single agentmcpUsage: [Issues, PRs], Target: MCPUsage: [Issues, PRs], Target: MCPpythonUsage: [Issues, PRs], Target: PythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflowUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
Description
Description
- What happened? Agent called MCP server tool but response visible to the Agent was duplicated: one was content, the other was structuredContent. Occurred when using my own MCP server, but also tested on MS Learn MCP and Deep Wiki MCP servers.
- What I expected: no duplication, but structured content.
- Directly call MS Learn MCP / Deep Wiki MCP via call_tool and see results.
Code Sample
import asyncio
from agent_framework import MCPStreamableHTTPTool
# DeepWiki's server (built on fastmcp) tags each tool's `_meta` with a `_fastmcp`
# key, whose leading underscore violates the MCP spec's _meta key-name format.
# agent_framework enforces that format strictly and aborts tool loading over it,
# so relax it here to reach the server for this repro. Separate bug, unrelated
# to the duplicate-content issue below.
import agent_framework._mcp as _af_mcp
_af_mcp._validate_mcp_meta_key = lambda key: None
DEEPWIKI_MCP_URL = "https://mcp.deepwiki.com/mcp"
TOOL_NAME = "ask_question"
TOOL_ARGS = {"repoName": "microsoft/agent-framework", "question": "What is this repository about?"}
async def main() -> None:
async with MCPStreamableHTTPTool(name="DeepWiki", url=DEEPWIKI_MCP_URL, load_prompts=False) as deepwiki:
result = await deepwiki.call_tool(TOOL_NAME, **TOOL_ARGS)
print(f"call_tool() returned {len(result)} content item(s) (expected 1)")
for i, item in enumerate(result):
print(f" item[{i}]: {getattr(item, 'text', item)!r:.200}")
# Bypass agent_framework's parsing to inspect the raw CallToolResult:
# confirms the server sends exactly one `content` block plus
# `structuredContent` -- the duplication is manufactured client-side.
raw = await deepwiki.session.call_tool(TOOL_NAME, arguments=TOOL_ARGS)
print(f"raw.content: {len(raw.content)} block(s); raw.structuredContent is None: {raw.structuredContent is None}")
if __name__ == "__main__":
asyncio.run(main())Error Messages / Stack Traces
uv run python repro_mcp_duplicate_content.py
call_tool() returned 2 content item(s) (expected 1)
item[0]: 'This repository, `microsoft/agent-framework`, is a multi-language framework designed for building, orchestrating, and deploying AI agents . It provides parallel implementations in both .NET and Pytho
item[1]: '{"result": "This repository, `microsoft/agent-framework`, is a multi-language framework designed for building, orchestrating, and deploying AI agents . It provides parallel implementations in both .N
raw.content: 1 block(s); raw.structuredContent is None: FalsePackage Versions
1.13.0
Python Version
3.12
Additional Context
Probable root cause: agent_framework/_mcp.py::_parse_tool_result_from_mcp unconditionally
appends a second Content item built from structuredContent, even when content
already carries an equivalent serialization.
So even if for-loop succeeds appending content, next if block will append structuredContent anyway.
def _parse_tool_result_from_mcp(
self,
mcp_type: types.CallToolResult,
) -> list[Content]:
"""Parse an MCP CallToolResult into a list of Content items.
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
``additional_properties["_meta"]``. Downstream layers (such as
:class:`agent_framework.security.SecureMCPToolProxy`) consume this key
to derive per-item security labels.
The sentinel is intentionally generic so any MCP server's ``_meta``
keys (current or future) can be interpreted by higher-level code.
"""
from mcp import types
raw_meta = mcp_type.meta
meta: dict[str, Any] | None = dict(raw_meta) if isinstance(raw_meta, Mapping) else None
# Stamp the server ``_meta`` payload directly via additional_properties on
# each newly constructed Content; empty when the server provided no meta.
additional_kwargs: dict[str, Any] = {"additional_properties": {"_meta": meta}} if meta else {}
result: list[Content] = []
for item in mcp_type.content:
match item:
case types.TextContent():
result.append(Content.from_text(item.text, **additional_kwargs))
case types.ImageContent() | types.AudioContent():
decoded = base64.b64decode(item.data)
result.append(
Content.from_data(
data=decoded,
media_type=item.mimeType,
**additional_kwargs,
)
)
case types.ResourceLink():
result.append(
Content.from_uri(
uri=str(item.uri),
media_type=item.mimeType,
**additional_kwargs,
)
)
case types.EmbeddedResource():
match item.resource:
case types.TextResourceContents():
result.append(Content.from_text(item.resource.text, **additional_kwargs))
case types.BlobResourceContents():
blob = item.resource.blob
mime = item.resource.mimeType or "application/octet-stream"
if not blob.startswith("data:"):
blob = f"data:{mime};base64,{blob}"
result.append(
Content.from_uri(
uri=blob,
media_type=mime,
**additional_kwargs,
)
)
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 resultMetadata
Metadata
Labels
.NETUsage: [Issues, PRs], Target: .NetUsage: [Issues, PRs], Target: .NetagentsUsage: [Issues, PRs], Target: Single agentUsage: [Issues, PRs], Target: Single agentmcpUsage: [Issues, PRs], Target: MCPUsage: [Issues, PRs], Target: MCPpythonUsage: [Issues, PRs], Target: PythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflowUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
Type
Projects
- StatusShow more project fieldsDone
Activity
[-]Python: [Bug]: MCP tools returns double output if CallToolResult contains both content and structuredContent[/-][+].NET: Python: [Bug]: MCP tools returns double output if CallToolResult contains both content and structuredContent[/+][-].NET: Python: [Bug]: MCP tools returns double output if CallToolResult contains both content and structuredContent[/-][+]Python: [Bug]: MCP tools returns double output if CallToolResult contains both content and structuredContent[/+][-]Python: [Bug]: MCP tools returns double output if CallToolResult contains both content and structuredContent[/-][+]Python: [Bug]: MCP tools return double output if CallToolResult contains both content and structuredContent[/+]github-actions commentedon Aug 25, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
Repro:
python/packages/core/agent_framework/_mcp.py::_parse_tool_result_from_mcpat lines 678-720 returns duplicate items whenCallToolResult.contentcontains text also represented by non-NonestructuredContent. Minimal repro: construct a result with plain text content and{"result": text}structured content, then assert the parser returns one structured result; it currently returns two.python/packages/core/tests/core/test_mcp.py::test_parse_tool_result_from_mcp_does_not_duplicate_equivalent_structured_content1.13.01.15.0Pavnat commentedon Aug 25, 2026
After seeing Agent's analysis and test at
python/packages/core/tests/core/test_mcp.py::test_parse_tool_result_from_mcp_structured_content_with_text,it seems Agent Framework by design expects Content and structuredContent to be complementary, while MCP servers (at least DeepWiki and MS Learn) serve the same content in both.
MCP Dev Perspective
This problem has also MCP devs attention. As per their Roadmap, they want to rework output / structured output so there is no ambiguity:
Source: https://modelcontextprotocol.io/development/roadmap
I would expect structured output to be prioritized whenever available, as it is supposed to be more deterministic.
Workaround
I am going to try implement custom parsers, which can be provided when initializing
MCPStreamableHTTPTool.Pavnat commentedon Aug 26, 2026
Implementing custom parser did the job.
It's almost identical to the default one.
Eduard van Valkenburg (@eavanvalkenburg) if that kind of design is fine by you, I can add some tests and make a PR.
eavanvalkenburg commentedon Aug 27, 2026
Thanks both Pavlo Natalenko (@Pavnat) and Shivani . (@Shivani767) for taking a look at this, my main concern is if there is a standard convention for what data is passed as
contentvsstructuredContent, if they should be side-by-side and different and it is up to a server to choose how to use them then I would be more comfortable with pointing users to a custom parser because the behavior is not fixed and it's hard to define a exact default. If on the other hand, the MCP spec does have a opinion, then we should follow that (while still having the custom_parser as a break glass for incompatible servers). So for now, I think a sample showing the custom parser would be my preference unless we know more about the spec to give us a default.Pavnat commentedon Aug 28, 2026
Eduard van Valkenburg (@eavanvalkenburg)
After more digging, I agree giving default is not a good idea.
Additional facts to consider
There was quite a discussion on MCP side, but here is the current conclusion from MCP core devs:
We feel this is a symptom of an underlying issue with the tools/call interface, that we need to address instead, by allowing polymorphic return types (structured or unstructured), instead of returning bothSource: modelcontextprotocol/modelcontextprotocol#2200 (comment)
This tells me they probably see returning both
contentandstructuredContentas a problem. Although I agree it's hard to decide what preference there should be in agent framework as of now.However, until this is resolved by MCP, there should be awareness of
contentandstructuredContentbeing returned together. Otherwise many MAF users may end up burning tokens and with worse agent performance without realizing it. I only noticed this because my MCP tools return large amounts of text, which made me notice extra costs.Interestingly, other agentic solutions do have preferences. Please see the screenshot below:

Source: modelcontextprotocol/modelcontextprotocol#1624
Proposal
Considering all of above, I'd prefer agent framework to enforce preference selection:
both,contentorstructuredContent- this makes ambiguity explicit, and it is a low effort to adjust existing code from user's perspective if added as a parameter inMCPStreamableHTTPToolAnd the very least that can be done: document the fact MCP servers do not adhere to a single standard, because there is no one yet.
eavanvalkenburg commentedon Aug 31, 2026
I agree, I think giving this choice make sense, in my mind the default should probably be to propagate from the most specific (structuredContent) to the more generic (content), which would make the param something like:
structuredContent or content(python styleorpropagation)content or structuredContent)content if any and structuredContent if any)Roger Barreto (@rogerbarreto) do you know if .Net already handles this in some way?
likebean commentedon Aug 31, 2026
Related follow-up for MCP Apps / AG-UI Hosts: #7959
#7866 is about which field the model should see (and the duplicate tokens from appending
structuredContentas extra text). #7959 is the other half: even when the model only getscontent, liveTOOL_CALL_RESULT,MESSAGES_SNAPSHOT, and history conversion still need the fullCallToolResultincludingstructuredContent. A content-only parser without that Host path will drop widget payloads after snapshot.eavanvalkenburg commentedon Aug 31, 2026
Pavlo Natalenko (@Pavnat) please also make sure to use the defined PR template
Pavnat commentedon Sep 9, 2026
I am currently short on time. Will try to do the PR later in September or the latest in October.
fix(mcp): add explicit tool_result_content selection policy
Python: fix(mcp): do not duplicate structuredContent when content pre…