Skip to content

fix(core): stop MCP servers in parallel on shutdown - #58351

Merged
rekram1-node merged 3 commits into
devfrom
mcp-leak-check
Oct 23, 2026
Merged

rekram1-node merged 3 commits into
devfrom
mcp-leak-check

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #50758

MCP servers were stopped one at a time on shutdown. Each local server that ignores its stdin closing costs 2 to 4 seconds (stdin close grace, then SIGTERM, then SIGKILL). opencode service restart force-kills the server after about 5 seconds, so with a couple of those servers the kill lands mid-shutdown and the servers that haven't been stopped yet are left running under PID 1.

+servers = Scope.fork(root, "parallel")
 startServer()
-  scope = Scope.fork(root)
+  scope = Scope.fork(servers)

Shutdown now stops all servers at once, so it takes as long as the slowest one. Disconnect, reconnect and config reload still stop only the server they target, as before.

With four test servers (a wrapper plus a child each, two ignoring stdin close and one also ignoring SIGTERM), shutdown went from about 8.1s to 4.2s, and a SIGTERM followed by SIGKILL at 5s no longer leaves anything behind.


Added by the opencode bot (AI-generated).

Related: #50363

Server scopes closed one at a time, and each misbehaving stdio server can take up to 4s to stop. With two or more, shutdown outran the service restart's 5s SIGKILL deadline and the servers not yet stopped were orphaned. Fork server scopes from a parallel child scope so shutdown takes as long as the slowest server.

Fixes #50758

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix works. With 4 local MCP servers that keep running after stdin closes (one also ignoring SIGTERM), opencode serve on the base was still shutting down when SIGKILL hit at 5s, and 3 MCP processes were left orphaned. With this PR the server exits in about 4.2s and leaves nothing behind. The PR's new test fails on the base and passes here. The core MCP tests pass apart from one failure that also happens on the base, and typecheck:tsc passes.

The public API is unchanged.

The parallel scope only changes how all servers are stopped together at shutdown. Disconnect, reconnect and reload still close a single server's scope, so their behaviour stays the same. Registration scopes now close before the server scopes rather than after, which doesn't matter at shutdown.

One thing worth changing, in the tests (see inline): the new test is large for a three-line fix and can be dropped, and the test file also picks up unrelated formatting changes to two existing tests.

Related: #50363

Comment thread packages/core/test/mcp.test.ts Outdated
}),
)

shutdownIt.live("stops MCP servers concurrently when its layer closes", () =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds about 90 lines (a hand-built JSON-RPC responder, a fake process handle, and stdin parsing) to check a three-line scope change. It's hard to follow and depends on the stop path's grace timer to fail on the base. I'd drop it to keep the change small; if you want to keep it, please cut it down to the bare minimum. The formatting-only changes to the two elicitation tests above aren't related to this fix and can be reverted too.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both earlier points are fixed: the added test is gone and the unrelated formatting changes are reverted, so this is now just the scope change. I ran the shutdown check again on the new head: serve exits in about 4.2s after SIGTERM and leaves no MCP processes behind. On the base, the server is killed at 5s and 3 are left orphaned. The public API is unchanged. Nothing further from me.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

server: stdio MCP child processes not terminated on service restart / config reload

1 participant