Codex MCP servers: config.toml, the add command, and what the docs skip
How to add MCP servers to Codex CLI with codex mcp add and config.toml, every key worth knowing, where tokens should live instead of the file, per-tool approval modes, and the version gaps between the docs and the CLI you actually have installed.
Codex keeps MCP servers in config.toml, as [mcp_servers.<name>] tables. You can write those tables by hand or let codex mcp add write them for you. The CLI, the IDE extension, and the desktop app are documented as sharing that one configuration.
Most guides stop at the add command. This one covers the keys that decide whether a server is safe and reliable, and the places where the current docs describe flags that the Codex version on your machine may not have yet. We checked everything below against Codex CLI 0.147.0.
Adding a server
A local stdio server:
codex mcp add context7 -- npx -y @upstash/context7-mcp
A remote streamable HTTP server:
codex mcp add figma --url https://mcp.figma.com/mcp --bearer-token-env-var FIGMA_OAUTH_TOKEN
The usage line explains the shape: codex mcp add [OPTIONS] <NAME> (--url <URL> | -- <COMMAND>...). Every server is either a URL or a command. For a command, the -- is required, and everything after it belongs to the server. Put Codex's own options before the --, or they get passed to the server instead.
Two flags only work for one kind of server:
--env KEY=VALUEworks only with stdio servers.--bearer-token-env-varworks only with streamable HTTP servers.
The other subcommands are list, get, remove, login, and logout. Both list and get accept --json, which is the version to use in scripts.
The same thing in config.toml
[mcp_servers.context7]
command = "npx"
args = ["-y", "@upstash/context7-mcp"]
env_vars = ["LOCAL_TOKEN"]
[mcp_servers.figma]
url = "https://mcp.figma.com/mcp"
bearer_token_env_var = "FIGMA_OAUTH_TOKEN"
http_headers = { "X-Figma-Region" = "us-east-1" }
Editing the file directly is the only way to reach most of the useful keys below, so it helps to be comfortable with it.
Keep tokens out of the file
config.toml is plain text that gets copied between machines, pasted into chats, and sometimes committed by accident. Two pairs of keys decide whether a secret ends up in it:
envversusenv_vars, for stdio servers.envis a map of values written into the file.env_varsis a list of variable names that Codex passes through from your environment. Tokens belong inenv_vars.http_headersversusenv_http_headersandbearer_token_env_var, for HTTP servers.http_headersholds fixed values in the file. The other two read their values from environment variables at runtime.
The rule is simple: a value that would hurt if the file leaked should be a variable name in the file, not the value itself.
For OAuth servers, codex mcp login <name> runs the flow. Where the credentials are stored is set by the top-level mcp_oauth_credentials_store key, which takes auto, file, or keyring. On a shared machine, choosing keyring explicitly is the safer option.
Approvals per tool
This part has no direct equivalent in most MCP setups, and it is the most useful thing in the schema.
[mcp_servers.github]
url = "https://example.com/mcp"
default_tools_approval_mode = "prompt"
[mcp_servers.github.tools.search_code]
approval_mode = "auto"
default_tools_approval_mode sets the policy for every tool on a server, and tools.<tool>.approval_mode overrides it for a single tool. Both take auto, prompt, writes, or approve. The pattern that works is to set a cautious default for the server and turn on auto only for the read-only tools you trust. That beats deciding a whole server's trust level in one yes-or-no.
You can also narrow what a server exposes in the first place:
enabled_toolsis an allow list.disabled_toolsis a deny list that is applied after the allow list, so a tool named in both ends up disabled.
tools.<tool>.output_token_limit caps how much of your context a single tool result can use. Servers that return entire documents or long logs are the reason this key exists.
Timeouts and failure modes
The defaults are short, and they are the first thing to check when a server looks broken:
startup_timeout_sec: 10 seconds by default. A stdio server launched throughnpxdownloads its package on first run, which can easily take longer than that, and the server then just seems to be missing. Raise the timeout, or install the package ahead of time.tool_timeout_sec: 60 seconds per tool call by default.required = truemakes Codex fail at startup if an enabled server cannot initialize. The default is to quietly continue without it. In CI, failing loudly is almost always better.enabled = falseturns a server off without deleting its configuration. Codex 0.147.0 has noenableordisablesubcommand, so to toggle a server you edit this key, or set it for a single run with-c:
codex -c 'mcp_servers.figma.enabled=false'
Project scope, and the flag that is not there
Codex documents project-scoped MCP servers in .codex/config.toml, for trusted projects only. An untrusted project's .codex/ layer is skipped entirely, and that includes its MCP servers.
What codex mcp add does not have in 0.147.0 is a way to target that file. The command has no --scope flag, so it always writes to your user config. Adding a server to a project means editing .codex/config.toml yourself. Guides that describe a --scope project option are describing something your CLI may not support. Check with codex mcp add --help before you rely on it.
The same applies to OAuth. The current docs describe codex mcp login <name> --oauth-client-registration cimd|dcr for choosing how the client registers. That flag is not in 0.147.0 either, where login accepts --scopes. The docs follow the newest release, and your install may be behind it, so the --help output is the reference that matches the binary you are running.
One open report is worth knowing about if you use the desktop app: issue #13025, filed against 0.104.0, says Codex Desktop loaded MCP servers from ~/.codex/config.toml but ignored a trusted project's .codex/config.toml, even though the docs say the surfaces share configuration. If a project server shows up in the CLI but not in the desktop app, that is the likely reason. Moving the server into your user config is the workaround.
Coming from Claude Code
If you set up MCP in Claude Code first, here is what differs:
- Where config lives. Claude Code has a
--scopeflag and a project.mcp.json. Codex uses[mcp_servers]tables inconfig.toml, and for project scope you edit a trusted.codex/config.tomlby hand. - Trust. Both gate project-level configuration behind trust, for the reason covered in the Codex hooks post: a repository you cloned should not get to launch processes on your machine.
- Granularity. Codex's per-tool
approval_modeandoutput_token_limitare finer-grained than a single allow-or-deny decision for a whole server. - Secrets. The pattern is the same in both: store the name of an environment variable, not its value.
A setup worth copying
mcp_oauth_credentials_store = "keyring"
[mcp_servers.docs]
command = "npx"
args = ["-y", "@upstash/context7-mcp"]
env_vars = ["CONTEXT7_API_KEY"]
startup_timeout_sec = 30
default_tools_approval_mode = "auto"
[mcp_servers.tracker]
url = "https://mcp.example.com/mcp"
bearer_token_env_var = "TRACKER_TOKEN"
default_tools_approval_mode = "prompt"
disabled_tools = ["delete_issue"]
Read-only documentation servers get auto and a generous startup timeout. Anything that can write gets prompt, and its destructive tools are removed entirely. Tokens stay as variable names, and OAuth credentials go to the keychain.
After any change, run codex mcp list --json to confirm Codex sees the configuration you intended. With startup_timeout_sec and required left at their defaults, a server that fails to start does not raise an error. It just is not there.