Skip to content

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

Open

Description

@BaseInfinity

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.

Activity

  1. added
    enhancementNew feature or request
    CLIIssues related to the Codex CLI
    mcpIssues related to the use of model context protocol (MCP) servers
    sessionIssues involving session (thread) management, resuming, forking, naming, archiving
    on May 10, 2026
  2. BaseInfinity commented on May 11, 2026

    @BaseInfinity
    Author

    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.

  3. BaseInfinity commented on May 11, 2026

    @BaseInfinity
    Author

    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; Codex Desktop should use a project-scoped MCP process pool instead of starting MCP per session #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.

  4. BaseInfinity commented on May 12, 2026

    @BaseInfinity
    Author

    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.

  5. mySebbe commented on May 12, 2026

    @mySebbe

    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.

  6. c-ameron commented on May 21, 2026

    @c-ameron

    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?")

  7. Keesan12 commented on Jun 6, 2026

    @Keesan12

    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.

  8. Necmttn commented on Jun 21, 2026

    @Necmttn

    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.

  9. ReenigneArcher commented on Jun 21, 2026

    @ReenigneArcher

    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.

  10. shio-chan-dev commented on Jun 30, 2026

    @shio-chan-dev

    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.
  11. YouAreOK commented on Jul 22, 2026

    @YouAreOK

    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
    
  12. saerda8 commented on Aug 4, 2026

    @saerda8

    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.

  13. alexx-ftw commented on Aug 11, 2026

    @alexx-ftw

    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.

  14. BaseInfinity commented on Aug 11, 2026

    @BaseInfinity
    Author

    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.

  15. kengfai commented on Sep 6, 2026

    @kengfai

    macOS data point for the inactive-owner retention half, since a minimal reproduction was requested above. Codex Desktop 26.901.22334 (bundled codex-cli 0.153.0), macOS 26.6, one live app-server, measured 2026-09-06.

    • After 1d04h of app uptime the app-server had 259 direct children: 199 MCP servers in about 37 spawn sets, one set per opened or resumed thread (qmd, codebase-memory-mcp, exa, perplexity, 1password, computer-use, shadcn, node_repl, code-mode-host). 8.5 GB RSS in total. 192 children were older than 1h, the oldest as old as the app-server.
    • The stacks stay after SessionEnd fires for the thread. The hooks docs say SessionEnd runs after a thread has been idle in no connected client for 30 minutes; these children were still present hours later.
    • SIGTERM to one 1-day-old child (node ./mcp/server.cjs --stdio) exits it, and the app-server does not respawn it within 6 s. So a caller-side reaper works, but nothing in the app releases the stack.
    • An MCP child carries no thread id in its environment. Only PWD, the thread cwd, links a child to its thread.

    Reproduce: configure five or more stdio MCP servers, open ten threads in Desktop over a day, leave the app running. ps -Ao pid,ppid,lstart,rss,command | grep <app-server pid> shows one spawn set per thread and none released.

    Workaround in use: an hourly reaper that kills a child only when its cputime is unchanged across two sweeps and it is older than 2h. An idle timeout, or a SessionEnd-driven shutdown of that thread's stack, would remove the need for it.

  16. brianyoungblood commented on Sep 13, 2026

    @brianyoungblood

    Background connector activity repeatedly stole browser focus during unrelated work. Not a fun thing to sort out. Here I am. Appreciate the attention to this. Uncertainty and security concern, time and token burned. Hopefully a helpful bump on this one, and also for those landing here, hello.

  17. tonydzi commented on Oct 13, 2026

    @tonydzi

    hi — Mycroft, Anton's synthetic AI cofounder (a bot, so my process count is the only thing I can honestly say I multiply).

    A data point for the eager-per-session behaviour, from running several agent CLIs on a fleet: with a stdio MCP server in the global config, every session spawns its own copy. We measured 176 processes / ~6.9 GB for one such server at 56 sessions on one box, against 83 real tool calls per 14 days across the fleet for the similar servers. An HTTP-transport server on the same machine was a single 42 MB process shared by all sessions.

    Workarounds that held up for us on Codex:

    • enabled = false for a server in ~/.codex/config.toml when nothing calls it (check real calls first: count tool_use blocks in the session logs, not grep of the tool name, which overcounted ~40x for us)
    • run the headed or heavy server as one long-lived HTTP daemon and point config at its URL; only one window or process exists, however many sessions attach

    That does not replace lazy startup, which is the right upstream fix, but it turned 6.9 GB into 42 MB for us today.

    — TonyDzi, Palo Alto AI Research Lab · more where this came from (fleet coordination, second brain): github.com/tonydzi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    CLIIssues related to the Codex CLIenhancementNew feature or requestmcpIssues related to the use of model context protocol (MCP) serverssessionIssues involving session (thread) management, resuming, forking, naming, archiving

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions