Skip to content

MCP: local servers are spawned per session and never released when a session ends #51249

Open

Description

@4Liberty

Summary

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

  1. Configure 3-5 local MCP servers.
  2. Use the app continuously for a few hours, opening many sessions (agent-style traffic: dozens of
    sessions per hour, each connecting its MCP tools).
  3. 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.

Activity

  1. dajiaohuang commented on Sep 25, 2026

    @dajiaohuang

    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.

  2. ggbdpq commented on Oct 2, 2026

    @ggbdpq

    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.

  3. ggbdpq commented on Oct 2, 2026

    @ggbdpq

    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.

  4. holny commented on Oct 5, 2026

    @holny
    Contributor

    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

  5. GaganDeep-007 commented on Oct 14, 2026

    @GaganDeep-007

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions