Skip to content

fix(agent): emit OpenAI Responses tool-call args events keyed by item_id - #2551

Merged
khandrew1 merged 4 commits into
mcp-use:canaryfrom
BlueX888:fix/prep-openai-responses-call-id-never-matches
Oct 19, 2026
Merged

khandrew1 merged 4 commits into
mcp-use:canaryfrom
BlueX888:fix/prep-openai-responses-call-id-never-matches

Conversation

@BlueX888

@BlueX888 BlueX888 commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Pull Request Description

Language / Project Scope

Check all that apply:

  • TypeScript
  • Python
  • Documentation only
  • CI/CD or tooling

Changes

OpenAIResponsesDriver streaming never emitted tool-call-args-delta or
tool-call-ready, so MCPAgent.stream() / agent.streamEvents() yielded zero
AgentSteps for tool calls on the Responses path (provider "openai", and
OpenRouter models routed to this driver). The tool still executed, so the bug
was silent.

callBuffers is now keyed by the output item id (item.id, fc_...), which is
what response.function_call_arguments.delta / .done actually carry, while
the emitted events keep yielding the MCP-facing call_id. The buffer stores
both so tool-call-ready's toolCallId still matches the call_id used by
tool-result.

Review Readiness

Help maintainers review this quickly:

  • The PR is scoped to one issue or one clearly related change
  • The PR description explains the user-visible problem and the fix
  • The diff avoids unrelated formatting, generated files, and bulk rewrites
  • I verified the affected package/docs path with the commands listed below
  • I checked for existing open PRs that already solve the same issue

Implementation Details

  1. libraries/typescript/packages/agent/src/llm/providers/openai-responses.ts
    • callBuffers is keyed by item.id from response.output_item.added
      instead of call_id.
    • The buffer value gains a callId field so the stored call_id survives.
    • response.function_call_arguments.delta and ...done now look up
      parsed.item_id, and emit toolCallId: buf.callId — the call_id, not
      the item id.
  2. No dependency changes. No public API change.
  3. A patch changeset is included, as required for TypeScript changes.

TypeScript Checklist

Packages Modified

Check all packages that were modified:

  • docs
  • tests
  • cli
  • create-mcp-use-app
  • mcp-use (server)
  • mcp-use (client)
  • inspector

@mcp-use/agent (the package containing src/llm/).

Pre-commit Checklist

Ensure all of the following have been completed:

  • Ran pnpm lint:fix to auto-fix linting issues
  • Ran pnpm format to format code with Prettier
  • Ran pnpm build and build succeeds without errors
  • Ran pnpm changeset to create a changeset (if this PR includes user-facing changes)
  • Added or updated tests if needed
  • Updated documentation in docs/ folder if needed

Python Checklist

Complete this section if your PR includes Python changes

N/A — TypeScript only.

Example Usage (Before)

const agent = new MCPAgent({ llm: "openai/gpt-5", mcpClient });
let steps = 0;
for await (const step of agent.stream({ prompt: "What is the weather in Paris?" })) {
  steps++;
}
// steps === 0 — the final text arrives, but the tool call produces no step

Example Usage (After)

const agent = new MCPAgent({ llm: "openai/gpt-5", mcpClient });
let steps = 0;
for await (const step of agent.stream({ prompt: "What is the weather in Paris?" })) {
  steps++;
}
// steps === 2 — one step on tool-call-ready, one on tool-result

Documentation Updates

  • None — internal streaming event mapping, no documented behavior changes.

Testing

Regression test (libraries/typescript/packages/agent/src/llm/__tests__/openai-responses.test.ts),
using the real Responses SSE event shapes with fetch stubbed:

  • emits tool-call-args-delta and tool-call-ready against the call_id — asserts
    the driver's event stream contains the two tool-call-args-delta events and
    the tool-call-ready with toolCallId: "call_abc".
  • yields AgentSteps for a streamed OpenAI Responses tool call — runs the full
    streamNativeAgentSteps(OpenAIResponsesDriver) path and asserts the two
    AgentSteps.

Both fail on the unpatched source and pass after the fix. The pre-existing
parses function_call_arguments.done into tool-call-ready shape case was
replaced: it only asserted JSON.parse of an inline object literal and never
called into streamResponsesTurn, and it encoded the incorrect assumption that
the done event carries call_id.

cd libraries/typescript/packages/agent

# new test — 8 passed (8)
pnpm vitest run src/llm/__tests__/openai-responses.test.ts

# module tests — 10 files, 48 passed (48)
pnpm vitest run src/llm

# full package unit suite — 18 files, 140 passed | 1 skipped
pnpm test:unit

# typecheck — no output, exit 0
pnpm exec tsc --noEmit

cd libraries/typescript

# format — All matched files use Prettier code style!
pnpm prettier --check packages/agent/src/llm/providers/openai-responses.ts \
  packages/agent/src/llm/__tests__/openai-responses.test.ts

# lint — no output, exit 0
pnpm eslint packages/agent/src/llm/providers/openai-responses.ts \
  packages/agent/src/llm/__tests__/openai-responses.test.ts

# changeset — @mcp-use/agent patch
pnpm changeset status --since=main

Edge cases considered: argument events are correlated by item_id only. The Responses API (OpenAI, and OpenRouter's drop-in Responses endpoint) always sends item_id on these events, so there is no call_id fallback. An unknown item_id in 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 that
were previously dropped. The toolCallId on emitted events is still the
call_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-delta and tool-call-ready events. Previously, the driver looked up argument events by call_id even though Responses sends item_id, so MCPAgent.stream() and streamEvents() dropped tool-call AgentSteps; the tool still executed, so the bug was silent.

Bug Fixes

  • Keys argument-event buffers by the output item id, which is what response.function_call_arguments.delta / .done carry as item_id; the call_id fallback is dropped since the official Responses API only sends item_id.
  • Emitted events keep the provider call_id in toolCallId, so tool-result correlation is unchanged.
  • Adds regression coverage for the SSE correlation path and end-to-end AgentSteps.

Written for commit 3162f78. Summary will update on new commits.

View guided diff

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 3 files

Re-trigger cubic

@MohammedAlkindi MohammedAlkindi 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.

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

@BlueX888

Copy link
Copy Markdown
Contributor Author

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 call_id as toolCallId, unchanged.

Verification, run in an installed workspace (pnpm --filter @mcp-use/agent test:run src/llm/__tests__/openai-responses.test.ts):

  • 9 passed. toolCallEvents now takes a correlation argument, and the new case feeds the same sequence with the argument events carrying call_id.
  • I checked the new test is load-bearing rather than decorative: on the previous commit it fails with expected [...(2)] to deep equally contain { type: 'tool-call-args-delta', ... } — only tool-call-start comes through — and it passes here. The existing item_id test passes on both.
  • prettier --check clean on both files.
  • The full @mcp-use/agent suite is 12 failed / 64 passed on this head, and 12 failed / 62 passed on a clean main in the same workspace; the failures are all Failed to resolve entry for package "@mcp-use/client" from an unbuilt workspace dependency, so they are not from this change. Hosted CI stays authoritative for that reason.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@BlueX888

Copy link
Copy Markdown
Contributor Author

@MohammedAlkindi agreed on the root cause, and the decision point is settled on this side in the two pushes after your review:

  • 519e7c2 keeps the compatibility lookup — argument events match by item_id first and fall back to the stored call_id, so a producer that identifies argument events by call_id still matches, as before. The emitted toolCallId also now comes from the buffer's registered callId (the one from output_item.added) rather than whatever identifier the event happened to carry.
  • 24174d0 separates the two key namespaces (item: / call:) instead of sharing one flat map, so one call's item id can no longer collide with — and overwrite — another call's call-id entry. That collision was real at 519e7c2, reproduced by a two-call stream where the second call's call_id equals the first call's item id, and is now pinned by a regression test; the producer-shape tests are parameterized over items that set id and items that omit it.

Since #2553 is closed, this PR carries the single unified path: primary item_id keying with a namespaced call_id fallback, both O(1) map lookups rather than a scan over the stored callIds. That keeps the compatibility #2553 was after, without the flat-namespace overwrite or the per-event linear scan.

@BlueX888
BlueX888 force-pushed the fix/prep-openai-responses-call-id-never-matches branch from 24174d0 to e232ff2 Compare October 8, 2026 10:12
`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.
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
@khandrew1
khandrew1 changed the base branch from main to canary October 19, 2026 16:42
@khandrew1
khandrew1 merged commit 91f70c0 into mcp-use:canary Oct 19, 2026
36 of 37 checks passed
khandrew1 added a commit that referenced this pull request Oct 19, 2026
…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
khandrew1 added a commit that referenced this pull request Oct 19, 2026
* 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants