You add an MCP server to ~/.codex/config.toml. codex mcp list shows it. The model answers as if the tool doesn't exist. Ask again a minute later and it works, and nothing in between printed an error.

This isn't a broken server or a typo in your config. Since Codex CLI 0.147, Codex gives all optional (non-required) MCP servers one shared 1-second window to start when it builds a turn's tool list. A server that is still starting after that is left out of the turn, and Codex shows no warning: the only record is a trace-level log line. So a slow-starting server misses your first prompt, or your first few, and the whole of a one-shot codex exec run. Its tools show up in later turns once it is ready.

The fix is one line, on Codex 0.151 or later:

mcp_optional_startup_grace_ms = 0

It goes at the top level of ~/.codex/config.toml, above any [table] header, and it makes Codex wait for each server up to that server's startup_timeout_sec. The rest of this post is why that works, what it costs, and how to check it on your own machine. Raising startup_timeout_sec alone won't help: the shared 1 s deadline cuts the wait off first. Unless we say otherwise, we tested on Codex 0.161.0 (the latest release as of October 7, 2026) and read the source at that tag.

The symptom: your MCP server is configured, but the model can't see it

How to confirm it's the startup grace

The omission is logged only at TRACE level. Turn that on for Codex's MCP crate and look for one line:

# macOS / Linux RUST_LOG=codex_mcp=trace codex exec --skip-git-repo-check "hi" 2>&1 | grep "omitting" # ... omitting pending optional MCP server server_name=slow # Windows (PowerShell) $env:RUST_LOG="codex_mcp=trace"; codex exec --skip-git-repo-check "hi" 2>&1 | Select-String "omitting" Remove-Item Env:RUST_LOG

A server dropped because it outlasted its own startup_timeout_sec shows up as omitting MCP server without an exact ready client instead, which is why the filter matches only omitting.

At the default log level there is no warning or error line for this. The TUI does report outright failures, with messages like MCP startup incomplete (...) and MCP client for `<name>` failed to start, but those are for servers that failed, not for servers that were simply still starting.

The quickest check needs no logging at all: ask the same thing twice in one session. If turn 1 lacks the tools and turn 2 has them, this is your cause.

What Codex is doing: one shared 1-second grace for optional servers

Before each model request, Codex rebuilds the tool catalog it sends to the model. For an optional server that hasn't finished starting, in the common case it waits until a single deadline, then leaves out any server that is still pending. The default is DEFAULT_OPTIONAL_MCP_STARTUP_GRACE: Duration = Duration::from_secs(1) in codex-mcp/src/mcp/mod.rs.

Two details matter. The deadline is one value set the first time it's needed (optional_startup_deadline.get_or_init(...) in tool_catalog.rs), so it is shared across all your optional servers, not one second each, and later turns don't restart it. And a server that has finished starting skips this branch entirely, which is why its tools appear on later turns.

This was deliberate: before 0.147, a slow server held up your first prompt. PR #35742, "Avoid blocking turns on optional MCP startup," summarizes the core change as: "Give optional MCP servers a shared one-second startup grace, then omit servers that are still pending from the captured tool catalog."

Codex versionBehavior
0.146 and earlierWaits for servers to start
0.147.0 (August 7, 2026)Shared 1 s grace, hardcoded (PR #35742)
0.151.0 (August 29, 2026)Grace configurable with mcp_optional_startup_grace_ms (PR #41199)
0.161.0 (October 7, 2026)Unchanged: default still 1000 ms

The 0.151 release notes mention "a configurable grace period" without naming the key. OpenAI's MCP documentation does cover the key, in a short paragraph. Nothing in either place connects it to the symptom.

The fix: mcp_optional_startup_grace_ms = 0

Put the key at the top level of ~/.codex/config.toml, above the first [table] header. A key written below a header belongs to that table, and Codex won't read it as the setting. For a server that is slow to launch, raise its own timeout in the same edit:

# ~/.codex/config.toml mcp_optional_startup_grace_ms = 0 [mcp_servers.my-server] command = "npx" args = ["-y", "your-mcp-package"] startup_timeout_sec = 60

Codex's own doc comment for the key, in config_toml.rs: "Milliseconds to wait for optional MCP servers while building the initial tool catalog. Defaults to 1000. Set to 0 to disable the shared grace and wait for each server's configured startup_timeout_sec instead."

If you'd rather keep a ceiling on that wait, a large non-zero value such as 15000 should hold the first turn for up to about 15 seconds and then go ahead without anything still starting (from the source; we didn't run that value).

The trade-off: a slower first prompt, and the timeout still applies

With the grace at 0, the first turn waits for the slowest optional server, up to that server's startup_timeout_sec. If a server hangs (a broken package, a stalled npx download), your first prompt waits its full startup_timeout_sec, 60 s in the example above, on every launch, and then goes ahead without it. Pick a timeout you can live with.

0 does not mean "wait forever." A server that is slower than its own startup_timeout_sec is still left out: codex exec exits 0 without a word about it, while the TUI reports it as a startup timeout. We ran it: grace 0, and startup_timeout_sec = 1 on a server that takes 3 seconds to answer initialize. codex exec exited 0, only the fast server's tool reached the model, and nothing was printed about the slow one.

The reverse also holds. Common troubleshooting advice for slow npx servers is to raise startup_timeout_sec. Since 0.147, that alone doesn't fix the first turn: with the grace above 0, the shared deadline cuts the wait off first, however long the server's own timeout is. Our Default run shows it: the 3 s server was dropped even though its own timeout was longer.

Set startup_timeout_sec explicitly on any slow server rather than relying on the default. Codex also accepts startup_timeout_ms; if both are set, the seconds value wins.

Why not required = true?

Marking the server required = true in its [mcp_servers.<name>] table also works. Required servers bypass the grace, and in our run with the slow server marked required, both tools were present on turn 1 and Codex exited 0.

The catch is that it fails closed. Codex's doc comment for the field (mcp_types.rs) says: "When true, codex exec exits with an error if this MCP server fails to initialize." With a 1-second timeout on the slow server, the session never started:

Fatal error: Failed to initialize session: required MCP servers failed to initialize: slow: timed out handshaking with MCP server after 999.9994ms

(The wording after slow: varies between runs.)

So it's a reasonable choice for a server you can't work without, and the wrong blanket fix: one flaky server then stops Codex from starting. The grace key covers every optional server at once and degrades to "tool missing" rather than "Codex won't start." On 0.147 to 0.150, required = true was the only config setting that made Codex wait.

There is also a per-prompt option: a server that a prompt names explicitly with an mcp://<server> mention is waited for on that turn. PR #35742 says so outright ("Continue waiting when the turn explicitly requires a server through a plugin, skill dependency, or mcp:// mention"), and the code is in turn.rs.

Reproduce it yourself in five minutes

You need Node and Codex 0.151 or later. The test server below answers MCP over stdio and waits DELAY_MS before replying to initialize, so you can make it as slow as you like. Save it as srv.js:

// Minimal stdio MCP server that waits DELAY_MS before answering initialize. const delay = Number(process.env.DELAY_MS || 0); const name = process.env.SRV_NAME || "srv"; const send = (o) => process.stdout.write(JSON.stringify(o) + "\n"); let buf = ""; process.stdin.on("data", (d) => { buf += d; let i; while ((i = buf.indexOf("\n")) >= 0) { const line = buf.slice(0, i).trim(); buf = buf.slice(i + 1); if (!line) continue; let m; try { m = JSON.parse(line); } catch { continue; } if (m.method === "initialize") { setTimeout(() => send({ jsonrpc: "2.0", id: m.id, result: { protocolVersion: m.params.protocolVersion || "2025-06-18", capabilities: { tools: {} }, serverInfo: { name, version: "1.0.0" } } }), delay); } else if (m.method === "tools/list") { send({ jsonrpc: "2.0", id: m.id, result: { tools: [{ name: "ping_" + name, description: "ping", inputSchema: { type: "object", properties: {} } }] } }); } else if (m.id !== undefined) { send({ jsonrpc: "2.0", id: m.id, error: { code: -32601, message: "not found" } }); } } });

Use a throwaway CODEX_HOME so your real ~/.codex is untouched. The directory has to exist:

# macOS / Linux mkdir -p /tmp/codex-test && export CODEX_HOME=/tmp/codex-test # Windows (PowerShell) $env:CODEX_HOME="$env:TEMP\codex-test"; mkdir $env:CODEX_HOME

With a real model you'll need to sign in once inside it; if your login is stored in ~/.codex/auth.json, copying that file into the new directory saves the sign-in. Delete that copy when you finish testing. Its config.toml registers the same script twice, once fast and once slow. On Windows, write the path with forward slashes (C:/Users/me/srv.js) or as a single-quoted TOML string ('C:\Users\me\srv.js'): in a double-quoted string, \U is an invalid escape and Codex won't load the file.

[mcp_servers.fast] command = "node" args = ["/full/path/to/srv.js"] env = { DELAY_MS = "0", SRV_NAME = "fast" } [mcp_servers.slow] command = "node" args = ["/full/path/to/srv.js"] env = { DELAY_MS = "3000", SRV_NAME = "slow" }

Then run codex exec with the trace filter from earlier, and change one thing at a time. We pointed Codex at a small local mock of the Responses API that logged the tool names in each request, so no model calls were made. With a real model, ask it which ping_ tools it has. Our results on Codex 0.161.0, on our Windows ARM64 machine:

ConfigTools on turn 1Result
Defaultslow left out (in some runs fast too)Exit 0, no warning
Default, two-turn sessionNeither on turn 1; both on turn 2Both reported ready between the turns
mcp_optional_startup_grace_ms = 0BothExit 0, about 3 s extra wait
Grace 0, startup_timeout_sec = 1 on slowfast onlyExit 0, no warning
required = true on slowBothExit 0
required = true, startup_timeout_sec = 1 on slowNoneExit 1, fatal error

The two-turn row came from driving codex app-server directly, with an 8-second gap between turns. Even the zero-delay server reported ready about 8 seconds after the first turn started, so turn 1 went out with no MCP tools at all. That slow spawn is our machine's; on a faster machine the zero-delay server usually makes the window.

If you use Yaw MCP with Codex

We hit this ourselves: Yaw MCP launches through npx, which took anywhere from 3 to over 35 seconds to start in our tests on Codex 0.156.1. Its yaw-mcp install codex-cli now writes mcp_optional_startup_grace_ms = 0 above the first table when the file doesn't set the key, never changes a value you set yourself, and gives its own [mcp_servers.mcp] entry startup_timeout_sec = 60. Uninstalling leaves the key in place. For where each client keeps its config, see our MCP server configuration guide; if you're new to MCP, start with Getting Started with MCP.

Frequently Asked Questions

Why does my MCP server show in codex mcp list but the model says it has no such tool?

Since Codex 0.147, optional MCP servers get one shared 1-second window to start before Codex builds a turn's tool list. A server that is still starting is left out of that turn, with only a trace-level log line. Its tools appear on later turns once it is ready.

How do I make Codex wait for my MCP servers?

On Codex 0.151 or later, add mcp_optional_startup_grace_ms = 0 at the top level of ~/.codex/config.toml, above any [table] header. Codex then waits for each optional server up to its startup_timeout_sec. Raise that value on servers that are slow to start.

Does mcp_optional_startup_grace_ms = 0 make Codex wait forever?

No. It turns off the shared grace, so each optional server is awaited up to its own startup_timeout_sec. A server that is slower than that is still left out; codex exec exits 0 without a word about it, while the TUI reports it as a startup timeout. Your first prompt can be delayed by up to that timeout.

Should I set required = true instead?

Only for a server you can't work without. It makes Codex wait for the server, but if the server fails or times out, Codex refuses to start the session. On Codex 0.147 to 0.150 it is the only config setting that makes Codex wait, because the 1-second grace is fixed there.

Published by Yaw Labs.

Related Articles