# How to add MCP servers to Codex (config.toml and codex mcp add)

Canonical: https://clawdocx.com/docs/codex-add-mcp-servers
Author: Theo Marsh
Difficulty: INTERMEDIATE
Published: 2026-02-24
Updated: 2026-10-20

> Add MCP servers to Codex with codex mcp add or an mcp_servers block in config.toml: stdio and HTTP transports, OAuth, bearer tokens and tool limits.

To add an MCP server to Codex, run `codex mcp add <name> -- <command>` for a local server or `codex mcp add <name> --url <url>` for a remote one, and Codex writes the entry into `~/.codex/config.toml` for you. Reach for the file directly when you need the fields the flags do not cover: tool allow and deny lists, startup and per-tool timeouts, and per-tool approval modes. Either way the server is defined as an `[mcp_servers.<server-name>]` table, and the ChatGPT desktop app, the Codex CLI and the IDE extension all read the same configuration, so you add a server once and get it in all three.

## Where does Codex keep MCP server configuration?

Two files, with different trust rules:

- **User config: `~/.codex/config.toml`.** This is the one `codex mcp add` writes to and the one most servers belong in.
- **Project config: `.codex/config.toml`.** The documentation is explicit that this one "works only in trusted projects". A project-level server is therefore not a way to ship an MCP server to anyone who clones a repository: in an untrusted project the block is inert.

That trust rule is the single most useful thing to know before you start, because it fails quietly. A project config in a project Codex does not trust does not error; the server simply is not there.

## What is the fastest way to add a server?

For a local server that Codex launches as a process, pass the launch command after `--`:

```bash
codex mcp add context7 -- npx -y @upstash/context7-mcp
```

The general form, including environment variables, is:

```bash
codex mcp add <server-name> --env VAR1=VALUE1 --env VAR2=VALUE2 -- <stdio server-command>
```

For a remote server, pass `--url` instead of a command. The two are mutually exclusive:

```bash
codex mcp add example --url https://mcp.example.com
```

The rest of the subcommands cover the lifecycle:

| Command | What it does |
|---|---|
| `codex mcp add <name>` | Registers a server, with `-- <command...>` for stdio or `--url <value>` for streamable HTTP |
| `codex mcp list` | Lists configured servers. `--json` gives machine-readable output |
| `codex mcp get <name>` | Shows one server's configuration. `--json` prints the raw config entry |
| `codex mcp remove <name>` | Deletes a stored server definition |
| `codex mcp login <name>` | Starts an OAuth login, for streamable HTTP servers that support OAuth only. Takes `--scopes scope1,scope2` |
| `codex mcp logout <name>` | Removes stored OAuth credentials for a streamable HTTP server |

`codex mcp --help` lists every MCP command, and inside the `codex` TUI, `/mcp` shows the servers active in the current session. That last one is the check to run after any change: it tells you what Codex actually loaded, rather than what you wrote.

## Which transport should you use, stdio or streamable HTTP?

Pick by where the server runs. A server you install and launch locally is stdio; a service you reach at an address is streamable HTTP.

| | stdio | Streamable HTTP |
|---|---|---|
| Required field | `command` | `url` |
| Where it runs | A local process Codex starts | A service reached over the network |
| Add it with | `codex mcp add <name> -- <command...>` | `codex mcp add <name> --url <value>` |
| Passing secrets | `env`, `env_vars` | `bearer_token_env_var`, `http_headers`, `env_http_headers`, `http_headers_helper` |
| Interactive sign-in | Not applicable | `codex mcp login <name>` |
| Other fields | `args`, `cwd`, `experimental_environment` | `auth`, which is `oauth` by default or `chatgpt` |

The stdio fields in full: `command` is required, `args` passes arguments, `env` sets environment variables, `env_vars` allows and forwards variables that already exist, `cwd` sets the working directory, and `experimental_environment` set to `remote` starts the server through a remote executor where one is available.

Note the difference between `env` and `env_vars`, because it is easy to reach for the wrong one. `env` sets a value you write in the config file. `env_vars` names variables already in your environment and forwards them, which is what you want for a secret you do not wish to commit.

## How do you write the TOML by hand?

A basic stdio server, showing both environment mechanisms:

```toml
[mcp_servers.context7]
command = "npx"
args = ["-y", "@upstash/context7-mcp"]
env_vars = ["LOCAL_TOKEN"]

[mcp_servers.context7.env]
MY_ENV_VAR = "MY_ENV_VALUE"
```

A streamable HTTP server with a bearer token and a static header:

```toml
[mcp_servers.figma]
url = "https://mcp.figma.com/mcp"
bearer_token_env_var = "FIGMA_OAUTH_TOKEN"
http_headers = { "X-Figma-Region" = "us-east-1" }
```

A server with tool filtering, timeouts and a per-tool approval mode:

```toml
[mcp_servers.chrome_devtools]
url = "http://localhost:3000/mcp"
enabled_tools = ["open", "screenshot"]
disabled_tools = ["screenshot"] # applied after enabled_tools
default_tools_approval_mode = "prompt"
startup_timeout_sec = 20
tool_timeout_sec = 45
enabled = true

[mcp_servers.chrome_devtools.tools.open]
approval_mode = "approve"
output_token_limit = 30000
```

Read that third block carefully, because it is the documentation's own illustration of an ordering rule rather than a configuration to copy. `screenshot` appears in `enabled_tools` and again in `disabled_tools`, and because the deny list is applied after the allow list, the result is that `screenshot` is off.

## How do you authenticate a remote server?

Four mechanisms, and they have a defined precedence.

- **Bearer token**: set `bearer_token_env_var` to the name of an environment variable holding the token. On the command line, `--bearer-token-env-var ENV_VAR` does the same.
- **Static headers**: `http_headers` for names and values written in the config, or `env_http_headers` to map header names to environment variable names.
- **A header helper**: `http_headers_helper` names a local command that prints a JSON object of headers, which is how you refresh a short-lived credential.
- **OAuth**: `codex mcp login <server-name>`. The `auth` field is `oauth` by default, with `chatgpt` as the other value.

Precedence matters when you combine them: explicit bearer tokens and OAuth credentials take precedence over an `Authorization` header supplied by a helper. If you expected the helper's header to win, it will not.

For registering an OAuth client at add time, `--oauth-client-id` sets the client identifier and requires `--url`; `--oauth-client-secret` reads a confidential client's secret from an environment variable; and `--oauth-resource` adds an OAuth resource parameter during login, also requiring `--url`:

```bash
codex mcp add example --url https://mcp.example.com --oauth-client-id my-client --oauth-client-secret "$MCP_CLIENT_SECRET"
```

`--oauth-client-registration` accepts `cimd` or `dcr` for a single login and defaults to `auto`.

The behaviour to plan around: **if no credential source resolves, Codex connects without authentication.** That is the documented fallback, and it means a typo in an environment variable name does not stop the server from loading. It produces an anonymous connection that looks healthy.

## How do you limit what a server can do?

The fields that decide how much of a server reaches the model, and how much it can do without asking:

| Field | Default | What it controls |
|---|---|---|
| `enabled_tools` | Unset | An allow list of tool names |
| `disabled_tools` | Unset | A deny list, applied after `enabled_tools` |
| `default_tools_approval_mode` | Unset | `auto`, `prompt`, `writes` or `approve` for the server's tools |
| `tools.<tool>.approval_mode` | Unset | The same choice for one named tool |
| `tools.<tool>.output_token_limit` | Unset | A cap on one tool's output |
| `startup_timeout_sec` | `10` | How long Codex waits for the server to initialize |
| `tool_timeout_sec` | `60` | How long a single tool call may take |
| `enabled` | Enabled | Set to `false` to switch a server off without deleting it |
| `required` | Not required | `true` makes Codex startup fail if the server cannot initialize |

`default_tools_approval_mode` is the field worth setting deliberately on anything that can write. An allow list controls what the model can see; the approval mode controls what it can do unattended. For the wider argument about giving an agent tools versus giving it instructions, see [MCP vs agent skills](/blog/mcp-vs-agent-skills).

## What breaks first?

In rough order of how often each one catches people:

1. **A project config in an untrusted project.** `.codex/config.toml` applies only in trusted projects, and an untrusted one gives you no server and no complaint. Check `/mcp` in the TUI, or `codex mcp list`, rather than assuming the file was read.
2. **A tool in both lists.** `disabled_tools` is applied after `enabled_tools`, so a name in both is disabled. If a tool you explicitly allowed is missing, look for it in the deny list.
3. **An unauthenticated connection you thought was authenticated.** With no resolvable credential, Codex connects anyway. A misspelled `bearer_token_env_var`, or an environment variable that is not exported in the shell Codex was started from, lands here.
4. **A `startup_timeout_sec` of 10 against a cold `npx`.** The default is 10 seconds. A stdio server whose launcher downloads a package on first run can exceed that, and the honest fix is raising the timeout for that server rather than retrying until it happens to be warm.
5. **`required = true` on a flaky server.** It does what it says: Codex startup fails when that server cannot initialize. Useful for a server your workflow genuinely depends on, and a self-inflicted outage on one that is merely nice to have.

## How do you install or update the Codex CLI?

If you do not have it yet, the documented installers are:

```bash
curl -fsSL https://chatgpt.com/codex/install.sh | sh
```

```bash
npm install -g @openai/codex
```

```bash
brew install --cask codex
```

On Windows, from a new PowerShell window:

```powershell
powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex"
```

Homebrew users update with `brew upgrade --cask codex`. The configuration in this guide was checked against the Codex CLI release that `openai/codex` marks as latest, 0.163.0, published 15 October 2026; the repository also carries `0.164.0-alpha` prereleases, which are not the stable line.

Next, the [Codex platform hub](/ai-agents/codex) covers the rest of the tool, [what MCP is](/blog/what-is-mcp) explains the protocol these servers speak, and [Codex vs Claude Code](/blog/codex-vs-claude-code) compares the two agents if you are choosing between them. If you are adding somebody else's MCP server rather than your own, treat it as third-party code with access to whatever you authenticate it against, and work through the [skill security audit](/docs/skill-security-audit) first. ClawDocx does not vet, endorse or certify any third-party MCP server, including the ones named in the documentation examples above.

## Frequently asked questions

**Where does Codex look for MCP server configuration?**

In two files. The user config is ~/.codex/config.toml, and a project config at .codex/config.toml works only in trusted projects. The ChatGPT desktop app, the Codex CLI and the IDE extension all share that configuration, so a server you add once is available in all three.

**Do you have to edit TOML by hand to add an MCP server to Codex?**

No. Run codex mcp add <name> -- <command> for a local stdio server, or codex mcp add <name> --url <url> for a streamable HTTP one, and Codex writes the entry for you. Hand-editing is for the fields the flags do not cover, such as tool filters, timeouts and per-tool approval modes.

**How do you sign in to an MCP server that uses OAuth?**

Run codex mcp login <server-name>, which starts an OAuth login for a streamable HTTP server that supports it. Add --scopes scope1,scope2 to request specific scopes. codex mcp logout <server-name> removes the stored credentials again.

**What happens if no credential is configured for a remote MCP server?**

Codex connects without authentication. It does not refuse to start the server or warn you that the connection is anonymous, so an unauthenticated connection to a server you believed was authenticated looks exactly like a working one until a tool call fails or returns the wrong data.

**How do you stop an MCP server from exposing a particular tool?**

Use enabled_tools as an allow list and disabled_tools as a deny list. disabled_tools is applied after enabled_tools, so a tool named in both ends up disabled. For a tool you want to keep but gate, set tools.<tool>.approval_mode instead.

## Sources

- [ChatGPT Learn: MCP](https://learn.chatgpt.com/docs/extend/mcp.md)
- [ChatGPT Learn: Developer commands (CLI)](https://learn.chatgpt.com/docs/developer-commands?surface=cli)
- [ChatGPT Learn: Codex CLI](https://learn.chatgpt.com/docs/codex/cli.md)
- [openai/codex releases](https://github.com/openai/codex/releases)