Skip to content

fix(mcp): release MCP servers for abandoned directories - #52946

Closed
abhisheksharma611 wants to merge 1 commit into
anomalyco:devfrom
abhisheksharma611:fix/mcp-release-abandoned-directories
Closed

abhisheksharma611 wants to merge 1 commit into
anomalyco:devfrom
abhisheksharma611:fix/mcp-release-abandoned-directories

Conversation

@abhisheksharma611

Copy link
Copy Markdown

Problem

A long-running opencode serve accumulates one complete set of MCP server processes per directory it has ever served, and never gives them back.

Measured on Windows 11, OpenCode 2.0.21, one session with 7 configured projects:

concurrent MCP fleets 13
node.exe 65
python 26
cmd.exe 67
still growing after 4 hours

Every directory touched — each project, and each chat/session directory — gets its own fleet, because MCP state is built per directory and each build spawns the configured servers.

Root cause

Per-directory state is released in two different places, and MCP is only in one of them:

  • packages/core/src/location-services.ts:109 builds LocationServiceMap with { idleTimeToLive: "60 minutes" }, so watchers, filesystem and the rest of a location's services are released once it goes idle.
  • MCP state is not in that layer. It is built at packages/opencode/src/mcp/index.ts:492 through InstanceState.make, whose cache was created with capacity: Number.POSITIVE_INFINITY and no expiry (packages/opencode/src/effect/instance-state.ts:31). InstanceState.invalidate exists but has no caller.

The only release paths were an explicit POST /instance/dispose, POST /global/dispose, or process exit. In normal server operation none of those fire, so every scope — and every MCP server process under it — stayed alive for the lifetime of the process.

Fix

Give InstanceState.make an optional capacity (default unchanged, so no other caller changes behaviour) and cap the MCP state at 8 directories. Least-recently-used directories then release their scope, which runs the disposal that already terminates their servers.

Verification

  • test/effect/instance-state.test.ts gains a case covering LRU release at capacity. Confirmed red before the fix (0 pass, 1 fail) and green after.
  • bun test test/server/ test/project/ test/mcp/ test/effect/ — 512 pass, 25 skip, 0 fail (537 tests, 74 files).
  • tsc --noEmit — no new errors. The 11 remaining errors are pre-existing in packages/tui/src/component/dialog-move-session.tsx; verified identical on a stashed tree.
  • oxlint could not be run: tsgolint.exe is blocked by a Windows Application Control policy on this machine (os error 4551). Worth a lint pass in CI.

Notes for review

  • The 8 is a policy choice, not a derived number. It is deliberately generous — more than the number of directories a normal session keeps warm — and exists only to stop unbounded growth. Happy to make it configurable, or to derive it from a limit the project prefers, if that fits the architecture better.
  • A closer fix may be to make MCP state part of the location layer, so it is released on the same 60-minute idle schedule as everything else instead of on an LRU bound. That is a larger structural change and I did not want to smuggle it in here.
  • I looked at descendants() in mcp/index.ts:420, which returns [] on win32 and so skips the process-tree walk during disposal. I measured that this does not leak in practice on Windows: a Bun-spawned or child_process.spawn-spawned grandchild dies with its parent because the runtime holds the tree in a job object. So I did not change it — flagging in case that assumption is wrong somewhere.

MCP state is per directory, and building it spawns the configured MCP
servers as child processes. Every other per-directory resource lives in
`locationServices`, which LayerMap releases after 60 minutes idle, but MCP
state is built through `InstanceState`, whose ScopedCache was created with
`capacity: Number.POSITIVE_INFINITY` and no expiry, and
`InstanceState.invalidate` has no caller. Nothing released those scopes
short of an explicit dispose endpoint or process exit.

A server session that visits N directories therefore kept N sets of MCP
server processes alive for the lifetime of the process. On Windows 11 a
single session reached 13 concurrent sets - 65 node, 26 python and 67 cmd
processes - and was still growing after four hours.

Give `InstanceState.make` an optional capacity (default unchanged) and cap
the MCP state, so least-recently-used directories release their scope,
which runs the disposal that already terminates their servers.
@github-actions github-actions Bot added the needs:compliance This means the issue will auto-close after 2 hours. label Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • PR description is missing required template sections. Please use the PR template.

Please edit this PR description to address the above within 72 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@abhisheksharma611

Copy link
Copy Markdown
Author

Closing in the comment above.

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

Labels

needs:compliance This means the issue will auto-close after 2 hours. needs:issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant