diff options
| author | ryanwyler <[email protected]> | 2026-03-01 05:38:17 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-03-01 18:08:17 +0530 |
| commit | c4c0b23bff52878014007e53de7657a59df95915 (patch) | |
| tree | 8a042db5a1a94be12d8e923f418642789af889a3 | |
| parent | 38704acacddad821d157d9a2c093e3751e016f53 (diff) | |
| download | opencode-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.ts | 1 | ||||
| -rw-r--r-- | packages/opencode/src/index.ts | 1 | ||||
| -rw-r--r-- | packages/opencode/src/mcp/index.ts | 37 |
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) => { |
