Connecting Codex to MCP Servers
Codex MCP integration means configuring OpenAI's Codex CLI to talk to Model Context Protocol servers, so the agent can call tools beyond its built-in shell and file access, things like a database client, a browser automation server, or your team's internal API. You do this by editing config.toml and adding an mcp_servers entry for each server you want Codex to launch. Once configured, Codex starts the server as a subprocess, discovers its tools automatically, and can call them mid-task the same way it calls its native tools.
This guide walks through the actual config syntax, the two transport types you'll run into, how to verify a server loaded correctly, and the failure modes that trip people up the first time they do this.
What MCP gives Codex that it doesn't already have
Codex CLI ships with a fixed toolset: it can read and write files, run shell commands, and search a codebase. That covers most coding tasks, but it stops at the edge of your local filesystem and whatever binaries happen to be on your PATH.
MCP (Model Context Protocol) is an open standard for exposing tools, resources, and prompts to an LLM agent over a small JSON-RPC interface. A server implementing MCP might wrap a Postgres database, a Jira instance, Playwright, Sentry, or an internal deployment pipeline. Any client that speaks MCP, Codex, Claude Code, Cursor, whatever, can connect to that same server and get the same tools without the server author writing client-specific integration code.
For Codex specifically, this matters most in two situations:
- Your task needs an external system that isn't a plain shell command. Querying a database is possible today via
psqlin a shell call, but a proper MCP server can enforce read-only access, expose a query tool with a defined schema, and return structured results instead of raw terminal text. - You want the same tool surface across agents. If your team runs both Codex and Claude Code, standing up one MCP server per tool means you configure it once per agent instead of writing bespoke wrappers for each.
It's worth being clear about what MCP is not: it's not a way to give Codex more autonomy or change its permission model. A shell-exec tool from an MCP server is still subject to whatever approval mode Codex is running in. MCP changes what Codex can reach, not how much you trust it to act unsupervised.
Where the config lives
Codex CLI reads its configuration from ~/.codex/config.toml. If the file or directory doesn't exist yet, create it:
mkdir -p ~/.codex
touch ~/.codex/config.tomlMCP servers are declared as TOML tables under the mcp_servers key, one table per server, keyed by a name you choose:
[mcp_servers.filesystem]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/Users/you/projects"]
[mcp_servers.postgres]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"]The name after mcp_servers. (here filesystem and postgres) is arbitrary and only used for display in Codex's own tool listing and logs. Pick something short and descriptive; it shows up prefixed to every tool that server exposes, so filesystem becomes tool names like filesystem__read_file inside Codex's tool-call trace.
Adding a stdio server
Most MCP servers you'll encounter today run over stdio: Codex spawns the process, and the two sides exchange JSON-RPC messages over stdin/stdout. This is the simplest transport and the one you should default to unless you have a specific reason to use HTTP.
A minimal stdio entry needs a command and, usually, args:
[mcp_servers.github]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]Most real servers need credentials. Pass them through the env table rather than baking them into args, so they don't end up in process listings or shell history:
[mcp_servers.github]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
[mcp_servers.github.env]
GITHUB_PERSONAL_ACCESS_TOKEN = "ghp_your_token_here"If you'd rather not store secrets in plaintext inside config.toml, reference an environment variable that's already set in your shell profile instead of hardcoding the value, and load it via a wrapper script as the command:
[mcp_servers.github]
command = "/Users/you/.codex/bin/github-mcp-wrapper.sh"#!/bin/sh
export GITHUB_PERSONAL_ACCESS_TOKEN="$(security find-generic-password -s github-mcp -w)"
exec npx -y @modelcontextprotocol/server-githubThis keeps the token out of a file that might get committed or synced somewhere it shouldn't be.
For a locally built server, point command at the binary or interpreter directly:
[mcp_servers.internal-tools]
command = "node"
args = ["/Users/you/tools/internal-mcp-server/dist/index.js"]
[mcp_servers.internal-tools.env]
API_BASE_URL = "https://internal.example.com/api"If the server is a Python process, the same pattern applies with uv or a virtualenv's Python as the command:
[mcp_servers.data-pipeline]
command = "uv"
args = ["run", "--directory", "/Users/you/tools/data-pipeline-mcp", "python", "server.py"]Adding an HTTP or SSE server
Some MCP servers run as a long-lived HTTP service rather than a subprocess Codex spawns and owns, useful when the server is shared across a team or already deployed somewhere. For those, use a url instead of command:
[mcp_servers.shared-tools]
url = "https://mcp.internal.example.com/sse"
[mcp_servers.shared-tools.headers]
Authorization = "Bearer your_token_here"Check the specific field names your Codex CLI version expects before relying on this: MCP's HTTP transport went through a spec revision (from a plain SSE transport to "streamable HTTP"), and CLI support for each variant lags the spec by a version or two. If a remote server won't connect, the first thing to check is which transport it actually speaks, not just that the URL is reachable.
For anything you control yourself, stdio is simpler to debug and doesn't require standing up auth or TLS, so prefer it unless you specifically need a shared, always-on server.
Verifying a server loaded
After editing config.toml, start a new Codex session, don't rely on a running session picking up the change. Codex reads MCP server configuration at startup and spawns each configured server as part of session initialization.
To confirm a server is live, ask Codex directly in the session:
list the MCP tools you currently have availableCodex will enumerate every tool from every configured server, prefixed with the server name you chose. If a server you expect is missing from that list, it either failed to start or failed the MCP handshake, and Codex silently drops servers that don't initialize cleanly rather than crashing the whole session.
For a lower-level check, run the server's command and args by hand in a terminal:
npx -y @modelcontextprotocol/server-postgres postgresql://localhost/mydbIf that hangs, errors, or prints something that isn't valid JSON-RPC on startup, that's the actual problem, and Codex's own error surface for a broken MCP server is minimal, so reproducing the failure outside Codex is usually faster than debugging through it.
Setting a startup timeout
Some servers take a few seconds to initialize, connecting to a database, warming a cache, authenticating against an API. If Codex gives up before that finishes, the server looks broken when it isn't. You can raise the startup timeout per server:
[mcp_servers.postgres]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"]
startup_timeout_ms = 10000If you're not sure whether a server is slow to start or genuinely broken, bump this value first before assuming it's misconfigured. A server that needs to resolve DNS for an internal host or do a TLS handshake against a slow endpoint can easily blow past a default timeout on a cold start.
Scoping tools you don't want exposed
A server frequently exposes more tools than you want an autonomous coding agent touching. A GitHub MCP server, for instance, might expose both list_issues and merge_pull_request. You generally want the former available to Codex and not the latter, at least not without a human in the loop.
Check whether the specific server you're using supports scoping at the server level first, many accept a flag or env var to disable write operations, since that's more reliable than trying to filter after the fact. If the server doesn't support scoping natively, run it under a service account or token with the minimum permissions it actually needs, the same way you'd scope a CI credential. Don't rely on prompting Codex not to call a tool as your only safety boundary; if the tool exists and is reachable, treat it as something that will eventually get called.
Removing or disabling a server
To temporarily disable a server without deleting its config, comment out its table:
# [mcp_servers.postgres]
# command = "npx"
# args = ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"]To remove it permanently, delete the table entirely and start a fresh session. There's no separate registry to clean up, config.toml is the single source of truth for which servers Codex knows about.
Running Codex itself as an MCP server
The relationship between Codex and MCP isn't one-directional. Codex CLI can also run in a mode where it exposes itself as an MCP server, letting other MCP-aware clients (an IDE, another agent, a custom orchestration script) delegate coding tasks to Codex as a tool call rather than a full interactive session. This is useful if you're building a multi-agent setup where one orchestrating agent farms out implementation work to Codex and expects a structured result back rather than a terminal transcript.
If you're building that kind of setup, check Codex's current CLI help output for the exact subcommand and flags, this surface has moved around across releases more than the client-side mcp_servers config has, and the invocation details are the part most likely to differ from what you read in an older post.
Debugging a server that Codex can see but won't call
Sometimes a server shows up in the tool list but Codex never actually calls it, or calls it and gets an error back. A few things to check, in order:
- Tool descriptions are vague. Codex, like any LLM-driven agent, decides whether to call a tool based on its name and description. If a server's tool is named
run_querywith no description of what kind of query or what database, Codex may not connect it to a task that obviously needs a database. This is a server-side fix: better tool descriptions, not a Codex config change. - The server returns unstructured errors. If a tool call fails and the server just returns a stack trace as plain text, Codex has to guess what went wrong from that text. Servers that return structured MCP error responses give Codex a much better shot at retrying correctly or explaining the failure to you.
- Environment variables aren't reaching the subprocess.
enventries inconfig.tomlare passed only to that server's process, they don't inherit your full shell environment by default in every Codex version. If a server needsPATHto find a binary it shells out to internally, and it's not finding it, check whether you need to passPATHexplicitly in theenvtable. - The server was already running from a previous session. If you're testing a locally built server, kill any stray process (
ps aux | grep your-server-name) before starting a new Codex session, so you're not accidentally testing against a stale build.
A worked example: wiring up a Playwright MCP server
To make this concrete, here's a full setup for giving Codex browser automation via a Playwright-based MCP server, useful if you want Codex to verify a UI change by actually loading the page.
[mcp_servers.playwright]
command = "npx"
args = ["-y", "@playwright/mcp"]
startup_timeout_ms = 15000Start a Codex session and confirm the tools loaded:
list the MCP tools you currently have availableYou should see tools like playwright__navigate, playwright__click, playwright__screenshot in the list, prefixed with the server name from the config. From there, a prompt like:
Start the dev server, navigate to localhost:3000/checkout,
and take a screenshot to confirm the new discount banner renders.lets Codex chain its native shell tool (to start the dev server) with the MCP-provided browser tools (to load the page and capture the screenshot) in a single task, without you writing any glue code between the two.
FAQ
Does Codex support MCP out of the box, or do I need a plugin? MCP client support is built into Codex CLI itself; there's no separate plugin to install. You only need to edit config.toml and, for individual servers, install or build the server you want to connect (most published MCP servers are npm packages runnable via npx).
Can I use the same MCP server with both Codex and Claude Code? Yes. MCP is a protocol, not a Codex-specific or Claude-specific feature, so the same server process works with any compliant client. The configuration syntax differs (Codex uses TOML in config.toml, Claude Code uses JSON in .mcp.json or its own config), but the server itself doesn't need to know or care which client connected.
Why does Codex say a tool isn't available even though I configured the server? Start a brand-new session after editing config.toml; Codex reads MCP configuration at startup and won't pick up a mid-session edit. If it's still missing after a restart, run the server's command/args by hand in a terminal to check it starts and speaks valid JSON-RPC on its own, that isolates whether the problem is the server or the Codex config.
Should I give Codex write-access MCP tools, like one that can merge a pull request or delete a database table? Treat that decision the same way you'd treat giving a junior engineer prod credentials: scope the underlying token to the minimum it needs, and prefer read-only or dry-run modes for anything destructive unless you're actively supervising the session. MCP doesn't add its own permission layer on top of what the tool itself allows.
Do MCP servers slow Codex down? Each configured server adds a subprocess Codex spawns at session start, so startup time grows a little with each server you add, and a slow-initializing server can stall the whole session unless you raise startup_timeout_ms for it. During a task, MCP tool calls add whatever latency the underlying operation has (a database query, an API call), same as if you'd run the equivalent command by hand.
Can I pass different MCP servers for different projects? config.toml at ~/.codex/config.toml is a single global file, so by default the same servers are available in every Codex session regardless of directory. If you need per-project server sets, the practical workaround is maintaining separate config files and pointing CODEX_HOME (or the equivalent config-path override, check your installed version's flags) at the right one before starting a session in that project.
AI CodingShip full-stack AI apps at conversation speed — specs, agents, deploys, all from the terminal.
CodexLearn to drive OpenAI's coding agent: real tasks, safe sandboxing, and terminal-to-cloud workflows that ship.