MCP tool order is nondeterministic across runs (server groups iterate HashMap order)

Open 💬 2 comments Opened Aug 7, 2026 by skirdey

Summary

The model-visible tool list groups MCP tools by server, but the server groups are emitted in std::collections::HashMap iteration order. HashMap iteration order is seeded per-process, so the exact same MCP configuration produces a reshuffled tool order on every fresh codex / app-server run. This makes the attention order of tool namespaces unstable and hard to reproduce or replay.

Why it matters

  • Uncontrolled run-to-run variance in a model-visible list that prompts may describe or enumerate.
  • Snapshots/replays of the per-step tool catalog are not reproducible between runs.
  • The order also leaks into context — a separate server can "win" the first (most prominent) position depending only on process hash-seed luck.

Root cause

In codex-rs/codex-mcp/src/connection_manager/tool_catalog.rs:

  • capture_binding_with_metadata builds the model-visible binding by iterating self.servers.iter() (a HashMap).
  • list_all_tools does the same for non-model discovery/resync surfaces.

Both emit server groups in HashMap iteration order, which is deliberately randomized per process.

Reproduction

Build the same MCP connection set with two or more servers in two separate processes (or two independent McpConnectionSet instances) and inspect capture_binding(...).tools(): the server-group order differs between runs even though the configuration is identical.

Suggested fix

Iterate configured servers by name when building tool lists so ordering is deterministic across runs:

  • sort the servers.iter() collection by key in capture_binding_with_metadata and in list_all_tools, then join_all over the sorted order.

This keeps the grouping intact while making the model-visible order stable and name-sorted.

Ready branch

I have a complete, green implementation on a public fork:

  • Branch: skirdey/codexclark/codex-main-21154b77
  • Commit 0ad07fc658 — sorts server iteration in both functions and adds regression test capture_binding_orders_servers_deterministically (builds two independent connection sets and asserts identical, name-sorted order; fails on pre-fix code).
  • Verified: cargo fmt --check clean, cargo clippy --tests -p codex-mcp clean, full codex-mcp suite (~165 tests) and codex-core tests pass.

Note on PR

I attempted to open this as a pull request, but openai/codex currently restricts PR creation to collaborators (skirdey is not one), so the PR cannot be opened externally. Per docs/contributing.md (invitation-only external contributions), I'm filing this issue first with analysis and a ready branch. Happy to open the PR if the team invites it; the change is small, well-scoped, and covered by a regression test.

View original on GitHub ↗

2 Comments

ded-furby · 21 days ago

Thanks for the ready-to-cherry-pick branch and test evidence. I also see the repo still enforces invite-only external PR flow per contributing docs, so I can’t safely open a maintainer PR from this tokenized workspace. If you want me to proceed once invited, I’ll take the deterministic-order change through a maintainer-side branch/PR targeting both capture paths plus the added regression test. In the meantime, this looks complete and high-confidence from the description and is good to prioritize for PR review.

skirdey · 21 days ago

Great — happy to have it proceed. For the cherry-pick: the change is on skirdey/codex, branch clark/codex-main-21154b77, commit 0ad07fc658 — sorts server iteration in tool_catalog.rs (capture_binding_with_metadata + list_all_tools) with the capture_binding_orders_servers_deterministically regression test. Let me know if you'd like anything else.