Skip to content

Comments

Python: Emit AG-UI events for MCP tool calls, results, and text reasoning#4233

Open
LEDazzio01 wants to merge 6 commits intomicrosoft:mainfrom
LEDazzio01:fix/4213-mcp-tool-call-events
Open

Python: Emit AG-UI events for MCP tool calls, results, and text reasoning#4233
LEDazzio01 wants to merge 6 commits intomicrosoft:mainfrom
LEDazzio01:fix/4213-mcp-tool-call-events

Conversation

@LEDazzio01
Copy link
Contributor

Summary

Fixes #4213_emit_content() in the AG-UI layer only handled text, function_call, function_result, function_approval_request, and usage content types. Foundry MCP content types (mcp_server_tool_call, mcp_server_tool_result) and text_reasoning fell through unhandled, producing no SSE events for AG-UI consumers.

Changes

_run_common.py

Added three new handler functions and wired them into _emit_content():

Content Type Handler AG-UI Events
mcp_server_tool_call _emit_mcp_tool_call TOOL_CALL_START + TOOL_CALL_ARGS
mcp_server_tool_result _emit_mcp_tool_result TOOL_CALL_END + TOOL_CALL_RESULT
text_reasoning _emit_text_reasoning CUSTOM (name="text_reasoning")

Design decisions:

  • MCP tool calls are emitted as complete start→args sequences (not streamed) because Foundry sends them as fully-formed content items
  • server_name/tool_name format for display name when server_name is available, for disambiguation
  • text_reasoning uses CustomEvent since AG-UI protocol has no dedicated reasoning event type — consistent with how _emit_usage works
  • MCP tool calls/results are tracked in FlowState (pending_tool_calls, tool_calls_by_id, tool_results, tool_calls_ended) for proper MESSAGES_SNAPSHOT inclusion

test_run.py

Added 15 test cases across 4 new test classes:

  • TestEmitMcpToolCall — start/args events, flow tracking, server name prefix, missing args, generated IDs
  • TestEmitMcpToolResult — end/result events, flow tracking, missing call_id, JSON serialization
  • TestEmitTextReasoning — custom event, protected_data fallback, empty handling, optional id
  • TestEmitContentMcpRouting — verifies _emit_content dispatch for all three new content types

…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
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 FlowState for MESSAGES_SNAPSHOT inclusion.
  • Add new unit tests covering MCP call/result emission and text_reasoning routing.

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.

Comment on lines 420 to 444
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
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

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

_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.

Copilot uses AI. Check for mistakes.
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)
Copy link
Contributor Author

@LEDazzio01 LEDazzio01 left a comment

Choose a reason for hiding this comment

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

Thanks for the thorough review! All 3 comments have been addressed in the latest commits:

  1. protected_data: No longer used as fallback display text — exposed under a separate "protected_data" key so consumers can decide how to render it
  2. Docstring: Updated to accurately describe start/args-only behavior
  3. FlowState cleanup: _emit_mcp_tool_result now mirrors _emit_tool_result — resets tool_call_id/tool_call_name, closes open text messages, resets accumulated_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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove Fixes #4213.

events: list[BaseEvent] = []

if not content.call_id:
return events
Copy link
Contributor

Choose a reason for hiding this comment

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

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]:
Copy link
Contributor

Choose a reason for hiding this comment

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

flow is never used in this method?

@markwallace-microsoft
Copy link
Member

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/ag-ui/agent_framework_ag_ui
   _run_common.py2482988%46, 51–52, 54, 61, 65, 69–70, 72, 96, 173, 221–222, 236, 263–265, 290–291, 297–302, 522, 524, 533–534
TOTAL21789276087% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
4549 246 💤 0 ❌ 0 🔥 1m 16s ⏱️

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
@LEDazzio01
Copy link
Contributor Author

Thanks for the review @moonbox3! All 3 comments have been addressed in the latest commits:

  1. Fixes #4213 — Removed from all three emitter docstrings (_emit_mcp_tool_call, _emit_mcp_tool_result, _emit_text_reasoning)
  2. Missing call_id logging — Added logger.warning("MCP tool result content missing call_id, skipping") in _emit_mcp_tool_result when call_id is absent
  3. Unused flow param — Removed flow from _emit_text_reasoning signature (it mirrors _emit_usage which also doesn't need flow state). Updated the _emit_content dispatcher and all test call sites accordingly.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: MCP Tool Calls + Reasoning Not Emitted as AG-UI Events (Foundry)

3 participants