OpenAI Codex · Code Mode

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.

Course goalAfter this lesson you can explain three things. First, the tool list freezes on one sample — an MCP drop mid-request cannot change a table already sent. Second, planning trims sources by identity; registered does not mean visible. Third, turn search on and the expensive MCP schemas retreat to deferred discovery, leaving only tool_search.
Try it first · Flip the switches; watch this round’s menu change
Same session: change a condition and this round’s table is recomputed
Conditions
Left side is this round’s input. Hit Play to walk the source order, or flip the switches yourself.
Menu sent to the model0 visible0 chars
This round is frozen; a drop cannot change this table
Registered but hidden0 items
Logic trail · each animation step maps to a stretch of source
  1. Resolve this step’s MCP bindingmcp.rs L310
  2. Collect the MCP catalog and hand it to the plannerturn.rs L1494
  3. Decide by identity whether to take the full sourcesspec_plan.rs L889
  4. Register shell, resources, utilities, collabspec_plan.rs L930
  5. Append MCP and apply the exposure policyspec_plan.rs L148
  6. Hang tool_search only if something is deferredspec_plan.rs L335
  7. Send the model only specs with is_directspec_plan.rs L484
  8. Freeze into StepContext.tool_routerstep_context.rs L44
  9. build_prompt reads the visible tableturn.rs L1320
Hit Play. Flip a few switches in the same session and watch this round’s menu grow or shrink.
Where the menu comes fromEnvironment is up, ShellTool and UnifiedExec are on: the shell pair goes first. Connect MCP and you add resource entries plus sanitized external names.
Visible vs registeredTurn search on and the expensive MCP schemas retreat to the gray zone; the initial table becomes tool_search. A reviewer skips whole source groups. Off Managed, the table is empty.
Which round a drop changesDisconnect mid-sample and the menu already sent stays put. The next sample recomputes from an empty binding — only then do those names vanish.
Teaching demo: Switch combos and description lengths are teaching fixtures; planning order follows build_tool_router. Line numbers on the logic trail match openai/codex commit 4f39251a01.
Idea 1 · Frozen on one sample
What problem it solves

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.

What the idea is

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

MCP binding This step’s catalog build_tool_router Plan by identity StepContext Freeze the router Prompt.tools Send to the model A mid-run drop cannot change this snapshot The next sample re-resolves the binding; an empty binding hangs no MCP names
Teaching diagram: The freeze sits at the sample boundary. Advertise and execute read the same router.

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

Why it lasts

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.

Idea 2 · Trim sources by identity
What problem it solves

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.

What the idea is

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.

Why it lasts

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.

Idea 3 · Registered does not mean visible
What problem it solves

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.

What the idea is

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

Registered Can dispatch Direct · initial table Deferred · search, then see Hidden · left for execution Prompt.tools tool_search’s retrieval face The model cannot see it; a call can still hit
Teaching comparison: One registry, three faces: directly visible, deferred discovery, execute-only.

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

Registered still does not mean this round sent it to the model.
Why it lasts

Visible, searchable, and executable are three sets. Tight token budget: defer discovery. Keep core names; externals yield. None of this depends on Rust.

Side-by-side · Another answer to the same question

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.

Source checked on both sides · 2026-08-22 · DSH · Central tool-order list

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.

Source checked on both sides · 2026-08-22
Classroom Exercise
01

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”?

Takeaway:The planner decides what to advertise. The registry decides what can dispatch. Exposure decides direct, deferred, or hidden. 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.