Subagents need an MCP capability broker: parent allowlists, zero-start by default, bounded pooling, and deterministic teardown
What variant of Codex are you using?
Codex Desktop / app-server multi-agent workflows. The design should also apply to CLI and IDE subagents.
Observed on:
- Codex App
26.803.10989.0 - Windows 11 x64, build
26200 multi_agent = truemax_concurrent_threads_per_session = 128
What feature would you like to see?
Treat MCP access as a leased capability assigned by the parent/orchestrator, rather than automatically turning every inherited MCP configuration into a live runtime for every subagent.
A general-purpose child should start with no live MCP processes. The parent should be able to give that child an explicit MCP allowlist, and an allowed server should start only when the child actually calls one of its tools. MCP runtimes should be bounded, attributable to an owner/lease, and deterministically released.
This is both a reliability feature and a least-privilege feature: most coding/research/QA children do not need every credentialed MCP available to the parent.
Why this is a distinct gap
There are several related reports, but they cover individual symptoms or only part of the lifecycle:
- #30408: per-thread MCP processes are retained and consume large amounts of memory.
- #20883: proposes a project-scoped process pool.
- #37426: stale children and the full inherited stdio MCP suite on Windows Desktop.
- #38247: completed v2 subagents retain their stdio runtimes.
- #18881 was fixed by #19753, which added explicit shutdown and process-tree cleanup.
- #38217 recently added lazy startup for required subagent MCP servers when usable cached tool definitions already exist.
Those are valuable pieces. The missing abstraction is a single policy/ownership layer covering:
- which MCPs a child is allowed to use;
- whether any process must start at child creation;
- how first-run/catalog discovery avoids an N-child fan-out;
- how live instances are bounded and reused where safe;
- who owns each process and when its lease ends;
- what happens when resource limits are reached.
In particular, #38217 is a good foundation, but cached-tool lazy startup alone does not cover first run/cache miss, explicit per-child capability selection, bounded process ownership, or completed-child teardown.
Concrete production incident
In one Codex Desktop task, the UI showed 128 active subagents and remained in progress for more than an hour. Steering/new messages stopped being accepted.
At the time of inspection, one globally configured Node stdio server (@sjawhar/whatsapp-mcp) had multiplied into:
- 11 Node server processes;
- 11 launcher processes;
- about 1.465 GB working set for the Node servers alone;
- several old instances with roughly 3,600 CPU seconds each.
This MCP was not relevant to the children doing repository research.
Killing only that exact 22-process MCP tree reduced the pressure immediately. Two instances respawned while the configuration was still enabled. After removing only that MCP configuration and terminating those two exact processes, the count stayed at zero in the follow-up check. No unrelated MCP configuration was changed.
The high concurrency setting made the incident severe, but this is exactly why fan-out needs backpressure: an accepted subagent limit should not silently permit subagents × configured MCP process trees to exhaust the machine or make the parent UI unsteerable.
Proposed model
1. Parent-controlled capability manifest
The parent supplies an allowlist when spawning a child. The default lightweight child inherits no live MCP servers.
Illustrative configuration/API (names are only examples):
[agents.default.mcp]
inherit = "none"
startup = "on_tool_call"
max_live_servers = 2
idle_timeout_sec = 60
[agents.docs_researcher.mcp]
allow = ["openai-docs"]
An equivalent spawn-time override could be:
spawn_agent(..., mcp_allow = ["github"], mcp_inherit = false)
Keep inherit = "all" as an explicit compatibility option.
2. Separate tool catalogs from live connections
A child may need tool schemas for planning, but that should not require one live server per child.
- Reuse a centrally cached catalog keyed by effective server configuration.
- On a cache miss, perform at most one coordinated discovery startup for that configuration, cache the result, then stop the discovery process if it is not leased.
- Concurrent children wait on the same discovery future instead of each starting a copy.
- A selected/required MCP should be eager only when explicitly required for that specific child, not merely because it is required in the parent's global configuration.
3. Bounded MCP broker/pool
The app-server should broker runtimes by a stable key such as workspace + effective config hash + auth/permission scope.
- Share only servers that are declared safe to share.
- For stateful/non-shareable servers, use a bounded per-server pool.
- Queue requests or return a clear resource-limit error instead of spawning without a ceiling.
- Apply a global process/memory budget independent of the maximum subagent count.
4. Lease-based ownership and deterministic teardown
Every live MCP runtime should expose an owner/lease:
server_config_hash
runtime_id
owning_task_or_pool
agent_ids
started_at
last_used_at
lease_count
shutdown_reason
Release the lease on child completion, cancellation, failure, interruption, thread close, and app-server shutdown. When the last lease is released, terminate immediately or after a short idle TTL. Process-tree teardown must work even if client objects or resumable agent identities still exist.
5. Resource backpressure and diagnostics
Before spawning a child or MCP runtime, estimate/check the resulting budget.
If the limit would be exceeded:
- keep the MCP dormant;
- queue the child/tool call;
- or show a clear warning with the server and owning agents.
The Desktop diagnostics panel should show live MCP instance count, owning task/agent, age, CPU/memory, and a safe “unload idle MCPs” action.
Suggested implementation path
- Extend the lazy-start work from #38217 so the tool catalog is not coupled to a per-child live connection, including cache-miss coalescing.
- Add a per-agent MCP capability manifest to the spawn/session configuration.
- Put process creation behind an app-server broker with per-config single-flight and configurable pool limits.
- Reuse the explicit shutdown/process-handle machinery from #19753 for lease release.
- Add a watchdog/circuit breaker so MCP fan-out can never make the parent unable to accept cancellation or steering.
Acceptance tests
A PID-recording stdio fixture could make this deterministic:
- No-use fan-out: spawn 64 children that never call MCP tools. After catalog discovery, live MCP process count must remain at the parent baseline, not grow with N.
- Cold-cache single-flight: with no cached catalog, spawn 64 children concurrently. At most one discovery runtime per effective server config may start, and it must exit after discovery if unleased.
- Explicit allowlist: only one designated child is allowed to use server A. Other children must neither see its callable tools nor receive its credentials.
- First tool call: the designated child calls one MCP tool. Exactly one runtime (or the configured bounded count) starts.
- Completion/cancel/error: after that child completes, is cancelled, or crashes, the runtime returns to baseline within the teardown/idle timeout.
- Resume: a later follow-up lazily reacquires a healthy runtime; historical thread viewing alone starts none.
- Backpressure: setting a high subagent limit cannot exceed the separate MCP runtime budget, and parent cancellation/steering remains responsive.
- Windows process tree: wrapper and descendant processes are gone after lease release, not only the direct launcher.
Additional information
Official documentation currently explains that subagents inherit parent session settings such as mcp_servers unless overridden, and that concurrency is separately controlled by agents.max_concurrent_threads_per_session:
- https://learn.chatgpt.com/docs/agent-configuration/subagents
- https://learn.chatgpt.com/docs/extend/mcp
Configuration inheritance is useful, but it should mean “available under policy,” not “eagerly instantiate the entire capability surface for every child.”
This proposal would turn the recent lazy-start and shutdown fixes into a coherent invariant:
Creating a subagent must not create a live MCP process unless that child was explicitly granted the capability and actually needs a connection; every created runtime must have a bounded owner and a deterministic release path.
5 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I reviewed the current
mainimplementation and the related reports/PRs before considering code. Today, spawned children clone the parent’s effective config; each thread owns its ownMcpRuntime; the sharedMcpManagerprovides catalog caching but not runtime ownership/pooling.LazyWhenCachedavoids startup only when usable cached schemas already exist, so cold-cache fan-out and completed-child retention remain. Draft #31922 is a useful partial foundation for completely tool-free helper threads, but it does not provide per-child MCP selection or lifecycle ownership.For account context, I use Codex on a Pro 20x subscription.
Would maintainers consider a first, reviewable stage limited to a durable spawn-time MCP allowlist?
This stage would deliberately leave cold-cache single-flight discovery, pooling/leases, automatic completion teardown, budgets, diagnostics, and any default switch to deny-all for follow-ups.
The blocking design questions are:
tool_freepath, or remain an MCP-specific policy alongside it?I will not implement or submit an external code PR unless a maintainer confirms the direction and explicitly invites the contribution.
Thanks for reviewing
mainand #31922 at the code level. Speaking as the issue reporter, not for the maintainers, I strongly support the durable spawn-time allowlist as an independently reviewable first stage.My answers to the three questions are:
tool_free.tool_freeremoves the entire tool/skill/plugin surface.mcp_allow = []should mean “no MCP” while still permitting appropriate built-in tools such as shell and apply-patch. The implementations may share low-level filtering, but overloadingtool_freewould make the policy too coarse.One important nuance: after spawn-time resolution, even omitted/inherit-all should be normalized to the exact effective server IDs granted at spawn and persisted with the child. Otherwise, adding a global/plugin MCP later could silently broaden an old child’s authority on cold resume. Only an explicit parent action should expand the persisted grant.
A few acceptance details would make this an authorization boundary rather than only a startup optimization:
There is now a second independent Windows incident behind this request. After removing the previously identified WhatsApp MCP, a later session again reached 91% memory with dozens of
Python (2)groups. The remaining Python MCPs were identified from their live process trees asworkspace-mcpandmcp-atlassian; each runtime used anuvx -> uv -> wrapper -> python -> pythonchain. The Desktop log simultaneously showed app-server saturation at six in-flight requests, repeatedthread/readhydration expiry, and even criticalturn/steerrequests expiring after 30 seconds. Disabling only those two MCPs and reducing subagent concurrency from 128 to 6 left multi-agent enabled, removed the exact ten-process current baseline, and produced zero respawns in the follow-up check.Per
docs/contributing.md, I will not open an unsolicited upstream PR. I am preparing a focused, tested branch implementing only this first-stage allowlist so the design can be evaluated concretely.@openai/codex-core-agent-team: could a maintainer confirm whether this stage boundary is aligned and, if so, explicitly invite the contribution? The proposed PR will stay under the repository’s size guidance and include integration coverage for omitted/empty/populated policy, unknown-name failure, zero disallowed PID startup, and persisted resume behavior.
@openai/codex-core-agent-team I implemented the proposed first-stage mitigation as a tested draft review artifact: https://github.com/NgoQuocViet2001/codex/pull/1
The patch adds a V1/V2
spawn_agent.mcp_allowcontract with three explicit states (snapshot current effective set / deny all / exact validated subset), applies it after all MCP contributions are merged but before connections start, and persists the exact grant across cold resume. A PID-level Windows test verifies thatmcp_allow: []does not launch another stdio MCP process.I did not open an upstream PR because the contribution policy says external PRs are invitation-only. If this direction fits the team's architecture, an invitation would let me retarget the same rebased and tested commit to
openai/codexwithout rework.Follow-up correction after a deeper state/runtime inspection: the Desktop "Active subagents" counter is not a trustworthy concurrency measurement.
In a separate affected task on the same Desktop build, the UI showed 273 Active + 13 Done. A read-only inspection found exactly 286 persisted descendants, and all 286
thread_spawn_edgesrows were stillopen. At the same time, the parent reported only 1 executing child, and the Windows process tree contained 9node_repl.exechildren rather than hundreds of agent runtimes.Detailed evidence and the proposed lifecycle/reconciliation invariant are here: #38364 (comment https://github.com/openai/codex/issues/38364#issuecomment-5289195013).
Therefore, the original incident's “128 active” UI value should be read as 128 entries classified Active by the panel, not as proof that 128 model threads were simultaneously executing. I do not want the capability-broker request to rely on an incorrect concurrency claim.
The MCP resource incident itself remains independently verified:
So there are two related but distinct defects:
This distinction also suggests a useful diagnostic contract: expose separate counts for
executing,resident, andhistorical/open-edge, plus MCP runtimes owned by each execution lease.