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:
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.
[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.
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.
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.
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.
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.
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.