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:

  • 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.

~/.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.

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.

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.

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

  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.

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.