Repository navigation
Description
Description
On Windows, local stdio MCP servers started via npx accumulate orphaned node.exe processes across opencode TUI/service restarts. Each restart leaves the previous generation's MCP process tree alive; nothing reaps them.
Root cause: the descendant-kill cleanup in packages/opencode/src/mcp/index.ts (descendants()) returns an empty array on win32, and the pgrep-based cleanup merged in #15516 explicitly skips Windows because pgrep is unavailable. client.close() only closes the MCP protocol connection, not the OS process tree.
Plugins
None
OpenCode version
2.2.0 (latest)
Steps to reproduce
- Configure a local MCP server launched via npx (e.g.
npx -y @agentmemory/mcp). - Start the opencode TUI and restart the service a few times (or let instances be disposed/recreated).
- List MCP node processes:
Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Select ProcessId, CreationDate, CommandLine. - Observe multiple generations (an npx wrapper + server per configured MCP, per generation) instead of one.
Screenshot
Process list after ~1 day of TUI use — 12 node.exe processes across 4 restart generations (07:12, 07:17, 07:41, 07:49 local). Each configured server leaves a pair (npx wrapper + server); only the newest generation should exist:
PID Created (local) Role
29092 07:49:07 npx wrapper (current generation)
37384 07:49:07 npx wrapper (current generation)
46392 07:49:14 @agentmemory/mcp (current generation)
+ 8 stale processes from the 07:12 / 07:17 / 07:41 generations (manually taskkilled)
Operating System
Windows 11 (10.0.26300, x64)
Terminal
Windows Terminal
Additional context:
- Reproduced consistently; each TUI/service restart adds one more generation.
- Workaround: manually
taskkillthe stalenode.exeprocesses (keeping the newest generation alive). - Suggested fix: kill the full process tree on Windows (e.g.
taskkill /T /F /PID <pid>, a Job Object, or Bun's built-in descendant cleanup). - Prior reports on the same root cause, all closed as
not_plannedby the auto-close bot: MCP servers spawned 8-10x per instance on Windows, consuming ~3.5 GB RAM on startup #26336, MCP child-process leak: server never reaps MCP children per (re)connection, leading to unbounded memory growth #52410, Bug: Shared OpenCode host keeps per-directory MCP child processes alive on Windows after switching routed directories/worktrees #21557, local MCP server child processes become orphaned on restart #30073, MCP server processes not cleaned up on exit (orphan processes) + eagerly starts all servers #30123, Local stdio MCP servers leak processes on disconnect / replace / rollback #26714.
Activity
opencodejr commentedon Oct 28, 2026
Thanks for the detailed report and the process listing.
Our reproduction sandbox runs Linux, so we can't test how OpenCode cleans up processes on Windows. A maintainer with a Windows machine will need to confirm this.
Here is what we found in the code, on 2.2.0 and the current
dev(fa1260d):packages/opencode/src/mcp/index.ts,descendants(), the pgrep cleanup) are from v1. In v2, local MCP servers are started bypackages/core/src/mcp/stdio.tsthrough the shared spawner inpackages/util/src/cross-spawn-spawner.ts.taskkill /pid <pid> /T /Fon Windows, which should kill the whole tree.It would help to know whether the extra generations also appear after a clean quit of the TUI and service, or only after restarts that might be forced or caused by a crash (for example the watchdog restart in #52049).
Related issues:
konopka commentedon Oct 28, 2026
Both — the extra generations appear without any TUI or service restart at all; they accumulate through instance creation/disposal within the long-running shared service.
Evidence, v2.2.0, Windows 11:
The shared service (
opencode.exe serve --service) ran continuously since Oct 27 16:34 (the TUI since Oct 26 23:03), yet the MCP process generations spawned at 07:12 / 07:17 / 07:41 / 07:49 local were all children of that same service process. So no crash or forced restart is involved — the stale trees are a byproduct of in-service instance/MCP re-creation.Minimal repro with no restart of anything:
node testorphan.mjs, a minimal stdio MCP server that stays alive while stdin is open), defined in a project-levelopencode.json.opencode run "..."in that dir. It attached to the already-running shared service and created a directory instance, which spawned 5 node processes: the throwaway server plus 4 from global MCP servers that were re-spawned for the new instance (npx wrapper + server per MCP).opencode runexited cleanly and the instance was disposed. All 5 child processes were still alive 30+ seconds later — nothing reaped them.So the missing reap is in the per-instance MCP dispose path on Windows, and it fires during normal use — no shutdown of any kind required. (Untested: whether a full clean shutdown of the service process itself reaps its own children; given the accumulation above, that's moot in practice.)
Process topology at the time (abridged):
Side observation (may be a separate issue): the new instance re-spawned the global MCP servers even though the project-level config set
enabled: falsefor them, while a project-only server in the same file was picked up. The config precedence docs say project configs override global ones for conflicting keys, but don't specify merge granularity for nestedmcpentries — unclear whether this is a docs gap or a merge-order bug.konopka commentedon Oct 28, 2026
Update: reaping works on clean paths — the accumulation is retention, not a missing kill. (v2.2.0, Windows 11)
Answer to your question: the extra generations appear in neither case. They accumulate during normal use while the service runs, and the kill path itself is functional:
1. Reaping verified working. I pointed a test dir at a 30 s
experimental.location_idle_timeoutwith a throwaway local MCP plus two global npx-based MCPs. On every idle-sweep eviction, the full MCP process tree (cmd → npx → cmd → server) was killed — verified with a throwaway server that ignores stdin-EOF and outlives its wrapper. Same result via manual eviction (DELETE /api/debug/location).2. Why stale generations accumulate anyway — two multiplicative factors:
location_idle_timeout), refreshed by session activity, and occupied locations are never evicted (packages/core/src/location-activity.ts). The "generations" in my report were retained locations each holding a live MCP set — not unreaped orphans.3. Second bug found while investigating: an evicted location was re-requested 38–500 ms after every eviction, in an exact 2-minute cycle, surviving manual eviction and deletion of all its headless sessions. Each cycle fully re-boots the location (watchers, config, plugins, full MCP set) and is reaped next cycle — no unbounded growth, but permanent process churn. The only connected client was the TUI, so the stale reference likely sits in TUI session retention (or a zombie job from a deleted session). Logs available if useful.
4. Related observation: two locations booting the same npx-based MCP concurrently — the second failed with
EBUSY(npx cachecopyfilelock). Per-location duplication can break connectivity, not just waste RAM.So the original "nothing reaps them" premise doesn't hold on v2.2.0. The true remaining gaps: (a) crash/force-kill of the service still orphans the tree (no Job Object /
KILL_ON_JOB_CLOSE), (b) retention policy × per-location duplication is what produces the accumulation, (c) the Windows descendant-termination test is skipped and taskkill failures in the spawner are swallowed.xjstar commentedon Nov 4, 2026
Windows 11 field data: 22 of 29 locations are opencode-generated temp dirs
Adding measurements from a Windows 11 / opencode 2.2.0 host, since the sandbox can't cover this platform. They corroborate the retention + churn analysis in the update, and add one data point I haven't seen reported: the locations driving the accumulation are opencode's own temp directories, not user project dirs.
Environment
@opencode/cli), installed 2026-10-27opencode.exe serve --service) had been running continuously for 6 dayscodegraph(node),chrome-devtools-mcp(npx)Symptom
95
node.exeprocesses / 8.0 GB, all parented to one live service process. Nothing was crashed or restarted — this accumulated during normal use, matching your "retention, not a missing kill" conclusion.Breakdown by MCP:
Finding 1 — the locations are temp dirs, and they outnumber real projects 3:1
Across all
mcp connectedlog lines, 29 distinct locations. Only 7 are real working directories (a few repos plus a home dir). The other 22 are opencode-generated temp dirs under%LOCALAPPDATA%\Temp:These 22 temp locations alone produced 87 MCP connections (15 of them × 5 connections, 6 of them × 2). Whatever creates these temp dirs is booting a full MCP set each time.
I could not determine what each prefix is for — they're not literal strings in the binary, so they're assembled at runtime. Worth a look from someone who knows the code: a temp scratch dir arguably shouldn't be treated as a project location at all.
Finding 2 — reproduced the re-request cycle, ~20–25 s period
Sampling new
chrome-devtoolsprocess creations over 180 s:Each event is a pair ~5 s apart (the MCP's own wrapper + server). The
oce-*series is the cleanest illustration — a brand-new location every ~25 s, each booting the full MCP set:My period is ~20–25 s rather than the 2 min you measured, and it never settled even after clearing all
oc*temp dirs. Only connected client was the TUI.Finding 3 — npx triples the per-location cost
Switching from
npx -y <pkg>@latestto invoking the already-installed package directly cut each MCP instance from 3 processes to 1:npx -y chrome-devtools-mcp@latestnode(npx-cli)+node(_npx/…)+node(.bin/…)node <global>/build/src/bin/chrome-devtools-mcp.jsSo the per-location multiplier is ~233 MB with npx vs ~107 MB direct — on top of the
EBUSYnpx-cache contention you noted when two locations start the same npx MCP concurrently. Worth calling out in docs as a recommendation even before the retention bug is fixed.Finding 4 —
location_idle_timeoutformat is undocumented and easy to get wrongThe schema accepts
string | false, and the description saysdefault: "60 minutes", but neither the docs nor the schema state the accepted format. I tested against the running server and readconfiguration normalization diagnosticwarnings from the log:"15 minutes""15m"kind=invalid … skipped malformed recognized value"PT15M"900000Only the prose-style
"15 minutes"works. Since the failure mode is a silent skip (a WARN in the log, config otherwise loads fine), it's easy to think the setting took effect when it didn't. A schemapatternor a docs line would help.Confirmed after the fix: last
configuration normalization diagnosticwarning was at 05:56:47Z, config rewritten at 05:56:59Z, and no new warning since — so"15 minutes"is genuinely applied, not silently skipped.What's still unexplained on my end
Even after (a) pointing chrome-devtools at the direct binary, (b) setting
location_idle_timeoutto"15 minutes", and (c) clearing alloc*temp dirs, I still see a slow trickle of new locations. Consistent with your gap (b): retention policy is the driver, and something keeps minting new temp locations. If it's useful I can attach the fullmcp connectedlog slice with timestamps and directory names.Happy to run any instrumentation you'd like on Windows — that's the platform the sandbox can't reach.
Darek3399 commentedon Nov 12, 2026
Yeah,
client.close()on Windows is basically a polite suggestion whennpxis involved. Becausenpxspawns throughcmd.exe/npx.cmdbefore launching the actualnode.exeworker, closing the stdio transport or killing the top wrapper PID leaves the grandchildren running orphaned. The only bulletproof way on Windows is assigning the spawned process to a Job Object withJOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE.Until OpenCode wires up Job Objects internally, a clean workaround is hosting your local stdio servers in MCP-Bridge and connecting OpenCode to Bridge's local HTTP endpoint. It already runs every managed server inside a Windows Job Object and checks for leftover port/PID locks, so restarting the TUI or toggling servers off leaves zero stray
node.exeprocesses in Task Manager.