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

Open 💬 14 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 ↗

14 Comments

github-actions[bot] contributor · 3 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 · 3 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 · 3 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 · 3 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 · 3 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 · 3 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 · 2 months 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 · 2 months 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 · 2 months 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 · 1 month 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.
YouAreOK · 1 month ago

I can reproduce this MCP lifecycle/resource retention issue on Codex Desktop for Windows.

Environment

  • Codex Desktop: 26.715.8383.0
  • Installation: Microsoft Store / MSIX
  • OS: Windows 11 Enterprise LTSC, build 26100, x64
  • Device: Lenovo 21TK
  • CPU: Intel Core Ultra 7 255H, 16 logical processors
  • RAM: 32 GB
  • MCP servers include Playwright MCP, Playwright test MCP, simple-yapi-mcp, bundled MCP servers, and node_repl.

Reproduction steps

  1. Start Codex Desktop.
  2. Open or resume a thread.
  3. Allow the turn to complete.
  4. Switch to another thread and resume it.
  5. Repeat this with several threads.
  6. Leave the completed/inactive threads idle.
  7. Inspect node_repl, node, cmd, and volta processes in Task Manager or PowerShell.

Actual behavior

The MCP/runtime processes remain alive after their corresponding turns have completed and the threads are no longer active.

In one reproduction, a single codex.exe app-server retained five separate node_repl.exe direct child processes, started at different times:

  • 10:33:46
  • 10:34:04
  • 12:37:06
  • 12:46:03
  • 12:49:37

The resulting process counts were:

  • 1 × codex.exe
  • 5 × node_repl.exe
  • 40 × node.exe
  • 51 × cmd.exe
  • 15 × volta.exe

All five node_repl processes were direct children of the same live codex.exe app-server. They were not OS-orphaned processes.

The Codex Desktop logs correlate new node_repl process creation with thread resume events. When switching threads, the logs show:

  • the previous thread changing to active=false
  • the next thread changing to active=true
  • thread/resume
  • disposeAfterSessionActivity=false

In the inspected log:

  • disposeAfterSessionActivity=false: 37 occurrences
  • disposeAfterSessionActivity=true: 0 occurrences
  • MCP/node_repl shutdown, terminate, or dispose messages: 0 occurrences

Fully terminating Codex Desktop removes the process trees, but normal turn completion and switching threads do not.

Impact

The number of Node/npm/MCP processes grows with the number of resumed threads. On an enterprise Windows device, endpoint security and Defender inspect these additional process trees, causing sustained CPU usage in security agents, WMI Provider Host, and Defender components.

Restarting Codex temporarily resolves the problem, but the processes accumulate again as threads are resumed.

Expected behavior

One of the following should happen:

  1. MCP servers for completed/inactive threads should be terminated after an idle timeout.
  2. Finished or archived threads should explicitly dispose their MCP connections and child process trees.
  3. Codex should enforce a maximum number of retained inactive runtimes using LRU eviction.
  4. MCP servers should start lazily only when their tools are used.
  5. A user-facing command should allow restarting or disposing MCP servers for an inactive thread.

This appears related to #11324 and #21984, but this reproduction is specifically from Codex Desktop on Windows/MSIX.
PS C:\Windows\system32> Get-Process |

> Group-Object ProcessName | > Where-Object Name -in @( > 'ChatGPT','codex','node','node_repl','cmd','volta' > ) | > Sort-Object Name | > Select-Object Name, Count

Name Count
---- -----
ChatGPT 7
cmd 51
codex 1
node 40
node_repl 5
volta 15

PS C:\Windows\system32> Get-CimInstance Win32_Process |

> Where-Object Name -eq 'node_repl.exe' | > Select-Object ProcessId, ParentProcessId, CreationDate

ProcessId ParentProcessId CreationDate
--------- --------------- ------------
35772 25272 2026/7/22 10:33:46
18204 25272 2026/7/22 10:34:04
15228 25272 2026/7/22 12:37:06
42944 25272 2026/7/22 12:49:37
30456 25272 2026/7/22 13:10:47

saerda8 · 23 days ago

Windows repro/update on Codex Desktop 26.727.6591.0 (MSIX, Windows 11):

  • One live official codex.exe app-server had 10 distinct globally configured stdio MCP commands.
  • While several tasks/subagents were loaded, I observed 120 direct Node children: exactly 12 copies of each of the 10 commands.
  • After some subagents completed, it fell to 50 children: 5 copies of each command, still about 2.62 GiB RSS in aggregate.
  • These copies are direct children of the still-live official app-server, not OS-level orphans. A naive "keep at most N copies per command" cleanup is unsafe: it can disconnect unrelated live tasks.
  • The MCP tools were not called by most of those subagents; the full configured set was nevertheless started.

This strongly matches eager per-session/subagent MCP initialization. A per-server on_demand/lazy-start option, reference-counted shared pool, or deterministic idle teardown after subagent completion would avoid both the startup pressure and the unsafe need for external process cleanup.

alexx-ftw · 17 days ago

Linux/Codex Desktop reproduction with a single live app-server (codex 0.147.0):

  • Immediately after a controlled Desktop restart, the app-server grew to 175 direct children while resuming tasks. With more active tasks it reached 236 direct children.
  • Repeated families included Serena, chrome-devtools-mcp, xcodebuildmcp, Context Mode, and the bundled/curated stdio plugin servers. Each task owned a separate cohort.
  • Before restart, the Codex tree was approximately 16.4 GiB RSS plus 29.2 GiB swap; host swap reached 44-47 GiB. During resume, the app-server itself read approximately 246-359 MiB/s and Linux I/O PSI reached full avg10 around 41-63%, with dozens of tasks blocked in D-state.
  • The installed Desktop bundle already contains an inactive-owner limit of four and computes overage = max(0, inactiveOwners.length - maxInactiveOwnerThreads), but the selection returned only TTL-expired threads. The computed overage was not included in conversationIdsToUnsubscribe, so the one-hour TTL remained the effective cleanup path.
  • A local fail-closed patch that selects the oldest overage owners plus TTL-expired owners (while preserving active-view, follower/reconnect, approval, active-runtime, in-progress, goal, and subagent guards) is working. After thread/unsubscribe, the existing app-server 30-minute idle shutdown path can tear down that thread's MCP runtime normally.
  • For active tasks, relative cgroup containment of MCP roots (CPUWeight=1, IOWeight=1, no CPU quota, no io.max, no memory/swap cap) protects the interactive app-server/UI without stopping work, but it does not solve the retained per-task memory or eager startup architecture.

The durable upstream fix still needs lazy MCP startup and/or lifecycle sharing only for transports that explicitly support it, plus bounded inactive subscription eviction in Desktop. Indiscriminately sharing stdio servers is not safe because auth, cwd, state, and session isolation can differ.

BaseInfinity · 17 days ago

Thanks — this is especially useful because it separates two related problems: eager MCP startup and inactive-owner retention.

Could you share the minimum details needed to reproduce/validate the retention mitigation upstream?

  • the exact Codex Desktop build (and bundle/platform, if relevant);
  • the file/module and function where overage is computed;
  • a minimal diff or pseudocode showing how the oldest overage owners are added to conversationIdsToUnsubscribe;
  • before/after retained owner and MCP child-process counts under the same resume workload and observation interval;
  • confirmation that the existing active-view, follower/reconnect, approval, active-runtime, in-progress, goal, and subagent exclusions remain unchanged.

A branch, gist, or compact patch would be ideal if you can share one. That would give maintainers a directly testable bounded-inactive-eviction fix while keeping the larger lazy-start/process-admission design in this issue separate.