Skip to content

fix(agents-core): handle same-server MCP tool name collisions - #1935

Merged
seratch merged 7 commits into
openai:mainfrom
BlueX888:fix/prep-mcp-intra-server-duplicate-tool-names
Sep 25, 2026
Merged

seratch merged 7 commits into
openai:mainfrom
BlueX888:fix/prep-mcp-intra-server-duplicate-tool-names

Conversation

@BlueX888

Copy link
Copy Markdown
Contributor

Summary

getAllMcpTools only detected duplicate function tool names across MCP servers. Two tools from the same server whose names normalize to one function tool name were both returned under that name, so only one of them stayed reachable by the model.

Why

Every MCP tool name is passed through toFunctionToolName, which replaces every non-alphanumeric character with _ (packages/agents-core/src/utils/tools.ts:87). Raw MCP names such as search-a and search_a therefore both become search_a.

The guard in getAllMcpTools compared the current server's names against only the names allocated by earlier servers, and collapsed the current batch through a Set first, so a repeat inside the batch never reached the comparison:

  • packages/agents-core/src/mcp.ts:892-900 on main (default path, includeServerInToolNames: false)
  • packages/agents-core/src/mcp.ts:865-873 on main (includeServerInToolNames: true)

In the prefixed path the collision originated in buildPrefixedToolNameOverrides (packages/agents-core/src/mcp.ts:1050): getSafeToolNamePart treats both - and _ as safe and preserves them, so search-a and search_a produced different prefixed base names, baseNameCounts counted one occurrence of each, no hash suffix was forced, and the two names collided only once toFunctionToolName normalized them.

Changes

  • packages/agents-core/src/mcp.ts — both getAllMcpTools paths now use a findDuplicateToolNames helper that reports a name repeating inside the current batch as well as a name already allocated by an earlier server. The non-prefixed path raises the existing UserError, matching the cross-server behavior.
  • packages/agents-core/src/mcp.ts — buildPrefixedToolNameOverrides counts and reserves prefixed names by their normalized function tool name, so a same-server collision forces the same deterministic hash suffix that other prefixed collisions already use. Both tools stay reachable instead of one being dropped by resolveModelVisibleToolNameCollisions.
  • .changeset/mcp-intra-server-tool-name-collisions.md — patch changeset for @openai/agents-core.

Test plan

Added throws when one server returns tool names that normalize to one function tool name, and extended the existing allocates normalized MCP tool name collisions stably with the search-a / search_a pair, in packages/agents-core/test/mcpCache.test.ts.

On the unpatched tree, before the fix:

$ pnpm exec vitest run packages/agents-core/test/mcpCache.test.ts --project @openai/agents-core

 FAIL  |@openai/agents-core| test/mcpCache.test.ts > MCP tools uniqueness > throws when one server returns tool names that normalize to one function tool name

- Expected:
Error {

+ Received:
[

 FAIL  |@openai/agents-core| test/mcpCache.test.ts > MCP tools uniqueness > allocates normalized MCP tool name collisions stably

AssertionError: expected 1 to be 2 // Object.is equality

- Expected
+ Received

- 2
+ 1

 Test Files  1 failed (1)
      Tests  2 failed | 67 passed (69)

With the fix:

$ pnpm exec vitest run packages/agents-core/test/mcpCache.test.ts --project @openai/agents-core

 Test Files  1 passed (1)
      Tests  69 passed (69)

Full verification on d187747e9b905a515608bc1d6720d0e52b766b5e plus this commit:

$ bash .agents/skills/code-change-verification/scripts/run.sh

[build-check] pnpm -r build-check exited with code 0
[dist-check] pnpm -r -F "@openai/*" dist:check exited with code 0
[test]  Test Files  227 passed (227)
[test]      Tests  6936 passed (6936)
[test] pnpm test exited with code 0
code-change-verification: all commands passed.

pnpm test:examples also passes. One earlier local run of the full suite reported two PTY-signal failures in packages/agents-core/test/sandboxes/unixLocal.signals.test.ts; that file contains no MCP code and fails the same way with this change stashed away, so it is unrelated to this diff.

Issue number

Closes #1934

Checks

  • I've added new tests (if relevant)
  • I've added/updated the relevant documentation
  • I've run pnpm test and pnpm test:examples
    • (If you made a major change) I've run pnpm test:integration (see details)
  • I've run .agents/skills/code-change-verification/scripts/run.sh
  • I've confirmed all verification steps pass
  • If using Codex, I've run /review before submitting this PR
  • I've added a changeset using pnpm changeset to indicate my changes

`getAllMcpTools` compared the current server's tool names against only the
names allocated by earlier servers, and collapsed the current batch through a
`Set` first, so two tools from the same server that normalize to one function
tool name were both returned under that name. `toFunctionToolName` rewrites
every non-alphanumeric character to `_`, so raw names such as `search-a` and
`search_a` both become `search_a`.

- Detect names that repeat inside the current batch as well, instead of
  collapsing them before the comparison, and use the same helper in both the
  prefixed and non-prefixed paths.
- Count and reserve prefixed names by their normalized function tool name in
  `buildPrefixedToolNameOverrides`, so an intra-server collision forces the
  same deterministic hash suffix already used for other prefixed collisions
  instead of surfacing only after normalization.
@BlueX888
BlueX888 requested a review from a team as a code owner September 15, 2026 14:18
@changeset-bot

changeset-bot Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f754145

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@openai/agents-core Patch
@openai/agents-extensions Patch
@openai/agents-openai Patch
@openai/agents-realtime Patch
@openai/agents Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@jbeckwith-oai jbeckwith-oai 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.

Reviewed the complete three-file change at 3281882 against d187747, including #1934, the existing allocation/dispatch paths, and the v0.18.0 compatibility baseline.

Need status: demonstrated. This is a supported tool-identity failure with a caller-visible consequence, and the patch is merge-worthy as-is. I found no blocking correctness, compatibility, or scope issues.

The root cause is twofold: the default check converts each server's names to a Set before checking only previously processed servers, hiding duplicates within a server; the prefixed allocator compares names before the final toFunctionToolName normalization, so search-a and search_a appear distinct until both become mcp_docs__search_a. The runner then retains only one function under its default collision policy.

I independently reproduced both levels of the problem using source snapshots of the exact base and head, synthetic MCP discovery/call stubs, and the SDK's ScriptedModel:

  • Running the PR's mcpCache tests against the base produces exactly the two reported failures: 67 pass and 2 fail. Both failures disappear on the PR head.
  • Through the public run API, the base exposes only one prefixed tool and invokes only the original search_a tool. The head exposes two unique tools and invokes both original names, search-a and search_a. This holds for streaming and non-streaming runs.
  • Without prefixing, the head raises UserError before any model request or MCP invocation, rather than silently allowing the runner to drop a tool.
  • Additional controls confirm that a normalized local function name is reserved correctly, and filtering out one colliding tool avoids an unnecessary error.

The implementation is placed at the right boundary. findDuplicateToolNames checks both intra-batch and prior-server duplicates without mutating the caller's set. Applying the existing normalization to the allocator's counts, reservation checks, and used-name checks closes the same identity gap throughout allocation. Keeping raw tool names in the hash seed and dispatch closure preserves deterministic disambiguation and correct remote invocation. Changing global function-name normalization would have a much broader compatibility impact; reusing this existing allocator is the narrower fix. Configuring prefixes alone does not solve the problem on the base.

I also inspected the affected identity/security boundary: server-name redaction still occurs before prefix/hash allocation, and this patch does not change MCP approval, guardrail, credential, or execution configuration. Existing focused coverage for redaction, guardrail isolation, cache rebinding/filtering, reserved handoff names, and long-name allocation passes.

Validation: 139 tests passed across mcpCache, mcpToFunctionTool, mcpToolFilter, runner/modelPreparation, and six independent reproduction/control cases. The six controls also pass against the base with expectations for its observed broken behavior. ESLint passed for both changed TypeScript files; Prettier passed for all three changed files; git diff --check passed.

Limits: these were deterministic local tests, not live model/MCP service calls. I did not rerun the full monorepo verification stack. GitHub CI for this head currently reports action_required, so this code approval does not establish CI readiness.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-16T22:38:18.172002Z 21d6543 New commits
🔒 Security Review ✅ Completed 2026-09-16T22:40:08.541587Z 21d6543 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

jbeckwith-oai and others added 2 commits September 16, 2026 15:36
The existing coverage pins the allocated names, but not the outcome a user
reports: a second tool on the same server disappearing from the run. This
drives run() with a ScriptedModel that calls both advertised tools and
asserts the server receives both original names, in streaming and
non-streaming modes.

Against the current base it fails with `expected 1 to be 2` on both modes,
because only one tool is advertised.
@seratch
seratch enabled auto-merge (squash) September 20, 2026 03:07
@seratch seratch modified the milestones: 0.18.x, 0.19.x Sep 20, 2026
@seratch
seratch disabled auto-merge September 22, 2026 02:07
@jbeckwith-oai
jbeckwith-oai enabled auto-merge (squash) September 25, 2026 16:15
@seratch
seratch disabled auto-merge September 25, 2026 22:06
@seratch
seratch merged commit fdaf0a6 into openai:main Sep 25, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants