The tool list the model sees is computed for one sample
Same chat, same config — the next sample may still add MCP names, a collab entry, or tool_search. What changed is which handlers this request is allowed to advertise.
tool_search.
- Resolve this step’s MCP bindingmcp.rs L310
- Collect the MCP catalog and hand it to the plannerturn.rs L1494
- Decide by identity whether to take the full sourcesspec_plan.rs L889
- Register shell, resources, utilities, collabspec_plan.rs L930
- Append MCP and apply the exposure policyspec_plan.rs L148
- Hang tool_search only if something is deferredspec_plan.rs L335
- Send the model only specs with is_directspec_plan.rs L484
- Freeze into StepContext.tool_routerstep_context.rs L44
- build_prompt reads the visible tableturn.rs L1320
build_tool_router. Line numbers on the logic trail match openai/codex commit 4f39251a01.You just attached MCP filesystem, and the model is still running this round. Freeze the tool table on a whole user turn, and a newly connected server waits for the next user message. Freeze it on every tool call, and two parallel calls in the same sample may see two different tables.
Advertise one table and execute another — or have the table change halfway through — that’s the usual way a tool table crashes.
Codex puts this sample’s model, approvals, environment, MCP binding, and the already-computed tool_router into StepContext. The comments call it request-scoped: it may change between sampling requests, not inside one. tool_router is the plan for this exact sampling request.
While the sample runs, the names the model sees and the runtime it can dispatch both read this snapshot. An MCP server dropping mid-sample cannot change a table already computed. The next sample walks mcp_runtime_for_step again; the binding may be reused, or swapped for an empty one.Source:codex-rs/core/src/session/step_context.rs lines 17–47;codex-rs/core/src/session/mcp.rs lines 348–357
So the freeze unit is the step, not the turn. One turn can sample more than once; each rebuilds a StepContext.Source:codex-rs/core/src/session/turn.rs lines 1318–1320
Visible and executable share one snapshot. The next round absorbs new connections. Rewrite in another language and the minimum shape is still one function: switches, MCP catalog, and identity in; visible table and executable table out. Compute once when the request starts, then freeze.
If the approval model sees the worker’s full MCP and collab table, it can spawn_agent or read external resources — the review itself is bypassed. Register everything then filter, and one missed branch ships a name that should never have gone out.
The planner picks sources by identity. A Guardian reviewer’s label must be exactly guardian. If the permission profile is not Managed, the function returns and the registry is empty. Managed with an environment: at most three — exec_command, write_stdin, optional view_image. Only a normal session walks the four groups — shell, MCP resources, utilities, collab — then appends MCP, extensions, and dynamic tools.Source:codex-rs/core/src/guardian/review.rs lines 212–220;codex-rs/core/src/tools/spec_plan.rs lines 144–164、lines 889–934
There is no first-class read-file entry either. The handlers directory has no ReadFileHandler. To read a file the model uses exec_command, MCP filesystem, or the exec-server API inside view_image.
Change the identity and whole source groups are skipped. The default closed door sits at the reviewer, a missing environment, and a feature off. That travels across languages.
MCP tool schemas are expensive. Hang them all and the initial request’s tokens get eaten by description text. Two servers both offering read_file collide unless you disambiguate.
Exposure splits the registry into visible faces. Direct enters the initial table. Deferred is left for tool_search. Hidden is left for dispatch. With search on, MCP tools default to Deferred and stay off the initial visible table.Source:codex-rs/tools/src/tool_executor.rs lines 51–80;codex-rs/core/src/mcp_tool_exposure.rs lines 90–94
finalize_tool_router hangs tool_search only when both are true: the model supports a search tool, and the registry still has at least one deferred tool with search_info. No deferred, it never enters the table.Source:codex-rs/core/src/tools/spec_plan.rs lines 335–370、lines 578–580
Same-named tools from two MCP servers are disambiguated at sanitize time: default mcp__ prefix, then a 12-character hash suffix if they still collide. An external tool that hits an occupied core name is skipped. Repo-root AGENTS.md still mentions mcp_connection_manager.rs; that file is not in the current tree. Disambiguation lives in tools.rs.Source:codex-rs/codex-mcp/src/tools.rs lines 1–5、lines 113–151;AGENTS.md line 35
Visible, searchable, and executable are three sets. Tight token budget: defer discovery. Keep core names; externals yield. None of this depends on Rust.
DeepSeek Harness: plugins toss schemas into the assembler
DSH has no central planner. Each toolkit tosses schemas via systemPrompt.tools() at assemble time; the assembler then seats them with the deploy-config toolOrder. Unnamed tools slot in at the reserved marker <unlisted-tools>, lexicographic by name. Miss that marker and assemble fails immediately.
Install @deepseek-ai/dsh-tool-fs and the model sees a first-class read. The description tells it to use that for text. Codex leaves disk reads to shell, MCP, and an internal API — the planner has no read_file entry.
Claude Code: a static base table plus MCP, builtins first
getAllBaseTools is a hardcoded array; FileReadTool is a first-class member. At request time assembleToolPool filters deny rules, then concatenates builtins and MCP each sorted by name. Builtins first; on a name clash the builtin wins. The comments say this order is for the prompt cache: slip MCP into the middle of builtins and every later cache key dies.
Claude Code’s base table can be counted by opening one file. To count Codex’s list you have to walk add_core_tool_sources. The same planner can trim by identity and budget.
What’s still on this round’s menu
Turn on MCP and search. Confirm mcp__filesystem__read_file is in the gray zone and tool_search is on the menu. Then walk three beats: search still on, but every MCP becomes Hidden; drop mid-sample; take the next sample. At each beat write down the visible table, the gray zone, and whether tool_search is there.
Advanced: switch identity to Guardian reviewer. Permission starts Managed, then something else. What is each menu, and why is it not “just hang fewer tools”?
StepContext freezes them at the sample boundary. The model can still read files with exec_command even when read_file is not on the table.