openai / openai/codex

Subagents need an MCP capability broker: parent allowlists, zero-start by default, bounded pooling, and deterministic teardown

Open
#38,353 5 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

app app-server enhancement mcp performance subagent windows-os
Dominant language
Rust
Stars
125k
Forks
19.5k
PR merge metrics
PR metrics pending

Description

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 = true
  • max_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:

  1. which MCPs a child is allowed to use;
  2. whether any process must start at child creation;
  3. how first-run/catalog discovery avoids an N-child fan-out;
  4. how live instances are bounded and reused where safe;
  5. who owns each process and when its lease ends;
  6. 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
  1. Extend the lazy-start work from #38217 so the tool catalog is not coupled to a per-child live connection, including cache-miss coalescing.
  2. Add a per-agent MCP capability manifest to the spawn/session configuration.
  3. Put process creation behind an app-server broker with per-config single-flight and configurable pool limits.
  4. Reuse the explicit shutdown/process-handle machinery from #19753 for lease release.
  5. 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:

  1. 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.
  2. 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.
  3. Explicit allowlist: only one designated child is allowed to use server A. Other children must neither see its callable tools nor receive its credentials.
  4. First tool call: the designated child calls one MCP tool. Exactly one runtime (or the configured bounded count) starts.
  5. Completion/cancel/error: after that child completes, is cancelled, or crashes, the runtime returns to baseline within the teardown/idle timeout.
  6. Resume: a later follow-up lazily reacquires a healthy runtime; historical thread viewing alone starts none.
  7. Backpressure: setting a high subagent limit cannot exceed the separate MCP runtime budget, and parent cancellation/steering remains responsive.
  8. 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:

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Begin with the lazy-start work in #38217 and the shutdown/process-handle machinery from #19753; the issue names no repository files. Trace the app-server subagent and MCP lifecycle, then build the proposed PID-recording stdio fixture. Done means the acceptance tests cover allowlists, single-flight discovery, bounded runtimes, lease release, backpressure, resume, and Windows process-tree teardown.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
backend, devtools, distributed-systems, testing-qa
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.