Codex Desktop should use a project-scoped MCP process pool instead of starting MCP per session

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

Codex Desktop currently appears to start stdio MCP servers per loaded chat/session instead of sharing them per project/workspace.

Observed behavior:

  • Opening the first chat in a project starts MCP server processes.
  • Opening another existing chat in the same project can start another MCP process group or restart the previous one.
  • Creating a new chat inside the same project can also start a new MCP process group.
  • Projectless/non-project chats may start MCP servers too.
  • This is easy to observe with Serena because it creates serena.exe, but it also affects other stdio MCP servers such as servers launched through npx or uvx.

Expected behavior:

  • MCP servers should be scoped to the normalized project/workspace root.
  • All chats/sessions/subagents in the same project should reuse one shared MCP pool.
  • Switching between chats in the same project should not kill or restart MCP servers.
  • Creating a new chat in an already loaded project should attach to the existing project MCP pool.
  • Projectless chats should not start project-scoped MCP servers.

Suggested fix:

  • Move MCP ownership from individual sessions to the app-server/thread-manager level.
  • Keep a shared MCP pool keyed by normalized project/workspace root, not by chat/session id.
  • Treat projectless chats as mcp_project_root = None, so MCP is disabled for them.
  • Initialize MCP once per project pool and reuse it for all sessions/subagents in that project.
  • Do not refresh/recreate the MCP manager during every session initialization; only refresh on explicit MCP reload/config changes.

I also prototyped a local proof-of-concept patch that fixes this behavior on my machine.

The patch changes MCP ownership from per-session to an app-server/thread-manager-level shared pool:

  • ThreadManager owns a shared MCP pool.
  • Sessions receive mcp_project_root: Option<PathBuf>.
  • None disables MCP for projectless chats.
  • Some(project_root) attaches the session to a project-scoped MCP pool.
  • The pool is keyed by normalized codex_home + project_root.
  • Session startup uses initialize-once semantics instead of recreating MCP on every session load.
  • Explicit MCP reload can still force-refresh the pool.

I can attach the patch file if useful:

shared-mcp-pool.patch
shared-mcp-pool.patch

View original on GitHub ↗

14 Comments

github-actions[bot] contributor · 2 months ago

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

  • #20494
  • #19469
  • #19845
  • #19858
  • #20349

Powered by Codex Action

AIast0r · 2 months ago

@etraut-openai please do not close this as a duplicate. So here I'm not talking about the problem, but about its solution.

AIast0r · 2 months ago

Also, it may seem to me, but it's like useless memory leaks have gone away, which clogged my swap file from 15gb to 55gb:(

Latand · 2 months ago

Same here

Please fix

mySebbe · 2 months ago

Additional Windows data point from Codex Desktop 26.506.3741.0.

I also submitted in-app feedback with correlation ID:

8e832ac8-8641-4794-9005-cdd1ce9d3812

Observed before reboot in one long-lived Codex Desktop session on Windows:

Codex app-server PID: 8900
Package: OpenAI.Codex_26.506.3741.0_x64
Platform: Windows 11

Duplicated MCP/helper process families under the same app-server:
- ue-mcp actual servers:        46  (~3.4 GB working set)
- xcodebuildmcp actual servers: 40  (~3.5 GB working set)
- hzos-dev-mcp.exe:             50  (~1.0 GB working set)
- node_repl.exe:                45  (~0.8 GB working set)
- ue/xcode wrapper processes:  259  (~7.0 GB working set)
- claude mcp serve:              4  (~0.5 GB working set)
- gemini-cli MCP:                2  (~0.15 GB working set)

Important detail: these were mostly not Windows-orphaned processes. They remained direct or descendant children of the live codex.exe app-server, so killing/restarting Codex/Windows cleaned them up. The issue looked like logical lifecycle leakage: new thread/subagent/tool contexts started fresh MCP/helper stacks while previous ones stayed alive.

Subagents made the symptom visibly worse. Starting several subagents caused new instances of hzos-dev-mcp, node_repl, ue-mcp, xcodebuildmcp, plus claude/gemini MCP processes to appear in the same app-server envelope.

Relevant local log signals from %USERPROFILE%\.codex\logs_2.sqlite:

RunningService dropped without explicit close(): 2414 occurrences
failed to initialize MCP client during shutdown: 108 occurrences
MCP startup failed: timed out handshaking: 79 occurrences

Examples included shutdown paths like:

RunningService dropped without explicit close(). The connection will be closed asynchronously. For guaranteed cleanup, call close() or cancel() before dropping.
failed to initialize MCP client during shutdown: MCP startup failed: timed out handshaking with MCP server after 30s/60s/120s

After a full Windows reboot and disabling/removing the previous explicit MCP starters, the massive ue-mcp/xcodebuildmcp/hzos-dev-mcp buildup was gone. Browser Use is still enabled and node_repl.exe instances are present, but currently bounded compared to the pre-reboot state.

This seems consistent with the issue title here: MCP/helper processes are scoped too narrowly to sessions/subagents/tool contexts, or old session-scoped managers are not explicitly closed/cancelled. A project/app-server-scoped pool plus explicit teardown on session/subagent shutdown would likely address this class of leak.

Related issues I found while triaging: #17574, #17832, #18881, #20349, #20867, #20980.

mySebbe · 2 months ago

I put together a focused draft patch branch for this class of issue:

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

Scope:

  • adds a lazy MCP startup mode so forked/subagent sessions can construct an MCP manager without immediately polling every enabled stdio client
  • passes an inherited MCP tool snapshot from the live parent thread into forks/subagents
  • uses that snapshot for model-visible MCP tool inventory, code-mode nested tools, app instructions, and MCP tool-name resolution
  • keeps eager startup for root sessions, explicit refresh/status/resource paths, and required MCP servers
  • skips shutdown polling for lazy clients that were never started

I tried to open this as a draft PR, but both gh pr create and the GitHub connector were blocked for this account/integration (CreatePullRequest / Resource not accessible by integration). Local verification was limited to git diff --check; this Windows test machine does not have cargo/rustc installed, so it still needs CI/maintainer validation.

mySebbe · 2 months ago

Additional narrower Windows node_repl.exe data point from Codex Desktop 26.506.3741.0.

This confirms the behavior is coming from the installed Desktop app, not from a local source checkout or custom build:

Codex.exe PID 6448
  path: C:\Program Files\WindowsApps\OpenAI.Codex_26.506.3741.0_x64__2p2nqsd0c76g0\app\Codex.exe
codex.exe app-server PID 8536
  path: C:\Program Files\WindowsApps\OpenAI.Codex_26.506.3741.0_x64__2p2nqsd0c76g0\app\resources\codex.exe

Current observation after a reboot and normal Codex/subagent usage:

node_repl.exe children under app-server PID 8536: 34
combined working set: ~639 MB
all node_repl.exe command lines:
  C:\Users\sebbe\AppData\Local\OpenAI\Codex\bin\node_repl.exe

Only one of those node_repl.exe processes currently has an actual Node kernel child:

node_repl.exe PID 13420
  child node.exe PID 10220
  node.exe --experimental-vm-modules ...\kernel.js --session-id 9aa94c3d7b7941e4bf89821d45e67b14

So the leak shape is mostly idle/stale node_repl.exe MCP host processes, not many active JS kernels.

The process creation times correlate with Codex thread/subagent starts from state_5.sqlite. Closing diagnostic subagents emitted fresh shutdown log entries like:

rmcp::service: RunningService dropped without explicit close().
The connection will be closed asynchronously. For guaranteed cleanup, call close() or cancel() before dropping.

After those subagents closed, the corresponding node_repl.exe count did not return to baseline. In this local run it moved from 32 to 34 during the investigation, which makes this a useful black-box repro for the teardown side of the issue.

Important distinction: there is a local patch branch experimenting with lazy MCP startup for forked/subagent sessions, but the observed live processes are all parented by the installed Windows app-server above. The earliest observed stale node_repl.exe in this session also predates that local patch commit, so this is not caused by the local patch.

Related issue with the eager-start side of the same lifecycle problem: #21984.

mySebbe · 2 months ago

Maintainer handoff for the draft patch branch, since GitHub currently rejects opening a PR from my fork with:

mySebbe does not have the correct permissions to execute CreatePullRequest

The branch is still available and fetchable:

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

Commit:

c00e26d Defer MCP startup for forked sessions

Compare view:

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

Patch intent:

  • add McpStartupMode::{Eager, Lazy}
  • keep root sessions / required MCP servers / explicit refreshes eager
  • let forked/subagent sessions inherit the parent MCP tool snapshot when possible
  • use that snapshot for tool inventory and tool-name resolution
  • avoid starting and then shutting down unused stdio MCP clients for subagents/forks

This is not meant as the full project-scoped MCP process pool from the issue title. It is a smaller mitigation for the observed duplicate-process amplification caused by subagents/forks and tool inventory startup. Maintainer review would be especially useful around Desktop/app-server lifecycle assumptions and whether the inherited snapshot should be invalidated in more cases.

mnuradli1 · 2 months ago

Linux headless app-server data point (OpenClaw gateway)

Confirming this affects the headless codex app-server --listen stdio:// path on Linux, not just Desktop.

Environment:

  • Ubuntu 24.04, 8 GB RAM VPS
  • @openai/codex-linux-x64 via OpenClaw gateway (Telegram bot → Codex app-server)
  • 11 MCP servers configured (gmail, google-calendar, gbrain, n8n, github, filesystem, youtube-transcript, fetch, time, hermes-tools, lazyweb)

Observed behavior:
After ~5 minutes of normal Telegram bot usage, the single codex app-server process (PID 2829) had spawned 3 complete generations of MCP server processes — 33 direct children total.

Generation 1 (elapsed 04:07): 11 MCP processes, ~891 MB RSS
Generation 2 (elapsed 03:52): 11 MCP processes, ~884 MB RSS  
Generation 3 (elapsed 03:35): 11 MCP processes, ~823 MB RSS (active)
Total under codex app-server: 2,598 MB RSS (33 children)

Each generation includes a full duplicate set: google-calendar-mcp (155 MB each), gmail-mcp (118 MB), hermes-tools-mcp (118 MB), n8n mcp-remote (108 MB), gbrain serve (94 MB), etc. Plus 3 Docker containers for n8n-mcp (one per generation).

This caused swap thrashing (kswapd0 pinned at top CPU) and triggered the hosting provider's automated CPU throttling.

Workaround: systemctl restart of the parent service clears all leaked generations. We've added MemoryMax=2G in systemd and a cron-based periodic restart as a safety net until this is fixed upstream.

The project-scoped MCP pool proposed here would completely resolve this for the headless app-server use case as well.

Keesan12 · 1 month ago

A project-scoped pool is probably the right destination, but I would make one boundary explicit so the pool does not become another immortal blob: shared process lifetime should be tied to active capability leases, not just project identity.

In practice the useful fields are small and boring:

  • project/workspace id
  • helper kind
  • current owners
  • last real tool call
  • idle timeout
  • refresh generation

That lets the runtime share expensive helpers across threads without losing the operator’s ability to answer \"who still owns this process and why is it alive?\" We ran into the same thing in MartinLoop: pooling helped, but only after lease receipts made teardown explainable instead of magical.

AIast0r · 1 month ago

@etraut-openai When will the problem be fixed?

wherewolf87 · 7 days ago

Additional current-build evidence supporting shared or project-scoped MCP ownership is in #32797.

On Windows package 26.707.8479.0, one Desktop app-server retained three distinct MCP/Node batches totaling 83 node.exe, 3 node_repl.exe, and 54 cmd.exe wrappers. Most configured MCP tools had not been deliberately invoked in the visible task. The three process batches remained simultaneously live and consumed about 7.74 GiB working set.

The desired behavior is not to make tools unavailable. It is to preserve automatic tool availability while reusing one bounded pool, lazily starting server processes when supported, and reliably terminating superseded session/manager pools.

LukeMainwaring0 · 5 days ago

I reproduced this on macOS arm64 with Codex App 26.707.72221 / bundled codex-cli 0.144.2, and narrowed the long-lived growth to subscribed-thread lifecycle rather than the MCP server implementations themselves.

Production-profile snapshot:

  • 18 loaded rollouts
  • 141 direct children under the live app-server
  • about 13.2 GB RSS across the Codex process tree before deeper descendant accounting
  • nearly one full node_repl + five plugin MCP servers + xcodebuildmcp group per loaded thread

Controlled test using the same prompt and binary in an isolated profile:

  • lean configuration: 4 processes / 128.9 MB RSS
  • same task with six MCP servers: 11 processes / 799.4 MB RSS
  • incremental cost: about 670 MB per loaded task
  • five Node plugin servers: 320.3 MB
  • xcode MCP launcher + child: 309.2 MB
  • app-server/core + sleep: 169.9 MB

In tag rust-v0.144.2, codex-rs/app-server/src/request_processors/thread_lifecycle.rs only schedules the existing 30-minute idle unload when a thread is inactive and has zero subscribers. The Desktop app appears to keep background/sidebar threads subscribed, so that condition never becomes true; the per-thread McpConnectionManager therefore remains alive and its children do not drop.

I tested an experimental, opt-in backend safety valve that permits subscribed-but-idle thread eviction after a separately configured timeout. It is disabled by default, so existing behavior is unchanged unless explicitly enabled. Four focused Rust unit tests passed.

End-to-end App Server test with a real persisted turn, an active subscription, and the six-server stack:

  • before idle eviction: 9 processes / 819 MB RSS
  • after 5 idle seconds: 1 process / 171 MB RSS; MCP helpers gone and thread/loaded/list empty
  • resume of the same persisted thread: succeeded; tools/helpers restored, 9 processes / 835 MB RSS

This preserved thread history and tool capability while releasing the idle MCP processes. My read is that the preferred product fix is for Desktop to unsubscribe background threads correctly, with a backend subscribed-idle eviction policy as defense in depth. Renderer retention appears to be a separate issue.

AIast0r · 5 days ago

can someone do a PR fix for this?