Skip to content

Emit AG-UI events for hosted MCP tool calls and results - #1121

Open
PratikDhanave (PratikDhanave) wants to merge 2 commits into
microsoft:mainfrom
PratikDhanaveFork:fix/agui-mcp-tool-events
Open

PratikDhanave (PratikDhanave) wants to merge 2 commits into
microsoft:mainfrom
PratikDhanaveFork:fix/agui-mcp-tool-events

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

On the AG-UI host path, contentToEvents had no case for *message.MCPServerToolCallContent or *message.MCPServerToolResultContent, so a hosted MCP tool invocation echoed back by the model fell through to the default and was silently dropped.

The Python host emits these via _emit_mcp_tool_call (→ ToolCallStartEvent + ToolCallArgsEvent) and _emit_mcp_tool_result (→ ToolCallResultEvent).

Change

  • Map MCPServerToolCallContentTOOL_CALL_START / TOOL_CALL_ARGS / TOOL_CALL_END and MCPServerToolResultContentTOOL_CALL_RESULT, mirroring the existing FunctionCallContent/FunctionResultContent cases (including the result-<callID> unique result id). The mixed-invocation server-tool filter only touches FunctionCallContent, so MCP content reaches this converter.

Test

  • TestHandler_MCPToolCallAndResultEmitEvents: an MCP call + result now produce TOOL_CALL_START (with mcp_tool) and TOOL_CALL_RESULT. Fails before (both dropped), passes after.

contentToEvents had no case for MCPServerToolCallContent or
MCPServerToolResultContent, so hosted MCP tool interactions fell through to
the default and were dropped. Emit them as AG-UI tool-call start/args/end and
tool-call result events, matching the Python host.
Copilot AI lite review requested due to automatic review settings September 20, 2026 04:28
@github-actions github-actions Bot added area:provider Changes files in the provider area area:provider/agui Changes files in the provider / agui area size:medium At most 100 changed lines across at most 5 files labels Sep 20, 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.

Copilot review overview

🟢 Approval recommended

The implementation is covered; remaining feedback is a non-blocking test-coverage nit.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

Adds AG-UI event emission for hosted MCP tool calls and results.

Changes:

  • Maps MCP calls to tool-call lifecycle events.
  • Maps MCP results to TOOL_CALL_RESULT.
  • Adds hosted MCP event coverage.
File Description
provider/​aguiprovider/​hosting_test.go Tests hosted MCP event emission.
provider/​aguiprovider/​hosting_events.go Converts MCP content into AG-UI events.

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

Comment thread provider/aguiprovider/hosting_test.go Outdated
Comment on lines +227 to +232
if !strings.Contains(content, "TOOL_CALL_START") || !strings.Contains(content, "mcp_tool") {
t.Fatalf("expected MCP tool-call start event, got %q", content)
}
if !strings.Contains(content, "TOOL_CALL_RESULT") {
t.Fatalf("expected MCP tool-call result event, got %q", content)
}
@github-actions github-actions Bot added kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure labels Sep 20, 2026
@github-actions

This comment has been minimized.

Assert TOOL_CALL_ARGS, TOOL_CALL_END, and the serialized result payload in
addition to start/result, so a regression dropping any event or the payload
is caught.
@github-actions github-actions Bot added size:large At most 300 changed lines across at most 10 files kind:dependencies Changes dependencies or manifests and removed size:medium At most 100 changed lines across at most 5 files labels Sep 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope: user-visible behavior

Changed Go contract: contentToEvents (unexported, provider/aguiprovider/hosting_events.go) gains *message.MCPServerToolCallContent / *message.MCPServerToolResultContent cases that emit ToolCallStartEvent/ToolCallArgsEvent/ToolCallEndEvent/ToolCallResultEvent over AG-UI instead of dropping hosted MCP tool activity. No exported Go symbols were added or changed.

Upstream evidence reviewed:

  • python/packages/ag-ui/agent_framework_ag_ui/_run_common.py::_emit_mcp_tool_call (lines ~986-1029) and _emit_mcp_tool_result (lines ~1032-1060), which this PR is explicitly ported from.
  • _run_common.py::_emit_tool_call (regular function calls) and _track_tool_call_segment (line 574), which both _emit_tool_call and _emit_mcp_tool_call use to compute a parent_message_id/tool_message_id for ToolCallStartEvent.
  • Go's existing *message.FunctionCallContent case in the same file/function, which already sets WithParentMessageID(messageID) on NewToolCallStartEvent "to associate the tool call with its assistant message" and avoid the multi-message coalescing bug (see TestHandler_ToolCallCarriesParentMessageID and toAgentMessages's coalescing workaround in hosting_convert.go).

Result: findings reported

Finding: MCP tool-call start event omits parentMessageId, unlike the sibling FunctionCallContent case and the Python source it ports

In provider/aguiprovider/hosting_events.go, the new case *message.MCPServerToolCallContent: calls:

aguiEvents.NewToolCallStartEvent(callID, c.Name),

with no WithParentMessageID option, whereas the adjacent case *message.FunctionCallContent: (same function, a few lines above) explicitly passes WithParentMessageID(messageID) when messageID != "", with the comment "Associate the tool call with its assistant message so the client does not create a separate assistant message per tool call ... Matches the Python host."

Upstream, both call sites route through the same mechanism: _emit_tool_call (regular function calls) and _emit_mcp_tool_call (this PR's stated source) each call _track_tool_call_segment(flow, tool_call_id) and pass its result as parent_message_id to ToolCallStartEvent. Python does not special-case MCP calls to omit parent_message_id.

Because Go's own AG-UI client path (toAgentMessages in hosting_convert.go) coalesces consecutive assistant messages by keying off ToolCallStartEvent.parentMessageId (see agui.go's pendingToolCall{... MessageID: cmp.Or(deref(e.ParentMessageID), e.ToolCallID)}), an MCP tool call streamed without parentMessageId will not coalesce with the surrounding assistant message the way a regular function call does. This can trigger the same "two consecutive single-tool-call assistant messages without intervening tool results" HTTP 400 that the FunctionCallContent fix was written to avoid, specifically for hosted MCP tool calls.

Suggested resolution: mirror the FunctionCallContent case — thread messageID through to the MCP call branch and pass WithParentMessageID(messageID) (guarded on messageID != "") to NewToolCallStartEvent for MCPServerToolCallContent, matching both the existing Go convention and the Python _emit_mcp_tool_call/_track_tool_call_segment behavior.

No other parity issues found: the ToolCallStart/Args/End + ToolCallResult shape, the result-<callID> unique result-message-id convention, and the MCP→AG-UI event mapping otherwise match the Python _emit_mcp_tool_call/_emit_mcp_tool_result semantics.

Generated by Go API Consistency Review Agent · copilot · auto · 101.3 AIC · ⌖ 8.92 AIC · ⊞ 9.2K ·

@github-actions github-actions Bot 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.

Generated by Go API Consistency Review Agent · copilot · auto · 101.3 AIC · ⌖ 8.92 AIC · ⊞ 9.2K

args = "{}"
}
return []aguiEvents.Event{
aguiEvents.NewToolCallStartEvent(callID, c.Name),

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.

MCP tool-call events lose the parentMessageId association that the sibling FunctionCallContent case sets a few lines above (WithParentMessageID(messageID), with the comment "Associate the tool call with its assistant message ... Matches the Python host").

Upstream, both _emit_tool_call (regular function calls) and _emit_mcp_tool_call — the function this PR's own description cites as the Python source — call _track_tool_call_segment(flow, tool_call_id) and pass the result as parent_message_id to ToolCallStartEvent (python/packages/ag-ui/agent_framework_ag_ui/_run_common.py, _emit_mcp_tool_call ~L1002-1008, _track_tool_call_segment ~L574). Python does not special-case MCP calls to omit it.

Because toAgentMessages (hosting_convert.go) coalesces consecutive assistant messages by keying off ToolCallStartEvent.parentMessageId (see agui.go's pendingToolCall{MessageID: cmp.Or(deref(e.ParentMessageID), e.ToolCallID)}), an MCP tool call streamed without parentMessageId won't coalesce with the surrounding assistant message the way a regular function call does — risking the same "two consecutive single-tool-call assistant messages without intervening tool results" HTTP 400 the FunctionCallContent fix was meant to prevent, now specifically for hosted MCP tool calls.

Suggested fix: pass WithParentMessageID(messageID) (guarded on messageID != "") to NewToolCallStartEvent here too, matching the FunctionCallContent case and the Python _emit_mcp_tool_call/_track_tool_call_segment behavior.

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

Labels

area:provider/agui Changes files in the provider / agui area area:provider Changes files in the provider area kind:code Changes production behavior or code kind:dependencies Changes dependencies or manifests kind:tests Changes tests, fixtures, or test infrastructure size:large At most 300 changed lines across at most 10 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants