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.

mcpintegrationscodexFact-checked 2026-06-13
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.

codex mcp add — stdio and HTTP
… scroll to run this session
A local stdio server (note the -- separator and --env) and a remote HTTP server (note --url). Then list and inspect what got stored.

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 --json for machine-readable output you can pipe into scripts.
  • codex mcp get <name> — one server's full stored entry. --json prints 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]

~/.codex/config.toml — a stdio server with full governance
toml
[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"
~/.codex/config.toml — a remote HTTP server
toml
[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
KeyTransportWhat it does
commandstdioLauncher binary for the local server process
argsstdioArguments passed to command
envstdioEnvironment variables forwarded into the child process
cwdstdioWorking directory for the spawned process
urlHTTPStreamable-HTTP endpoint Codex connects to
bearer_token_env_varHTTPEnv var holding the bearer token
http_headersHTTPStatic headers sent on every request
env_http_headersHTTPHeader values sourced from env vars
startup_timeout_secbothOverride the default 10s init timeout
tool_timeout_secbothOverride the default 60s per-tool timeout
enabled / requiredbothToggle off without deleting / fail startup if it can't init
The keys that decide where a server runs and how it authenticates. stdio uses the command group; HTTP uses the url group.

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.

OAuth login for a remote server
… scroll to run this session
Add over HTTP, then complete OAuth with codex mcp login. Scopes are optional; logout revokes the stored token.

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.

CLIcodex mcp add
codex mcp add context7 \    -- npx -y @upstash/context7-mcp
Config~/.codex/config.toml
[mcp_servers.context7]command = "npx"args = ["-y", "@upstash/context7-mcp"]default_tools_approval_mode = "prompt"
Pick a transport, toggle governance, and watch the exact config.toml table assemble — including the per-tool approval override.

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

  1. Launch Codex as a server

    The client spawns codex mcp-server as a stdio MCP server — exactly the way Codex spawns your stdio servers, but in reverse.

  2. Call the `codex` tool

    Send a prompt through the codex tool. Codex runs the turn and returns its result plus structuredContent.threadId. Capture that id.

  3. Continue with `codex-reply`

    For every follow-up, call codex-reply and pass the captured threadId. 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.

toml
# /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.