MCP servers eagerly start per session, causing headed browser MCP processes to accumulate

Open 💬 10 comments Opened May 10, 2026 by BaseInfinity
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

Codex CLI appears to eagerly start configured MCP servers for every session/resume, even when the tools are not used. This creates a noisy lifecycle problem for headed browser MCP servers: each long-lived Codex session keeps its own visible browser/tool server processes alive.

This is most visible on macOS when a Python-based headed MCP server is configured globally, because every Codex session shows another Python app in the Dock.

Example Configuration

A global MCP setup with headed browser tools such as:

[mcp_servers.browser-use]
command = "browser-use"
args = ["--headed", "--profile", "Default", "--mcp"]

[mcp_servers.browser-use-cdp]
command = "browser-use"
args = ["--headed", "--cdp-url", "http://127.0.0.1:9222", "--mcp"]

Starting or resuming multiple Codex sessions results in each Codex process owning its own MCP child processes, for example:

codex resume ...
  browser-use --headed --profile Default --mcp
  browser-use --headed --cdp-url http://127.0.0.1:9222 --mcp
  npm exec @playwright/mcp@latest --browser=chrome

Expected Behavior

One of these would avoid the accumulation:

  • Lazy-start MCP servers only when a tool from that server is first used.
  • Add a config option to mark MCP servers as lazy/on-demand vs eager.
  • Avoid auto-starting headed/browser MCP servers until needed.
  • Clearly shut down unused MCP servers after an idle timeout.

Actual Behavior

Each Codex session starts the configured MCP server processes and keeps them alive for the lifetime of that Codex session. If several old sessions remain open, the user sees many headed MCP processes, even if none of those sessions are actively using browser tooling.

The child processes are not necessarily orphaned: their parent is still the live Codex session. So this is not always a process leak in the OS sense. The problem is eager startup and session-lifetime retention of GUI-capable MCP tools.

Collision Notes

This does not appear to be primarily a Chrome profile or CDP port collision with current browser-use behavior:

  • browser-use --profile Default copies the Chrome profile into a temporary user data directory before launch.
  • The local browser launcher chooses a free remote debugging port.
  • A browser_close_all style tool can close active browser sessions, but it does not necessarily terminate the MCP server process while Codex keeps that server connected.

So the confusing user-visible failure mode is many Dock/process-list entries, not necessarily profile corruption.

Why This Matters

Headed MCP browser tools are useful for auth-heavy/operator flows where isolated Playwright tests are the wrong model. But when configured globally, eager startup makes normal Codex usage feel like it is spawning unexplained GUI apps.

A lazy MCP lifecycle would make global browser-tool configuration much safer and less surprising.

View original on GitHub ↗

10 Comments

github-actions[bot] contributor · 2 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #20883
  • #21318
  • #20494
  • #20349
  • #20992

Powered by Codex Action

BaseInfinity · 2 months ago

I confirmed the core behavior with a black-box red/green repro on Codex CLI 0.130.0.

The test configures a dummy MCP server whose command only creates a probe file, then asks Codex to reply OK and not use tools.

Repro command shape:

tmpdir=$(mktemp -d /tmp/codex-mcp-repro.XXXXXX)
log="$tmpdir/probe.touched"
out="$tmpdir/out.jsonl"

codex exec --ignore-user-config --skip-git-repo-check --ephemeral --json \
  -c 'mcp_servers.probe.command="/usr/bin/touch"' \
  -c "mcp_servers.probe.args=[\"$log\"]" \
  'Reply exactly OK. Do not use any tools.' > "$out" 2>&1

test -e "$log" && echo "probe launched"

Control, without MCP config:

control_no_probe=PASS

Current behavior with the configured dummy MCP server:

lazy_expected_no_probe_with_config=FAIL (red)
eager_observed_probe_with_config=PASS (green)

The JSONL output only shows the model replying OK; no MCP tool is invoked. The probe file is still created, which proves the configured MCP server process was started eagerly before actual MCP tool use.

This issue may be a concrete headed-browser/process-lifecycle symptom of the broader lazy-loading request in #2335, with overlap with #20883 for per-session MCP process pooling. It also relates to #21318, but that issue focuses on startup/tool-discovery blocking; this one focuses on unwanted process startup and accumulation for GUI/headed MCP servers.

BaseInfinity · 2 months ago

Possible design direction, if maintainers agree this belongs as more than a duplicate of #2335:

  1. Preserve current behavior by default:
[mcp_servers.example]
startup_mode = "eager" # default/current behavior
  1. Add explicit opt-in modes for expensive or GUI/headed servers:
[mcp_servers.browser-use]
startup_mode = "manual" # never start automatically; user/session must load it explicitly

[mcp_servers.docs]
startup_mode = "lazy" # do not start during session init; start on first use/search/load
  1. Keep required servers eager or reject invalid config:
required = true
startup_mode = "eager"

A required = true server probably should not be manual, because codex exec already has useful fail-fast semantics for required MCP startup failures.

  1. Make unloaded servers visible without starting them. Possible options:
  • show a lightweight MCP status entry for unloaded servers;
  • add a /mcp load <server> command in the TUI;
  • add a CLI one-shot allowlist such as --with-mcp=playwright,browser-use;
  • use cached or declarative tool metadata where available, but do not require every arbitrary MCP server to start just to make the first turn work.
  1. Keep process pooling as a related but separate layer. startup_mode reduces unnecessary startup; #20883-style project/session pooling reduces duplication when a server is actually loaded.

This would let users keep globally useful MCP definitions without paying the process, UI, and memory cost in every session. It would also give headed/browser MCP servers a safer path: configured globally, but started only when the user actually needs real browser automation.

If this direction is aligned with maintainers' expectations, I can attempt a scoped PR for the config shape and initial manual/lazy loading behavior. No worries if this should instead stay consolidated under #2335.

BaseInfinity · 2 months ago

One more design note after looking at the two browser-use variants:

browser-use-cdp should probably be treated as a distinct, valuable opt-in mode rather than just another browser server to eagerly start. CDP is useful specifically because it can attach to an already-running Chrome/Chromium instance in a known state: logged in, on the right tab, with the user's existing cookies/profile/session. That is different from launching an isolated browser.

The lifecycle fix should preserve that value while avoiding accidental attachment/process buildup. A good shape might be:

  • Do not start/attach CDP-backed MCP servers until a tool from that server is actually selected or the user explicitly enables the session.
  • Keep CDP attachment session-scoped and visible, since it can control an existing browser.
  • Avoid sharing one CDP endpoint across multiple Codex sessions unless the user explicitly opted into that, because multiple agents can collide on tabs/state.
  • Prefer project/session-level opt-in for CDP over global always-on startup.

So the bug is not that CDP exists; the bug is that eager MCP startup makes a powerful attach-to-my-browser workflow happen too early and too often.

mySebbe · 2 months ago

Cross-linking a focused mitigation branch relevant to this eager-start behavior:

https://github.com/openai/codex/compare/main...mySebbe:codex:mysebbe/defer-subagent-mcp-startup?expand=1

Since opening a PR against openai/codex from this account currently fails with mySebbe does not have the correct permissions to execute CreatePullRequest, maintainers can fetch it directly:

git fetch https://github.com/mySebbe/codex.git mysebbe/defer-subagent-mcp-startup:mysebbe/defer-subagent-mcp-startup
git switch mysebbe/defer-subagent-mcp-startup

This branch does not implement a user-facing per-server startup_mode setting. It is narrower: forked/subagent sessions can inherit the parent MCP tool snapshot and keep MCP clients lazy unless required/explicitly used. That should reduce the duplicate stdio process buildup discussed here without changing root-session eager behavior by default.

c-ameron · 2 months ago

This would be very helpful to me. There is no feedback within the GUI app that this is happening, however due to some slow MCP servers (I had to have a wrapper script to stagger auth), I ended up having to wait 80 seconds before it starts to actually process something (even a 0s "what is the time?")

Keesan12 · 1 month ago

The key split here is "tool availability" versus "process admission." A lot of the pain comes from treating them as the same decision.

For headed/browser MCPs, the safer default is:

  • advertise the server in tool discovery immediately
  • defer process launch until the first real call
  • after launch, keep a tiny lease record explaining why it is still alive (owning_session, last_tool_call_at, idle_timeout_at)
  • tear it down when the lease expires instead of pinning it to the whole session lifetime

That keeps globally configured browser tooling usable without turning every idle Codex session into a visible GUI process farm. We ran into the same class of issue with long-lived coding-agent runtimes, and the big win was making admission explicit: a process should be alive because something still owns it, not just because a session once had access to it.

Necmttn · 29 days ago

Split MCP lifecycle into configured, advertised, process started, first tool invoked, leased, and idle expired. Tool discovery can advertise a manifest without owning a process; the first actual call starts it and writes a lease receipt with owning session, last call, pid, and idle deadline.

Diagnostics can then show whether latency came from manifest load or process admission, and headed/browser servers stop accumulating across idle sessions.

---

_Generated with ax._

ReenigneArcher · 29 days ago

Never had this problem with copilot, only 1 instance was started no matter how many agents were used. With codex it's horrendous and all my ram is consumed. I am constantly destroying the MCP containers, very annoying.

shio-chan-dev · 21 days ago

I can reproduce the high baseline memory impact on Linux/WSL2 with chrome-devtools-mcp.

Environment:

  • codex-cli 0.142.4
  • Linux WSL2: 6.6.87.2-microsoft-standard-WSL2, x86_64
  • machine memory: 7.6 GiB RAM + 2 GiB swap
  • chrome-devtools-mcp watchdog args show app version 1.4.0

While investigating high memory usage, I found that multiple live Codex sessions had started separate chrome-devtools-mcp process trees even though I was not intentionally using the Chrome DevTools MCP tool in those sessions.

Using /proc/*/smaps_rollup PSS to avoid over-counting shared memory:

2027.4 MiB PSS   42 procs  chrome-devtools-mcp-family
 707.7 MiB PSS   20 procs  codex-family

The individual process list had many repeated trees like:

npm exec chrome-devtools-mcp@latest
  sh -c chrome-devtools-mcp
    chrome-devtools-mcp
      node .../chrome-devtools-mcp/build/src/telemetry/watchdog/main.js --parent-pid=...

Typical RSS values were roughly:

chrome-devtools-mcp: ~175-178 MiB RSS each
watchdog node:       ~149-151 MiB RSS each

So the practical effect on a small 8 GiB dev machine is that idle or mostly idle Codex sessions can keep around ~2 GiB of browser MCP helper processes.

I also pulled current main locally and saw that there are recent mitigations around MCP manager shutdown/reuse, but this still does not appear to be true lazy startup. The current session startup path still constructs an MCP connection manager and initializes enabled MCP servers, so this issue seems only partially mitigated unless the process admission model changes.

Expected behavior for heavy/browser MCPs:

  • configured MCP servers can be discoverable without immediately owning a process;
  • process launch happens on first actual tool use or explicit /mcp load;
  • started MCP servers have an idle timeout or lease tied to actual use;
  • multiple idle Codex sessions should not each retain their own browser MCP process tree indefinitely.