Repository navigation
Conversation
MohammedAlkindi
left a comment
There was a problem hiding this comment.
The fix is correct for the real API. response.function_call_arguments.delta and .done carry item_id, while the buffer was keyed by the call_id read off response.output_item.added, so callBuffers.get(callId) never matched and neither event was ever emitted.
One narrowing worth a decision. The old lookup read parsed.call_id, so a producer that identifies argument events by call_id matched before and does not now, because the buffer is keyed on item.id whenever that field is present. #2553 was opened about five hours after this one, makes the same item_id change, and keeps a fallback that also matches a buffer by its stored callId. The two PRs disagree only on that compatibility path, so it is probably worth picking one rather than carrying both.
The streamNativeAgentSteps test is the stronger half here. It pins the symptom a user actually reports, AgentSteps never arriving for a tool call, rather than only the event shape.
Generated by Claude Code
|
Thanks — both points taken, and the narrowing is gone in 519e7c2. On picking one: @Elioooon withdrew #2553 the same morning, so this is the only PR left on #2550 and there is nothing to reconcile. That also removed the reason to keep the two behaviours mutually exclusive, so I kept the compatibility path rather than dropping it. The buffer is now indexed under both identities. The item id is tried first and the fallback only runs when it misses, so a spec-conformant producer takes exactly the path you verified against the real API: // response.output_item.added
callBuffers.set(typeof item.id === "string" ? item.id : callId, buffer);
callBuffers.set(callId, buffer);
// response.function_call_arguments.{delta,done}
const buf = callBuffers.get(itemId) ?? callBuffers.get(callId);Emitted events still report the protocol Verification, run in an installed workspace (
|
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
@MohammedAlkindi agreed on the root cause, and the decision point is settled on this side in the two pushes after your review:
Since #2553 is closed, this PR carries the single unified path: primary |
24174d0 to
e232ff2
Compare
`response.function_call_arguments.delta`/`.done` carry `item_id`, not `call_id`, so the callBuffers lookup always fell back to "" and never matched the buffer registered by `response.output_item.added`. Neither `tool-call-args-delta` nor `tool-call-ready` was emitted on the Responses path, so `streamNativeAgentSteps` saw no ready calls and `MCPAgent.stream()`/`streamEvents()` reported no AgentStep for tool calls made through OpenAIResponsesDriver.
Index the buffer under both the output item id and the protocol call id, so
a producer that identifies `response.function_call_arguments.{delta,done}`
by `call_id` keeps matching. Spec-conformant producers are unaffected: the
item id is tried first and the fallback only runs when it misses.
Adds a regression test that feeds the same SSE sequence with the argument
events correlated by `call_id`; it fails on the previous commit and passes
here, while the existing item_id test keeps passing.
Storing every buffer under both the item id and the call_id in one flat Map let one call's item id collide with another call's call_id: the second call's output_item.added overwrote the key the first call was stored under, so the first call's argument events were emitted against the second call's tool name and call id. The two identifiers come from the producer and share no namespace, so they are prefixed with item: and call: and can no longer overlap. The call_id-only producer test is also parameterized over a producer that omits the output item's id, and a two-call case covers the collision.
e232ff2 to
ac79030
Compare
The Responses API sends function_call_arguments delta/done with item_id, and this provider only serves the official OpenAI API, so the call_id fallback key had no producer. Drop it and its tests. Claude-Session: https://claude.ai/code/session_01MEJoDUZb843zNMtq3jGMXu
…l-call-ready #2551 made the OpenAI Responses provider emit tool-call-ready, so chat tool-invocation parts carry their arguments again and the drawer test passes. Closes #2769. Claude-Session: https://claude.ai/code/session_01MEJoDUZb843zNMtq3jGMXu
* fix(cli): quote client SDK install args on Windows (#2853) * fix(cli): quote client SDK install args on Windows On Windows spawnAndWait ran npm/pnpm through cmd.exe with an args array. cmd.exe strips ^ from @mcp-use/client@^2.0.0-alpha.0, so the package manager asked for the unpublished 2.0.0-alpha.0 and mcp-use client / mcp-use screenshot auto-install always failed. Quote each argument and pass one command string, matching create-mcp-use-app's runPackageManager (#2696); this also drops the DEP0190 warning. * chore(cli): add changeset for Windows client install fix * test(cli): keep only the argument-quoting cases in the spawn test * test(cli): clarify that only Windows parses the spawn arguments through a shell * fix(cli): read the client peer range from @mcp-use/cli readClientPeerRange() still looked for a package.json named mcp-use, so after the move into @mcp-use/cli it never found the declared peer range and every auto-install fell back to ^2.0.0-alpha.0. Check for @mcp-use/cli, test that the declared range is returned, and use ^2.0.1 as the fallback. * chore: enter canary prerelease mode * chore(typescript): version packages (canary) * fix(inspector): surface streamed chat failures (#2877) * chore(typescript): version packages (canary) * fix(agent): emit OpenAI Responses tool-call args events keyed by item_id (#2551) * fix(agent): emit OpenAI Responses tool-call args by item_id `response.function_call_arguments.delta`/`.done` carry `item_id`, not `call_id`, so the callBuffers lookup always fell back to "" and never matched the buffer registered by `response.output_item.added`. Neither `tool-call-args-delta` nor `tool-call-ready` was emitted on the Responses path, so `streamNativeAgentSteps` saw no ready calls and `MCPAgent.stream()`/`streamEvents()` reported no AgentStep for tool calls made through OpenAIResponsesDriver. * fix(agent): keep the call_id lookup for argument events Index the buffer under both the output item id and the protocol call id, so a producer that identifies `response.function_call_arguments.{delta,done}` by `call_id` keeps matching. Spec-conformant producers are unaffected: the item id is tried first and the fallback only runs when it misses. Adds a regression test that feeds the same SSE sequence with the argument events correlated by `call_id`; it fails on the previous commit and passes here, while the existing item_id test keeps passing. * fix(agent): key the tool-call buffers by identifier namespace Storing every buffer under both the item id and the call_id in one flat Map let one call's item id collide with another call's call_id: the second call's output_item.added overwrote the key the first call was stored under, so the first call's argument events were emitted against the second call's tool name and call id. The two identifiers come from the producer and share no namespace, so they are prefixed with item: and call: and can no longer overlap. The call_id-only producer test is also parameterized over a producer that omits the output item's id, and a two-call case covers the collision. * fix(agent): correlate Responses argument events by item_id only The Responses API sends function_call_arguments delta/done with item_id, and this provider only serves the official OpenAI API, so the call_id fallback key had no producer. Drop it and its tests. Claude-Session: https://claude.ai/code/session_01MEJoDUZb843zNMtq3jGMXu --------- Co-authored-by: Andrew Khadder <[email protected]> * test(inspector): expect tool drawer args now that Responses emits tool-call-ready #2551 made the OpenAI Responses provider emit tool-call-ready, so chat tool-invocation parts carry their arguments again and the drawer test passes. Closes #2769. Claude-Session: https://claude.ai/code/session_01MEJoDUZb843zNMtq3jGMXu * chore(typescript): version packages (canary) * fix(agent): return only the final turn's text from MCPAgent.stream() (#2723) * fix(agent): return only the final turn's text from MCPAgent.stream() streamNativeAgentSteps appended every text-delta to the return value, so text the model wrote before a tool call was glued onto the final answer and saved to memory. Reset it after each tool result, matching runNativeAgent, which keeps only the turn without tool calls. * test(agent): cover maxSteps ending the run after tool results Both stream() and run() return an empty string in that case. Also reset the collected text only for tool results that match a reported tool call, so ignored events have no effect. * fix(agent): drop a tool-call turn's text when the stream is aborted before the tool runs Decide per turn instead of on tool results: a turn that requests tools is intermediate, so an abort between the tool call and its dispatch returns an empty answer, as runNativeAgent does. * fix(agent): reset stream() turn text on every tool result The reset ran only after a pending step was found, and steps exist only after tool-call-ready, so on the OpenAI Responses provider (which never emitted tool-call-ready) the preamble text was still glued onto the answer. Reset on every tool-result and mark a tool-calling turn on tool-call-start, which every provider emits. Adds a regression test with real Responses event shapes. Claude-Session: https://claude.ai/code/session_01MEJoDUZb843zNMtq3jGMXu --------- Co-authored-by: Andrew Khadder <[email protected]> * chore(typescript): version packages (canary) * fix(client): close OAuth loopback connections on teardown (#2766) * chore(typescript): version packages (canary) * fix(agent): treat missing error field as success in RemoteAgent.run (#2651) * fix(agent): treat missing error field as success in RemoteAgent.run A 200 response body like {"result": "..."} has no error key, so result.error is undefined and the result.error !== null check flagged every such success response as a failure; only bodies carrying an explicit error: null passed. Compare with != null so an absent error field counts as no error, while real error values and status: "error" bodies still reject. Also stringify the error value so object errors render as JSON instead of [object Object]. * fix(agent): keep string error messages unquoted in remote error path JSON.stringify(result.error) wrapped primitive string errors in quotes and produced undefined for status-only error responses. Render primitive error values with String(), keep JSON.stringify for object errors, and fall back to serializing the whole response body when no error payload exists. * fix(agent): simplify RemoteAgent error message and drop dead init check Build the message in one expression and let the catch add the "Remote agent execution failed:" prefix once, instead of twice. Remove the "failed to initialize" check: String() of a response object is always "[object Object]", so it could never match (#2851). Claude-Session: https://claude.ai/code/session_01MEJoDUZb843zNMtq3jGMXu --------- Co-authored-by: Andrew Khadder <[email protected]> * chore(typescript): version packages (canary) * fix(cli): join inspector paths through one basePath rule (#2555) * fix(cli): join inspector paths through one basePath rule views/document.ts special-cased basePath "/" while inspector-route.ts and dev-api.ts interpolated directly, so the root produced "//inspector". That made `mcp-use start --with-inspector` advertise /inspector while the mount guard matched //inspector, and left the three dev-API routes unreachable. The rule moves to src/base-path.ts and every consumer joins through it. The two URL sites in dev.ts move with the predicate: dev was self-consistent in the doubled form, so changing only the guard would have broken it. * fix(cli): return basePath for an empty child and correct the inspector docs Addresses review on #2555. pathUnderBase("/mcp", "") returned "/mcp/", contradicting the documented "never containing an empty segment". An empty child now yields basePath. The Inspector integration guide told readers to set basePath: '' to serve the Inspector at /inspector, but assertServerConfig rejects an empty string because basePath must start with "/". The working value is '/', which is exactly the case this PR repairs. Documented that, and replaced the naive `${basePath}/inspector` in the options table and the v2 CLI reference with the normalized rule. * docs(inspector): scope the basePath root note to server config My previous commit claimed an empty basePath is rejected, which is wrong in the mountInspector section it sits in: registerInspectorShell defaults basePath to "" and builds "/inspector" from it, so "" is the inspector's own root value. Only MCPServer config rejects it, via assertServerConfig. Restores the original `''` guidance for mountInspector and adds the MCPServer caveat separately. * docs: drop unrelated doc edits from this fix Claude-Session: https://claude.ai/code/session_01MEJoDUZb843zNMtq3jGMXu --------- Co-authored-by: Aishwary Dongre <[email protected]> Co-authored-by: Andrew Khadder <[email protected]> * fix(client): pin OAuth token expiry when tokens are saved (#2879) getOAuthTokenExpiry fell back to Date.now() + expires_in for opaque tokens on every read, so useMcp().authTokens.expires_at moved later on each reconnect. Save the absolute expiry with the tokens and prefer it over expires_in. JWT exp still wins. Supersedes #2674. Claude-Session: https://claude.ai/code/session_01MEJoDUZb843zNMtq3jGMXu Co-authored-by: Arpitm544 <[email protected]> * chore(typescript): version packages (canary) * feat: customize MCP landing page from landing.tsx (#2648) Claude-Session: https://claude.ai/code/session_018JTsXB5q2SkN8rNAfS7vn4 * chore(typescript): version packages (canary) * fix: landing page dark mode, page-owned Tailwind, and dev styles before hydration (#2880) * feat(server): follow the OS color scheme on landing pages; link dev page CSS The managed Tailwind `dark:` variant keys off `data-theme`, which nothing set on a landing page outside a host. Mirror `prefers-color-scheme` into it from an inline head script before the first paint, until the page sets its own value. Dev loads can also pass the stylesheets the page imports, linked in the head and marked for the dev entry to remove once its imports insert them. Claude-Session: https://claude.ai/code/session_018JTsXB5q2SkN8rNAfS7vn4 * fix(cli): let landing pages own Tailwind; style dev markup before hydration A landing stylesheet with its own `@import "tailwindcss"` now replaces the managed copy instead of compiling beside it, so the page's `dark` variant is the only one. The landing page gets its own managed stylesheet id, separate from the views' copy. In dev, the head links the stylesheets in the page's module graph, and only those extra URLs pass the dev server's Vite route. Claude-Session: https://claude.ai/code/session_018JTsXB5q2SkN8rNAfS7vn4 * docs(server): document landing dark mode and page-owned Tailwind Claude-Session: https://claude.ai/code/session_018JTsXB5q2SkN8rNAfS7vn4 * chore(typescript): version packages (canary) * fix(inspector): start connection timeout fields blank (#2881) The Connect form pre-filled 10000 / 60000. Since #2656 saves those values and tool calls honor them, every server added from the dashboard capped tool calls at 10s per request and 60s overall. Blank means the 10-minute default and no overall limit, as the help text says. Claude-Session: https://claude.ai/code/session_01MEJoDUZb843zNMtq3jGMXu * chore(typescript): version packages (canary) * test(inspector): wait for the settings form before editing the URL (#2882) The dashboard Connect form renders the same connection-form-url-input and stays mounted until the view transition to Connection Settings finishes, so waiting for visibility let fills land in the dashboard form. Save then kept the old URL and the toast still fired. Wait for the connection's URL instead, as the other settings tests already do. Claude-Session: https://claude.ai/code/session_01MEJoDUZb843zNMtq3jGMXu * fix(inspector): JSON-only OAuth metadata responses; per-caller proxy limits (#2855) * fix(inspector): JSON-only OAuth metadata responses; per-caller proxy limits The OAuth BFF relayed /oauth/metadata bodies with the upstream Content-Type, including on error responses, and accepts top-level navigations (no Origin). A link to /inspector/api/oauth/metadata?serverUrl=<attacker> therefore rendered attacker HTML on the host's origin (manufact.com for Manufact Cloud, inspector.manufact.com for the hosted Inspector). Metadata responses are now always application/json with nosniff; /oauth/proxy already answered with a JSON envelope. The proxy and OAuth BFF consumed one limiter key for every request (the stringified Hono request), so one busy caller exhausted the 120/min budget for everyone on the process. Each caller (client IP) now gets that budget, under a process-wide ceiling of 1,200/min for rotating or spoofed keys. The two copies of the limiter middleware became one helper. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(inspector): key proxy budgets on the proxy-added X-Forwarded-For entry X-Real-IP and the leftmost X-Forwarded-For entry can be set by the client, so each spoofed value got a fresh budget and only the process ceiling held. The rightmost entry is the one the platform proxy adds (Vercel overwrites the header, Railway appends), so a client behind it cannot choose its budget. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(inspector): skip the per-caller budget without X-Forwarded-For A local Inspector or mcp-use dev has no platform proxy adding X-Forwarded-For, so every request fell into one "unknown" bucket of 120/min, the same shared budget as before. The e2e suite drains it and gets 429s. Without the header callers can't be told apart, so apply only the process-wide ceiling. Claude-Session: https://claude.ai/code/session_01MEJoDUZb843zNMtq3jGMXu --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Andrew Khadder <[email protected]> * chore(typescript): version packages (canary) * docs: changelog for mcp-use v2.11.0 and inspector v20.3.22 Claude-Session: https://claude.ai/code/session_01MEJoDUZb843zNMtq3jGMXu --------- Co-authored-by: Vansh Bhardwaj <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: 哈基米 <[email protected]> Co-authored-by: Freetrip <[email protected]> Co-authored-by: aishwary-dongre <[email protected]> Co-authored-by: Aishwary Dongre <[email protected]> Co-authored-by: Arpitm544 <[email protected]> Co-authored-by: Enrico Toniato <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
Pull Request Description
Language / Project Scope
Check all that apply:
Changes
OpenAIResponsesDriverstreaming never emittedtool-call-args-deltaortool-call-ready, soMCPAgent.stream()/agent.streamEvents()yielded zeroAgentSteps for tool calls on the Responses path (provider
"openai", andOpenRouter models routed to this driver). The tool still executed, so the bug
was silent.
callBuffersis now keyed by the output item id (item.id,fc_...), which iswhat
response.function_call_arguments.delta/.doneactually carry, whilethe emitted events keep yielding the MCP-facing
call_id. The buffer storesboth so
tool-call-ready'stoolCallIdstill matches thecall_idused bytool-result.Review Readiness
Help maintainers review this quickly:
Implementation Details
libraries/typescript/packages/agent/src/llm/providers/openai-responses.tscallBuffersis keyed byitem.idfromresponse.output_item.addedinstead of
call_id.callIdfield so the storedcall_idsurvives.response.function_call_arguments.deltaand...donenow look upparsed.item_id, and emittoolCallId: buf.callId— thecall_id, notthe item id.
TypeScript Checklist
Packages Modified
Check all packages that were modified:
docstestsclicreate-mcp-use-appmcp-use(server)mcp-use(client)inspector@mcp-use/agent(the package containingsrc/llm/).Pre-commit Checklist
Ensure all of the following have been completed:
pnpm lint:fixto auto-fix linting issuespnpm formatto format code with Prettierpnpm buildand build succeeds without errorspnpm changesetto create a changeset (if this PR includes user-facing changes)docs/folder if neededPython Checklist
N/A — TypeScript only.
Example Usage (Before)
Example Usage (After)
Documentation Updates
Testing
Regression test (
libraries/typescript/packages/agent/src/llm/__tests__/openai-responses.test.ts),using the real Responses SSE event shapes with
fetchstubbed:emits tool-call-args-delta and tool-call-ready against the call_id— assertsthe driver's event stream contains the two
tool-call-args-deltaevents andthe
tool-call-readywithtoolCallId: "call_abc".yields AgentSteps for a streamed OpenAI Responses tool call— runs the fullstreamNativeAgentSteps(OpenAIResponsesDriver)path and asserts the twoAgentSteps.
Both fail on the unpatched source and pass after the fix. The pre-existing
parses function_call_arguments.done into tool-call-ready shapecase wasreplaced: it only asserted
JSON.parseof an inline object literal and nevercalled into
streamResponsesTurn, and it encoded the incorrect assumption thatthe done event carries
call_id.Edge cases considered: argument events are correlated by
item_idonly. The Responses API (OpenAI, and OpenRouter's drop-in Responses endpoint) always sendsitem_idon these events, so there is nocall_idfallback. An unknownitem_idin a delta/done still yields nothing rather than throwing.Backwards Compatibility
Backwards compatible. Consumers that only read the final text are unaffected;
consumers of
tool-call-ready/MCPAgent.stream()now receive the steps thatwere previously dropped. The
toolCallIdon emitted events is still thecall_id, so tool-result correlation is unchanged.Related Issues
Closes #2550
Summary by cubic
Fixes OpenAI Responses streaming so tool calls now emit
tool-call-args-deltaandtool-call-readyevents. Previously, the driver looked up argument events bycall_ideven though Responses sendsitem_id, soMCPAgent.stream()andstreamEvents()dropped tool-call AgentSteps; the tool still executed, so the bug was silent.Bug Fixes
response.function_call_arguments.delta/.donecarry asitem_id; thecall_idfallback is dropped since the official Responses API only sendsitem_id.call_idintoolCallId, so tool-result correlation is unchanged.Written for commit 3162f78. Summary will update on new commits.