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 throughnpxoruvx.
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:
ThreadManagerowns a shared MCP pool.- Sessions receive
mcp_project_root: Option<PathBuf>. Nonedisables 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
14 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
@etraut-openai please do not close this as a duplicate. So here I'm not talking about the problem, but about its solution.
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:(
Same here
Please fix
Additional Windows data point from Codex Desktop
26.506.3741.0.I also submitted in-app feedback with correlation ID:
Observed before reboot in one long-lived Codex Desktop session on Windows:
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, plusclaude/geminiMCP processes to appear in the same app-server envelope.Relevant local log signals from
%USERPROFILE%\.codex\logs_2.sqlite:Examples included shutdown paths like:
After a full Windows reboot and disabling/removing the previous explicit MCP starters, the massive
ue-mcp/xcodebuildmcp/hzos-dev-mcpbuildup was gone. Browser Use is still enabled andnode_repl.exeinstances 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.
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:
I tried to open this as a draft PR, but both
gh pr createand the GitHub connector were blocked for this account/integration (CreatePullRequest/Resource not accessible by integration). Local verification was limited togit diff --check; this Windows test machine does not havecargo/rustcinstalled, so it still needs CI/maintainer validation.Additional narrower Windows
node_repl.exedata point from Codex Desktop26.506.3741.0.This confirms the behavior is coming from the installed Desktop app, not from a local source checkout or custom build:
Current observation after a reboot and normal Codex/subagent usage:
Only one of those
node_repl.exeprocesses currently has an actual Node kernel child:So the leak shape is mostly idle/stale
node_repl.exeMCP 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:After those subagents closed, the corresponding
node_repl.execount 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.exein 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.
Maintainer handoff for the draft patch branch, since GitHub currently rejects opening a PR from my fork with:
The branch is still available and fetchable:
Commit:
Compare view:
https://github.com/openai/codex/compare/main...mySebbe:codex:mysebbe/defer-subagent-mcp-startup?expand=1
Patch intent:
McpStartupMode::{Eager, Lazy}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.
Linux headless
app-serverdata point (OpenClaw gateway)Confirming this affects the headless
codex app-server --listen stdio://path on Linux, not just Desktop.Environment:
@openai/codex-linux-x64via OpenClaw gateway (Telegram bot → Codex app-server)Observed behavior:
After ~5 minutes of normal Telegram bot usage, the single
codex app-serverprocess (PID 2829) had spawned 3 complete generations of MCP server processes — 33 direct children total.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 (
kswapd0pinned at top CPU) and triggered the hosting provider's automated CPU throttling.Workaround:
systemctl restartof the parent service clears all leaked generations. We've addedMemoryMax=2Gin 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-serveruse case as well.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:
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.
@etraut-openai When will the problem be fixed?
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 83node.exe, 3node_repl.exe, and 54cmd.exewrappers. 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.
I reproduced this on macOS arm64 with Codex App
26.707.72221/ bundledcodex-cli 0.144.2, and narrowed the long-lived growth to subscribed-thread lifecycle rather than the MCP server implementations themselves.Production-profile snapshot:
node_repl+ five plugin MCP servers +xcodebuildmcpgroup per loaded threadControlled test using the same prompt and binary in an isolated profile:
In tag
rust-v0.144.2,codex-rs/app-server/src/request_processors/thread_lifecycle.rsonly 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-threadMcpConnectionManagertherefore 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:
thread/loaded/listemptyThis 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.
can someone do a PR fix for this?