You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
MCP: local servers are spawned per session and never released when a session ends #51249
The background service starts a fresh local MCP server set per session/directory and does not
release it when that session ends. On a host with several local stdio servers configured, the MCP
process count grows monotonically for the lifetime of the service and is only cleared by a service
restart.
Environment
OpenCode desktop v2.0.15, Windows 11, shared background service
Global config with several local stdio MCP servers (one Node wrapper, two uvx/Python servers, one
prepackaged executable) and two remote servers
Steps
Configure 3-5 local MCP servers.
Use the app continuously for a few hours, opening many sessions (agent-style traffic: dozens of
sessions per hour, each connecting its MCP tools).
Count the spawned MCP server processes over time.
Actual
Each session/directory spawns its own set; nothing from an ended session exits.
Measured ~46 sets (~230 live processes, about five local servers per set) after roughly two hours
of continuous use, and ~121 sets (~600 processes) across a longer service lifetime earlier the
same day. The count only returned to zero after the service restarted.
Remote MCP servers show no local processes, as expected; the growth is entirely local stdio
servers.
Expected
MCP servers should be closed when their session ends, or shared per server instead of spawned per
session, so the live count is bounded by concurrent sessions rather than by sessions opened over
the service's lifetime.
Impact
Hundreds of idle processes retain memory and scheduler pressure on a busy host, slowing unrelated
work. Operators currently work around it with an external reaper or periodic service restarts.
The current MCP service caches clients per project directory and closes their transports when that directory instance is disposed. In this reproduction, do the project directories remain open after sessions end, and should local servers be shared until directory disposal or stopped after each session? That lifecycle boundary determines whether a fix should preserve the current per-directory design or change it.
Taking this one. Plan: first confirm the actual lifecycle boundary dajiaohuang outlined — whether directory instances are disposed in the shared background service while their cached MCP clients keep processes alive — then pick the smallest fix that bounds live local server processes by concurrent sessions rather than session history. Will credit the analysis here and follow up with a PR.
Update: after tracing the code I'm stepping back from this one — the analysis confirms the boundary question @dajiaohuang raised is exactly the blocker.
What the code does today (dev):
MCP clients (and their stdio processes) are cached per project directory in the MCP service's InstanceState, and the finalizer that kills the process tree is complete and correct (packages/opencode/src/mcp/index.ts, Effect.addFinalizer in the state init).
The directory-keyed instance cache in InstanceStore (packages/opencode/src/project/instance-store.ts) is an unbounded Map with no idle eviction; disposal only happens via the explicit dispose APIs or the per-request markInstanceForDisposal used by a couple of endpoints.
Session end triggers no instance disposal, so a directory's MCP set intentionally lives until the instance is disposed — "shared per directory" is the current design.
So the leak isn't inside the MCP module; it's that instance lifetime is unbounded, and fixing it well means choosing the eviction boundary (idle TTL in InstanceStore vs client-driven dispose), which affects every per-instance state, not just MCP. That's a maintainers-level design call I shouldn't make unilaterally, so I'm releasing my claim. The trace above should be reusable for whoever picks the design decision up.
I'll take this. The lifecycle gap is in the session-scoped MCP instance teardown: clients are cached per project directory inside the MCP service, and the cache cleanup only runs when the whole directory instance disposes — an individual session ending never touches it. On the shared background service that means every visited directory accumulates its own set of stdio servers for the life of the process.
Plan: trace the MCP client cache against the session/directory lifecycle to pin the exact ownership boundary, then wire client release into the path that already fires when a session closes. Covered by a test that opens two sessions in different directories, closes one, and asserts its stdio servers are gone.
Concrete Linux impact case: on an 11GB Arch Linux machine, 3 concurrent opencode TUI instances each spawned their own local MCP servers (playwright, firecrawl, postgres, plus billion-context plugin). That's ~15 node processes at 90-115MB each, plus 3 opencode binaries at ~900MB RSS each. Total memory demand exceeded physical RAM, driving swap to 8.2GB/23GB. The system froze under swap thrashing (whole desktop unresponsive for minutes), and two opencode instances then crashed with SIGABRT (see #55459).
The MCP process fan-out per instance is the dominant memory multiplier here. Lazy spawn (#53028) or shared MCP server pools would prevent this class of freeze on memory-constrained hosts.
Activity
dajiaohuang commentedon Sep 25, 2026
The current MCP service caches clients per project directory and closes their transports when that directory instance is disposed. In this reproduction, do the project directories remain open after sessions end, and should local servers be shared until directory disposal or stopped after each session? That lifecycle boundary determines whether a fix should preserve the current per-directory design or change it.
ggbdpq commentedon Oct 2, 2026
Taking this one. Plan: first confirm the actual lifecycle boundary dajiaohuang outlined — whether directory instances are disposed in the shared background service while their cached MCP clients keep processes alive — then pick the smallest fix that bounds live local server processes by concurrent sessions rather than session history. Will credit the analysis here and follow up with a PR.
ggbdpq commentedon Oct 2, 2026
Update: after tracing the code I'm stepping back from this one — the analysis confirms the boundary question @dajiaohuang raised is exactly the blocker.
What the code does today (
dev):InstanceState, and the finalizer that kills the process tree is complete and correct (packages/opencode/src/mcp/index.ts,Effect.addFinalizerin the state init).InstanceStore(packages/opencode/src/project/instance-store.ts) is an unboundedMapwith no idle eviction; disposal only happens via the explicit dispose APIs or the per-requestmarkInstanceForDisposalused by a couple of endpoints.So the leak isn't inside the MCP module; it's that instance lifetime is unbounded, and fixing it well means choosing the eviction boundary (idle TTL in
InstanceStorevs client-driven dispose), which affects every per-instance state, not just MCP. That's a maintainers-level design call I shouldn't make unilaterally, so I'm releasing my claim. The trace above should be reusable for whoever picks the design decision up.holny commentedon Oct 5, 2026
I'll take this. The lifecycle gap is in the session-scoped MCP instance teardown: clients are cached per project directory inside the MCP service, and the cache cleanup only runs when the whole directory instance disposes — an individual session ending never touches it. On the shared background service that means every visited directory accumulates its own set of stdio servers for the life of the process.
Plan: trace the MCP client cache against the session/directory lifecycle to pin the exact ownership boundary, then wire client release into the path that already fires when a session closes. Covered by a test that opens two sessions in different directories, closes one, and asserts its stdio servers are gone.
Fixes #51249
GaganDeep-007 commentedon Oct 14, 2026
Concrete Linux impact case: on an 11GB Arch Linux machine, 3 concurrent opencode TUI instances each spawned their own local MCP servers (playwright, firecrawl, postgres, plus billion-context plugin). That's ~15 node processes at 90-115MB each, plus 3 opencode binaries at ~900MB RSS each. Total memory demand exceeded physical RAM, driving swap to 8.2GB/23GB. The system froze under swap thrashing (whole desktop unresponsive for minutes), and two opencode instances then crashed with SIGABRT (see #55459).
The MCP process fan-out per instance is the dominant memory multiplier here. Lazy spawn (#53028) or shared MCP server pools would prevent this class of freeze on memory-constrained hosts.