The Forge · 10 min mission
MCP in Codex: Client, Server & Tool Governance
Connect Codex to your tools the right way — stdio vs HTTP, OAuth, per-tool approval, and Codex-as-server.
On this page
Codex out of the box can read your files, run your shell, and edit your repo. The instant a task reaches past that boundary — a Context7 doc lookup, a row in production Postgres, a Sentry issue, your own internal API — the agent is blind, and you go back to copy-pasting between Codex and the other tool.
The Model Context Protocol (MCP) is the open standard that deletes that courier work. You register a server once; Codex then calls its tools directly, inside the same session, with no paste in the middle. Codex plays both roles in the protocol: it is an MCP client (it connects out to servers you add) and an MCP server (other apps connect into Codex with codex mcp-server). This guide covers both directions, plus the part most teams skip until it bites them — tool governance: deciding exactly which tools a server may expose and which ones run without asking.
Two transports: stdio vs HTTP
Every server you add speaks to Codex over one of two transports, and the transport is implied by how you add it. There is no --transport flag — you pick stdio by passing a launch command after --, or HTTP by passing --url.
stdio runs the server as a local child process on your machine, exchanging JSON-RPC over standard input/output. Use it for tools that need direct system access or ship as a local package — the canonical example is codex mcp add context7 -- npx -y @upstash/context7-mcp. The -- is load-bearing: everything after it is the command Codex spawns, untouched. Drop the -- and Codex tries to parse the server's own flags as its own and the add fails.
Streamable HTTP points Codex at a remote endpoint with --url. This is the path for cloud services and anything behind OAuth. It is the only transport that supports the codex mcp login OAuth flow covered later — stdio servers authenticate with environment variables instead.
The four management commands
Adding is one of five codex mcp subcommands. The other four are the daily workflow [V]:
codex mcp list— every configured server with its transport and command/URL. Add--jsonfor machine-readable output you can pipe into scripts.codex mcp get <name>— one server's full stored entry.--jsonprints the raw config.codex mcp remove <name>— deletes the stored definition.codex mcp login <name>/codex mcp logout <name>— the OAuth pair for HTTP servers (next section).
Whatever you do through the CLI is just a thin writer over config.toml. codex mcp add writes a [mcp_servers.<name>] table; codex mcp remove deletes it. You can hand-edit that file instead and Codex picks it up identically — which is the right move once a server needs the governance keys the CLI flags don't expose.
The [mcp_servers.id] schema, in full
This is the artifact that actually matters: the [mcp_servers.<id>] table in ~/.codex/config.toml. A stdio server uses command/args/env; an HTTP server uses url and its header/token keys. The two halves below show every documented key. [V]
[mcp_servers.db]
command = "npx" # launcher for the stdio server
args = ["-y", "@bytebase/dbhub"] # args passed to that command
env = { DSN = "postgresql://localhost/app?sslmode=disable" }
cwd = "/Users/you/work/app" # working dir for the child process
startup_timeout_sec = 10 # override default 10s init timeout
tool_timeout_sec = 60 # override default 60s per-tool timeout
enabled = true # flip to false to disable, keep config
required = false # true = fail Codex startup if it won't init
# Tool governance — the part most setups skip
enabled_tools = ["query", "list_tables"] # allow-list: only these are exposed
disabled_tools = ["execute"] # deny-list, applied AFTER enabled_tools
default_tools_approval_mode = "prompt" # auto | prompt | approve, for all tools
# Per-tool override — read-only query runs without asking
[mcp_servers.db.tools.query]
approval_mode = "auto"[mcp_servers.sentry]
url = "https://mcp.sentry.dev/mcp" # streamable HTTP endpoint
bearer_token_env_var = "SENTRY_TOKEN" # read a bearer token from this env var
http_headers = { "X-Org" = "acme" } # static headers on every request
env_http_headers = { "Authorization" = "SENTRY_AUTH" } # header value from env var
startup_timeout_sec = 10
tool_timeout_sec = 60| Key | Transport | What it does |
|---|---|---|
command | stdio | Launcher binary for the local server process |
args | stdio | Arguments passed to command |
env | stdio | Environment variables forwarded into the child process |
cwd | stdio | Working directory for the spawned process |
url | HTTP | Streamable-HTTP endpoint Codex connects to |
bearer_token_env_var | HTTP | Env var holding the bearer token |
http_headers | HTTP | Static headers sent on every request |
env_http_headers | HTTP | Header values sourced from env vars |
startup_timeout_sec | both | Override the default 10s init timeout |
tool_timeout_sec | both | Override the default 60s per-tool timeout |
enabled / required | both | Toggle off without deleting / fail startup if it can't init |
Tool governance: enabled_tools, disabled_tools, approval modes
A busy MCP server can hand Codex twenty tools when you wanted three, and some of those twenty write — they drop tables, push commits, send messages. Governance is how you bound that surface. Codex gives you two independent levers. [V]
Which tools exist at all is controlled by two lists. enabled_tools is an allow-list: set it, and only the named tools are exposed — everything else on the server is invisible to Codex. disabled_tools is a deny-list applied after enabled_tools. That ordering is the rule to memorize: deny wins. If a tool name appears in both, it stays hidden. A common, safe pattern is to allow-list the read tools and deny-list the one dangerous write you can't fully trust.
Which tools run without asking is controlled by approval modes, with three values: auto (run silently), prompt (ask you each time), and approve (require explicit approval). default_tools_approval_mode sets the baseline for every tool on that server. Then [mcp_servers.<id>.tools.<tool>] with its own approval_mode overrides the default for one specific tool. So the idiomatic stance — ask before anything, except let the obviously-safe read tool run free — is exactly default_tools_approval_mode = "prompt" plus a per-tool approval_mode = "auto" on the read tool, as shown in the schema above.
The two governance levers are different questions
enabled_tools / disabled_tools — does the tool EXIST?
A visibility filter. enabled_tools allow-lists; disabled_tools deny-lists and is applied after, so deny always wins. A tool not exposed here cannot be called at all, regardless of approval mode. Use it to amputate write tools you never want Codex to see.
approval_mode — may it run WITHOUT asking?
A friction setting on tools that do exist. default_tools_approval_mode is the per-server baseline (auto/prompt/approve); a per-tool [...tools.<tool>] approval_mode overrides one tool. Use it to let read tools run silently while writes still stop for confirmation.
OAuth for HTTP servers
Many remote servers won't take a static bearer token — they want a real OAuth flow. Codex handles this for streamable-HTTP servers only, with the login/logout pair. [V]
codex mcp login <name> opens a browser, runs the OAuth dance, and stores the resulting credentials so the server connects on every future session without re-auth. If the server gates capabilities behind scopes, request them with --scopes scope1,scope2. To revoke, codex mcp logout <name> deletes the stored credentials for that server.
Two knobs shape the flow. The callback is a local port the browser redirects back to — override it with the top-level mcp_oauth_callback_port in config.toml if the default collides with something. And where credentials live is controlled by mcp_oauth_credentials_store, which takes auto, keyring, or file. On a laptop, keyring stores tokens in the OS secret store (Keychain / libsecret); file writes them to disk, which is what you'll reach for on a headless box with no keyring daemon. auto lets Codex choose.
Build an [mcp_servers] block
Codex MCP config builder
Wire an MCP server into Codex. Pick the transport, add env vars, and set tool governance — the codex mcp add command and the matching [mcp_servers.<id>] block stay in sync, so either one drops straight into your setup.
Server identity
The name becomes the [mcp_servers.<id>] table key — it gets slugified to a safe id.
Config id: context7
Transport
stdio launches a local process and talks over stdin/stdout. HTTP connects to a remote streamable endpoint.
Launch command
The command Codex spawns, plus its arguments. Everything after -- is the server launcher.
Environment variables
Passed to the launched process. Forwarded as --env KEY=VALUE on the CLI and an env = { … } map in TOML.
Tool governance
How Codex treats this server's tools. default_tools_approval_mode sets the baseline; lists narrow what's exposed.
default_tools_approval_mode
Ask before each tool call.
codex mcp add context7 \ -- npx -y @upstash/context7-mcp[mcp_servers.context7]command = "npx"args = ["-y", "@upstash/context7-mcp"]default_tools_approval_mode = "prompt"Codex as a server: codex mcp-server
Flip the protocol around. codex mcp-server runs Codex itself as an MCP server over stdio, so another MCP client — a different agent, an IDE, an orchestration script — can drive Codex as a tool. It inherits your global config overrides and exits cleanly when the downstream client closes the connection. [V]
It exposes two tools to whoever connects:
codex— start a new Codex session. The client passes a prompt and configuration; Codex runs the turn and returns its output.codex-reply— continue an existing session. This is the multi-turn primitive.
Continuation hangs on one value: threadId. When you call the codex tool, the response carries structuredContent.threadId; you feed that same threadId into codex-reply to keep talking in the same thread, with full context preserved. (conversationId is a deprecated alias kept for backward compatibility — use threadId.) This is the exact mechanism the upcoming MCP-bridge tandem guide uses to let Claude Code call Codex as a sub-agent: Claude is the client, codex mcp-server is the server, and threadId threads the conversation. [V]
Driving Codex from another client
Launch Codex as a server
The client spawns
codex mcp-serveras a stdio MCP server — exactly the way Codex spawns your stdio servers, but in reverse.Call the `codex` tool
Send a prompt through the
codextool. Codex runs the turn and returns its result plusstructuredContent.threadId. Capture that id.Continue with `codex-reply`
For every follow-up, call
codex-replyand pass the capturedthreadId. Same thread, full context — no re-priming.
Enterprise lockdown: requirements.toml
On a single laptop, you decide what Codex connects to. In an org, an admin does — and the mechanism is requirements.toml, a file of constraints users cannot override. It lives at /etc/codex/requirements.toml on Linux/macOS and %ProgramData%\OpenAI\Codex\requirements.toml on Windows. [V]
For MCP, the admin adds an [mcp_servers] table that acts as a strict allow-list keyed by identity. For stdio servers the identity matches on command; for HTTP servers it matches on url. A server is permitted only when both its config name and its identity match an approved entry — and if the table is present but empty, all MCP servers are disabled. That empty-table-means-deny-all behavior is the safe default to know: a locked-down fleet ships every developer the same vetted set and nothing else.
# /etc/codex/requirements.toml (admin-managed; users cannot override)
[mcp_servers.docs]
identity = { command = "codex-mcp" }
[mcp_servers.remote]
identity = { url = "https://example.com/mcp" }The same file also governs approval policies, sandbox modes, permission profiles, and command rules — MCP is one slice of a broader managed-configuration surface. If your codex mcp add silently fails to enable a server in a managed environment, this allow-list is the first place to look.
Knowledge check
You add a Postgres MCP server. You want Codex to be able to run read queries silently, but you never want it to call the server's `drop_table` tool at all, and any other tool should stop and ask first. Which config is correct?
Reach the end and this star joins your charted sky.