summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorryanwyler <[email protected]>2026-03-01 05:38:17 -0700
committerGitHub <[email protected]>2026-03-01 18:08:17 +0530
commitc4c0b23bff52878014007e53de7657a59df95915 (patch)
tree8a042db5a1a94be12d8e923f418642789af889a3
parent38704acacddad821d157d9a2c093e3751e016f53 (diff)
downloadopencode-c4c0b23bff52878014007e53de7657a59df95915.tar.gz
opencode-c4c0b23bff52878014007e53de7657a59df95915.zip
fix: kill orphaned MCP child processes and expose OPENCODE_PID on shu… (#15516)
-rw-r--r--packages/app/script/e2e-local.ts1
-rw-r--r--packages/opencode/src/index.ts1
-rw-r--r--packages/opencode/src/mcp/index.ts37
3 files changed, 39 insertions, 0 deletions
diff --git a/packages/app/script/e2e-local.ts b/packages/app/script/e2e-local.ts
index 112e2bc60..9a83411b1 100644
--- a/packages/app/script/e2e-local.ts
+++ b/packages/app/script/e2e-local.ts
@@ -145,6 +145,7 @@ try {
Object.assign(process.env, serverEnv)
process.env.AGENT = "1"
process.env.OPENCODE = "1"
+ process.env.OPENCODE_PID = String(process.pid)
const log = await import("../../opencode/src/util/log")
const install = await import("../../opencode/src/installation")
diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts
index 9af79278c..35b42dce7 100644
--- a/packages/opencode/src/index.ts
+++ b/packages/opencode/src/index.ts
@@ -76,6 +76,7 @@ let cli = yargs(hideBin(process.argv))
process.env.AGENT = "1"
process.env.OPENCODE = "1"
+ process.env.OPENCODE_PID = String(process.pid)
Log.Default.info("opencode", {
version: Installation.VERSION,
diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts
index 3c29fe03d..0dca27d65 100644
--- a/packages/opencode/src/mcp/index.ts
+++ b/packages/opencode/src/mcp/index.ts
@@ -160,6 +160,28 @@ export namespace MCP {
return typeof entry === "object" && entry !== null && "type" in entry
}
+ async function descendants(pid: number): Promise<number[]> {
+ if (process.platform === "win32") return []
+ const pids: number[] = []
+ const queue = [pid]
+ while (queue.length > 0) {
+ const current = queue.shift()!
+ const proc = Bun.spawn(["pgrep", "-P", String(current)], { stdout: "pipe", stderr: "pipe" })
+ const [code, out] = await Promise.all([proc.exited, new Response(proc.stdout).text()]).catch(
+ () => [-1, ""] as const,
+ )
+ if (code !== 0) continue
+ for (const tok of out.trim().split(/\s+/)) {
+ const cpid = parseInt(tok, 10)
+ if (!isNaN(cpid) && pids.indexOf(cpid) === -1) {
+ pids.push(cpid)
+ queue.push(cpid)
+ }
+ }
+ }
+ return pids
+ }
+
const state = Instance.state(
async () => {
const cfg = await Config.get()
@@ -196,6 +218,21 @@ export namespace MCP {
}
},
async (state) => {
+ // The MCP SDK only signals the direct child process on close.
+ // Servers like chrome-devtools-mcp spawn grandchild processes
+ // (e.g. Chrome) that the SDK never reaches, leaving them orphaned.
+ // Kill the full descendant tree first so the server exits promptly
+ // and no processes are left behind.
+ for (const client of Object.values(state.clients)) {
+ const pid = (client.transport as any)?.pid
+ if (typeof pid !== "number") continue
+ for (const dpid of await descendants(pid)) {
+ try {
+ process.kill(dpid, "SIGTERM")
+ } catch {}
+ }
+ }
+
await Promise.all(
Object.values(state.clients).map((client) =>
client.close().catch((error) => {