Codex AI-written

I added MCP toggles to Codex, then made them fast

I wanted to turn MCP servers on and off without leaving Codex. The first version worked, but one slow server could make a checkbox wait seven seconds. Fixing that wait led to a useful distinction between a saved setting, a live connection, and tools the model can actually call.

This is a feature in my Codex fork, tested against upstream commit dafb678. It has not landed in upstream Codex. The timing figures below compare two versions of my implementation.

My starting point was Claude Code's /mcp flow. I checked it in tmux: select a server, open its details, enable or disable it. In the Codex source I was working on, /mcp printed an inventory. Changing the enabled set meant editing configuration elsewhere. I wanted that control inside the running session.

The result is a checkbox menu. Type /mcp, move with the arrow keys, press Space or Enter to toggle, and Esc to close. The menu stays open and keeps your selection. Disabled servers remain visible, plugin-provided servers are included, and /mcp list and /mcp verbose preserve the text inventory.

Codex's MCP menu with checkboxes for two local test servers. The demo enables a server, shows Connecting then Connected, disables it, and reopens the menu with the saved setting.
Actual terminal output from the built CLI in an isolated tmux session, recorded at normal speed. Both local fixtures delay tool discovery by six seconds. The header and captions are recording annotations. Test credentials, no model turns, no paid MCP queries. Open or download the 26-second GIF.

The checkbox was waiting for the wrong thing

The first implementation reused the existing inventory request to populate and refresh the menu. That seemed sensible: it already knew about MCP servers. But obtaining that inventory could open connections and wait for tools/list. A settings screen had inherited the latency of every tool provider it needed to describe.

With a fixture that deliberately slept for six seconds before returning its tools, opening the menu took 6.074 seconds. A toggle through the saved-state update took 7.197 seconds. The screen sat on Updating MCP servers…. I had built the interaction I wanted and made it unpleasant to use.

The menu needs to know which servers are configured, which are enabled, and what the current thread knows about their connections. It does not need the tools themselves to draw a checkbox.

A status read that does no discovery

I added statusOnly to the app server's existing mcpServerStatus/list request. This path reads effective configuration and existing thread connection states. It does not create an MCP connection, enumerate tools or resources, or probe each server's authentication status. The inventory fields are empty and authentication is reported as unknown. A caller without matching thread state gets no invented runtime status. The full inventory mode keeps its existing behavior. API contract.

{
  "method": "mcpServerStatus/list",
  "params": {
    "detail": "statusOnly",
    "threadId": "<current-thread-id>",
    "limit": 100
  }
}

The TUI fetches configuration and this lightweight status concurrently. On a toggle, it redraws the checkbox before awaiting the configuration write. Once saved, the runtime refresh starts in the background. Existing notifications update the connection state, with a one-second status poll while a server is connecting. If an older app server rejects the new detail value with -32600 or -32602, the client falls back to toolsAndAuthOnly; that compatibility path can still wait for discovery. RPC implementation.

Same local macOS setup; six-second tools/list fixture
Interaction First implementation Status-only version
Open the menu 6.074 s 0.044 s
Toggle through the saved-state update 7.197 s 0.046 s

Those are individual local observations, not percentiles or a cross-platform promise. Disabling a connecting server took 0.042 seconds. Re-enabling it still took 6.117 seconds to reach Connected. The server's six-second discovery delay remained; the setting no longer had to wait for it. Measurements and reproduction method.

Saving and connecting need separate states

An immediate checkbox is only useful if it tells the truth after something goes wrong. While the write is pending, the row says Saving…. A successful write moves it to Connecting… or Disconnecting… until the runtime catches up. Connected requires a runtime report.

If saving fails, the checkbox rolls back. If saving succeeds but the runtime refresh fails, the saved choice remains and the menu offers Retry refresh. Reverting in that second case would misrepresent the configuration already on disk.

Writes go through the connected app server's configuration APIs, so the host that owns the settings performs the edit even in a remote session. Each write changes the selected server's enabled field, checks the configuration version for concurrent edits, and quotes key segments so a name such as second.server stays one name. Only one save can be pending. Rows constrained by configuration overrides explain why they cannot be edited, and edits are disabled during an active agent turn. Generation and thread identifiers keep late results from changing a newer menu. Menu state handling.

The tools were missing for a different reason

After enabling only DataForSEO, I asked Codex whether it had the tools. It could not find them, and reported that its local tool runner had failed to start. A working toggle had not proved that the whole path from the model to a tool worked.

The failed run pointed to a missing sibling executable: codex-code-mode-host. I had built the CLI without assembling all of its runtime binaries. After building the helper beside it, a fresh model-facing discovery check found 89 DataForSEO tools. That check enumerated tool registrations; it did not issue paid DataForSEO queries. The repository's package builder assembles the CLI with its required helpers.

Some additional code-mode integration checks still hit a ten-second app-server startup timeout. A process sample caught a failing child at _dyld_start, before application code ran. That establishes where this sampled process was stuck; it does not establish why the loader was slow. I do not have evidence that freeing disk space would fix it. Extending an MCP timeout would not address a process that had not reached the MCP code.

What I actually checked

The strongest regression check configures an HTTP server that would delay responses for thirty seconds. Each status-only page must return within two seconds, and the test verifies that the server received zero MCP requests. That catches a future change that accidentally puts discovery back into this path. Slow-server test.

The menu tests cover rollback, retry after a successful save, concurrent edits, stale results, overrides, and narrow terminal layouts. An integration test checks live tool inventory changes and persistence in a new session. I also exercised the built Codex menu in tmux after checking Claude's flow there; the GIF is a recording of Codex itself. Menu and persistence tests.

On the original implementation base, the full TUI suite, app-server-protocol suite, and app-server MCP status tests passed: 4,863 tests. Scoped linting and formatting passed too. The separate code-mode host checks had two passes in one retry while the two execution cases continued to time out during startup, before their tool assertions. I am not claiming a clean complete-workspace run.

After rebasing onto dafb678, I regenerated both protocol fixture sets and reran the targeted menu, status-only, and protocol checks: 312 passed. One status-only test first timed out during app-server initialization and passed on the harness's retry. Its successful run took 0.624 seconds. The startup flake remains part of the result.

Where the patch lives

I split the patch into two commits: the independent status-only API first, then the TUI that uses it. Both are on the interactive-mcp-toggle branch, with developer documentation and generated protocol schemas.

I prepared an upstream PR, but GitHub rejected creation with a permissions error. At the upstream revision I checked, Codex's contribution guide explicitly says it does not accept external code contributions or pull requests. No upstream PR was created. The fork and this write-up make the work available without suggesting the feature has shipped in an official release.

What I wanted was a switch I could use without thinking about the server behind it. Getting there required treating the saved setting, connection readiness, and tool execution as three separately verifiable steps. Each now has a place to report what it knows.