Repository navigation
Description
Description
In the streaming path, mcp_server_tool_result is emitted immediately on
response.output_item.added (status='calling', output=None) as a UI lifecycle
signal only. The actual tool output is never forwarded because response.mcp_call.completed
has no handler and falls through to the debug-level case _ branch.
This creates a parity gap across three paths:
| Path | mcp_server_tool_result content |
|---|---|
Non-streaming (stream=False) |
✅ Populated from item.output |
| Agent-as-tool (local orchestrator) | ✅ Populated via TOOL_CALL_RESULT |
Streaming (stream=True) |
❌ Always output=None |
UI layers built on the streaming path (e.g. AG-UI/CopilotKit) cannot display MCP tool
outputs to the user, despite this being supported in both the non-streaming path and the
when using an agent as a tool.
Root Cause
In _parse_chunk_from_openai, the response.output_item.added handler for mcp_call
eagerly emits both mcp_server_tool_call and mcp_server_tool_result at dispatch time,
before the MCP server has responded:
# _parse_chunk_from_openai → response.output_item.added → mcp_call
case "mcp_call":
call_id = getattr(event_item, "id", None) or getattr(event_item, "call_id", None) or ""
contents.append(
Content.from_mcp_server_tool_call(
call_id=call_id,
tool_name=getattr(event_item, "name", "") or "",
server_name=getattr(event_item, "server_label", None),
arguments=getattr(event_item, "arguments", None),
raw_representation=event_item,
)
)
result_output = (
getattr(event_item, "result", None)
or getattr(event_item, "output", None) # ← always None at this point
or getattr(event_item, "outputs", None)
)
# ...
contents.append(
Content.from_mcp_server_tool_result(
call_id=call_id,
output=parsed_output, # ← always None
raw_representation=event_item,
)
)response.mcp_call.completed — which carries the actual output — is listed in the event
type comment block but has no handler, causing it to be silently discarded:
# Line 1766 — listed in comment only, never handled:
# ResponseMcpCallCompletedEvent,The non-streaming path confirms the intent — item.output is conditionally populated,
proving the framework authors knew output could exist:
# _parse_response_from_openai — non-streaming, works correctly
case "mcp_call":
contents.append(Content.from_mcp_server_tool_call(...))
if item.output is not None: # ← guard proves output can be populated
contents.append(
Content.from_mcp_server_tool_result(
output=[Content.from_text(text=item.output)],
...
)
)Steps to Reproduce
- Configure a Foundry agent with a hosted MCP server tool
- Call the agent with
stream=True - Observe
mcp_server_tool_resultevents withoutput=Nonein the content stream - Confirm
response.mcp_call.completedevents are logged only at DEBUG as "Unparsed event" - Call the same agent with
stream=False - Observe
mcp_server_tool_resultevents withoutputcorrectly populated
Expected Behaviour
mcp_server_tool_result emitted in the streaming path should carry the tool output,
matching the non-streaming path. The result should be emitted when
response.mcp_call.completed fires, not at response.output_item.added.
Actual Behaviour
mcp_server_tool_result is emitted immediately on response.output_item.added with
output=None. The response.mcp_call.completed event is dropped. The frontend receives
a lifecycle signal but no content, making it impossible to display MCP tool outputs to
the user in streaming mode.
Suggested Fix
Remove the premature mcp_server_tool_result emission from the
response.output_item.added / mcp_call branch, and add a handler for
response.mcp_call.completed:
# response.output_item.added / mcp_call — emit call only, defer result
case "mcp_call":
call_id = getattr(event_item, "id", None) or getattr(event_item, "call_id", None) or ""
contents.append(
Content.from_mcp_server_tool_call(
call_id=call_id,
tool_name=getattr(event_item, "name", "") or "",
server_name=getattr(event_item, "server_label", None),
arguments=getattr(event_item, "arguments", None),
raw_representation=event_item,
)
)
# Result deferred to response.mcp_call.completed
# New handler — emit result with actual output when MCP call completes
case "response.mcp_call.completed":
item = event.item
call_id = getattr(item, "id", None) or ""
output_text = getattr(item, "output", None)
parsed_output = (
[Content.from_text(text=output_text)]
if isinstance(output_text, str)
else None
)
contents.append(
Content.from_mcp_server_tool_result(
call_id=call_id,
output=parsed_output,
raw_representation=event,
)
)Workaround
Use stream=False. The non-streaming path correctly populates output at the cost of
losing progressive text rendering
Code Sample
Error Messages / Stack Traces
Package Versions
agent-framework-core==1.0.0rc5 agent-framework-azure-ai==1.0.0rc5
Python Version
Python 3.12
Additional Context
No response
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
Fix streaming path to deliver mcp_server_tool_result content (microso…
Fix streaming path to deliver mcp_server_tool_result content (microso…
Address review: call_id fallback and raw_representation consistency (m…
Address review: call_id fallback for non-streaming path and test cove…
Python: Fix streaming path to emit mcp_server_tool_result on output_i…
.NET: Project ToolExecution events as FunctionCallContent/FunctionRes…