fix: do not re-finalize mcp_list_tools in closeLastOutputItem#44
Merged
Conversation
Made-with: Cursor
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
There was a problem hiding this comment.
Pull request overview
Fixes duplicate response.output_item.done emission for mcp_list_tools by ensuring closeLastOutputItem does not re-finalize items whose lifecycle is already fully emitted by listMcpToolsStream, preventing duplicate downstream TOOL_CALL_RESULT events.
Changes:
- Split
closeLastOutputItemhandling somcp_approval_requeststill emitsresponse.output_item.done, whilemcp_list_toolsbecomes a no-op. - Strengthen
listMcpToolsStreamtest to assert exactly oneresponse.output_item.done. - Add a regression test ensuring
closeLastOutputItemyields no events when the last item ismcp_list_tools.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/routes/responses/mcpStream.test.ts | Adds an assertion to ensure listMcpToolsStream emits response.output_item.done exactly once. |
| src/routes/responses/closeOutputItem.ts | Prevents re-emitting response.output_item.done for mcp_list_tools while keeping existing behavior for mcp_approval_request. |
| src/routes/responses/closeOutputItem.test.ts | Adds a regression test verifying closing a trailing mcp_list_tools item is a no-op. |
Comments suppressed due to low confidence (1)
src/routes/responses/closeOutputItem.ts:242
- The StreamingError message is now misleading: this function also supports
reasoning,mcp_approval_request, and explicitly no-opsmcp_list_tools, but the error says it only expectsmessage,function_call, ormcp_call. Please update the message to reflect the full set of supported output item types (or make it generic, e.g. "unexpected output item type").
throw new StreamingError(
`Not implemented: expected message, function_call, or mcp_call, got ${(lastOutputItem as ResponseOutputItem)?.type}`
);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
frac
approved these changes
Apr 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a bug where the Responses streaming layer emitted
response.output_item.donetwice for anmcp_list_toolsoutput item, surfacing as a duplicateTOOL_CALL_RESULTin downstream AG-UI consumers.Root Cause
Output-item finalization is split between two places in this codebase:
response.output_item.doneis emittedmessagecloseLastOutputItemreasoningcloseLastOutputItemfunction_callcloseLastOutputItemmcp_callcloseLastOutputItemmcp_approval_requestcloseLastOutputItemmcp_list_toolslistMcpToolsStream(full lifecycle)listMcpToolsStreamalready emits the full lifecycle for its own item (added→in_progress→completed→output_item.done). However, the previous shared branch incloseLastOutputItemmatched bothmcp_approval_requestandmcp_list_tools:When
handleOneTurnStreamlater calledcloseLastOutputItem(e.g. before transitioning to reasoning/text output) and the last output item was still that samemcp_list_tools, this branch re-emittedresponse.output_item.donefor it.Why this only manifests for
mcp_list_toolsPydanticAI's Responses adapter maps each
response.output_item.donefor anMcpListToolsitem into aBuiltinToolReturnPart, andAGUIAdapterrenders each return as aTOOL_CALL_RESULT. The duplicateoutput_item.donetherefore surfaced as two identicalTOOL_CALL_RESULTevents for the sametoolCallIdin AG-UI.Other tool types (
function_call,mcp_call,mcp_approval_request) getoutput_item.doneexclusively fromcloseLastOutputItemand are never re-finalized, so they're unaffected.Before / After
Before
PydanticAI/AG-UI rendered:
After
AG-UI now emits exactly one
TOOL_CALL_RESULTfor the list-tools discovery.Fix
Split the combined branch in
closeLastOutputItemso:mcp_approval_requestkeeps its existing close behavior (it is genuinely closed only here).mcp_list_toolsis treated as already finalized bylistMcpToolsStreamand produces no events.Event schema is unchanged; this only prevents the redundant completion. No behavioral change for any other item type.
Test plan
pnpm run test -- src/routes/responses/mcpStream.test.ts src/routes/responses/closeOutputItem.test.ts src/routes/responses/handleOneTurn.test.ts(all 148 tests pass)pnpm run check(tsc clean)closeOutputItem.test.ts: closing when last item ismcp_list_toolsyields no eventsmcpStream.test.ts:listMcpToolsStreamemits exactly oneresponse.output_item.done