Python: Emit AG-UI events for MCP tool calls, results, and text reasoning#4233
Python: Emit AG-UI events for MCP tool calls, results, and text reasoning#4233LEDazzio01 wants to merge 6 commits intomicrosoft:mainfrom
Conversation
…t reasoning Fixes microsoft#4213 — mcp_server_tool_call, mcp_server_tool_result, and text_reasoning content types now produce AG-UI SSE events instead of being silently dropped by _emit_content(). - _emit_mcp_tool_call: maps to ToolCallStart + ToolCallArgs events - _emit_mcp_tool_result: maps to ToolCallEnd + ToolCallResult events - _emit_text_reasoning: maps to CustomEvent(name="text_reasoning")
Tests cover: - _emit_mcp_tool_call: start/args events, flow tracking, server name prefix - _emit_mcp_tool_result: end/result events, flow tracking, serialization - _emit_text_reasoning: custom event, protected_data fallback, empty handling - _emit_content routing: verifies dispatch for all three new content types
There was a problem hiding this comment.
Pull request overview
Adds missing AG-UI SSE emission for Foundry-specific content types so MCP tool activity and text_reasoning are visible to AG-UI consumers (fixes #4213).
Changes:
- Add
_emit_mcp_tool_call,_emit_mcp_tool_result, and_emit_text_reasoning, and route them from_emit_content(). - Track MCP tool calls/results in
FlowStateforMESSAGES_SNAPSHOTinclusion. - Add new unit tests covering MCP call/result emission and
text_reasoningrouting.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| python/packages/ag-ui/agent_framework_ag_ui/_run_common.py | Implements new emitters for MCP tool call/result and text_reasoning, and wires dispatch in _emit_content(). |
| python/packages/ag-ui/tests/ag_ui/test_run.py | Adds test coverage for new emitters and routing behavior. |
| events.append(ToolCallEndEvent(tool_call_id=content.call_id)) | ||
| flow.tool_calls_ended.add(content.call_id) | ||
|
|
||
| raw_output = content.output if content.output is not None else "" | ||
| result_content = raw_output if isinstance(raw_output, str) else json.dumps(make_json_safe(raw_output)) | ||
| message_id = generate_event_id() | ||
| events.append( | ||
| ToolCallResultEvent( | ||
| message_id=message_id, | ||
| tool_call_id=content.call_id, | ||
| content=result_content, | ||
| role="tool", | ||
| ) | ||
| ) | ||
|
|
||
| flow.tool_results.append( | ||
| { | ||
| "id": message_id, | ||
| "role": "tool", | ||
| "toolCallId": content.call_id, | ||
| "content": result_content, | ||
| } | ||
| ) | ||
|
|
||
| return events |
There was a problem hiding this comment.
_emit_mcp_tool_result doesn’t perform the same FlowState cleanup as _emit_tool_result (e.g., applying predictive_handler updates, resetting tool_call_id/tool_call_name, and closing/resetting an open text message_id/accumulated_text). This can leave a text message “open” across MCP tool results and can make MessagesSnapshotEvent IDs/content inconsistent compared to function_result handling. Consider mirroring _emit_tool_result’s cleanup logic (or refactoring to share it) so MCP results behave the same as standard tool results.
1. _emit_text_reasoning: Don't fall back to protected_data as display text — expose it under a separate key so consumers can decide how to render it (avoids leaking provider-specific metadata as text). 2. _emit_mcp_tool_call: Fix docstring to accurately describe behavior (start + args only; end handled by _emit_mcp_tool_result). 3. _emit_mcp_tool_result: Mirror _emit_tool_result cleanup logic — reset tool_call_id/tool_call_name, close open text messages, reset accumulated_text so MCP results behave consistently with standard tool results.
- TestEmitTextReasoning: test protected_data as separate key (not fallback text), test protected_data-only content still emits event - TestEmitMcpToolResult: add tests for FlowState cleanup (message closing, tool_call_id/name reset, accumulated_text reset)
LEDazzio01
left a comment
There was a problem hiding this comment.
Thanks for the thorough review! All 3 comments have been addressed in the latest commits:
- protected_data: No longer used as fallback display text — exposed under a separate
"protected_data"key so consumers can decide how to render it - Docstring: Updated to accurately describe start/args-only behavior
- FlowState cleanup:
_emit_mcp_tool_resultnow mirrors_emit_tool_result— resetstool_call_id/tool_call_name, closes open text messages, resetsaccumulated_text
Updated tests added to cover the new cleanup behavior and protected_data separation.
| execution visible to AG-UI consumers. Completion/end events are handled | ||
| separately by ``_emit_mcp_tool_result``. | ||
|
|
||
| Fixes #4213. |
There was a problem hiding this comment.
Please remove Fixes #4213.
| events: list[BaseEvent] = [] | ||
|
|
||
| if not content.call_id: | ||
| return events |
There was a problem hiding this comment.
in the event that call_id is missing, we simply return. Should we log anything?
| return events | ||
|
|
||
|
|
||
| def _emit_text_reasoning(content: Content, flow: FlowState) -> list[BaseEvent]: |
There was a problem hiding this comment.
flow is never used in this method?
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
1. Remove `Fixes microsoft#4213` from all emitter docstrings 2. Add logger.warning when call_id is missing in _emit_mcp_tool_result 3. Remove unused `flow` param from _emit_text_reasoning
|
Thanks for the review @moonbox3! All 3 comments have been addressed in the latest commits:
|
Summary
Fixes #4213 —
_emit_content()in the AG-UI layer only handledtext,function_call,function_result,function_approval_request, andusagecontent types. Foundry MCP content types (mcp_server_tool_call,mcp_server_tool_result) andtext_reasoningfell through unhandled, producing no SSE events for AG-UI consumers.Changes
_run_common.pyAdded three new handler functions and wired them into
_emit_content():mcp_server_tool_call_emit_mcp_tool_callTOOL_CALL_START+TOOL_CALL_ARGSmcp_server_tool_result_emit_mcp_tool_resultTOOL_CALL_END+TOOL_CALL_RESULTtext_reasoning_emit_text_reasoningCUSTOM(name="text_reasoning")Design decisions:
server_name/tool_nameformat for display name when server_name is available, for disambiguationtext_reasoningusesCustomEventsince AG-UI protocol has no dedicated reasoning event type — consistent with how_emit_usageworksFlowState(pending_tool_calls,tool_calls_by_id,tool_results,tool_calls_ended) for properMESSAGES_SNAPSHOTinclusiontest_run.pyAdded 15 test cases across 4 new test classes:
TestEmitMcpToolCall— start/args events, flow tracking, server name prefix, missing args, generated IDsTestEmitMcpToolResult— end/result events, flow tracking, missing call_id, JSON serializationTestEmitTextReasoning— custom event, protected_data fallback, empty handling, optional idTestEmitContentMcpRouting— verifies_emit_contentdispatch for all three new content types