Skip to content

Commit c4c0b23

Browse files
authored
fix: kill orphaned MCP child processes and expose OPENCODE_PID on shu… (#15516)
1 parent 38704ac commit c4c0b23

File tree

3 files changed

+39
-0
lines changed

3 files changed

+39
-0
lines changed

packages/app/script/e2e-local.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ try {
145145
Object.assign(process.env, serverEnv)
146146
process.env.AGENT = "1"
147147
process.env.OPENCODE = "1"
148+
process.env.OPENCODE_PID = String(process.pid)
148149

149150
const log = await import("../../opencode/src/util/log")
150151
const install = await import("../../opencode/src/installation")

packages/opencode/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ let cli = yargs(hideBin(process.argv))
7676

7777
process.env.AGENT = "1"
7878
process.env.OPENCODE = "1"
79+
process.env.OPENCODE_PID = String(process.pid)
7980

8081
Log.Default.info("opencode", {
8182
version: Installation.VERSION,

packages/opencode/src/mcp/index.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,28 @@ export namespace MCP {
160160
return typeof entry === "object" && entry !== null && "type" in entry
161161
}
162162

163+
async function descendants(pid: number): Promise<number[]> {
164+
if (process.platform === "win32") return []
165+
const pids: number[] = []
166+
const queue = [pid]
167+
while (queue.length > 0) {
168+
const current = queue.shift()!
169+
const proc = Bun.spawn(["pgrep", "-P", String(current)], { stdout: "pipe", stderr: "pipe" })
170+
const [code, out] = await Promise.all([proc.exited, new Response(proc.stdout).text()]).catch(
171+
() => [-1, ""] as const,
172+
)
173+
if (code !== 0) continue
174+
for (const tok of out.trim().split(/\s+/)) {
175+
const cpid = parseInt(tok, 10)
176+
if (!isNaN(cpid) && pids.indexOf(cpid) === -1) {
177+
pids.push(cpid)
178+
queue.push(cpid)
179+
}
180+
}
181+
}
182+
return pids
183+
}
184+
163185
const state = Instance.state(
164186
async () => {
165187
const cfg = await Config.get()
@@ -196,6 +218,21 @@ export namespace MCP {
196218
}
197219
},
198220
async (state) => {
221+
// The MCP SDK only signals the direct child process on close.
222+
// Servers like chrome-devtools-mcp spawn grandchild processes
223+
// (e.g. Chrome) that the SDK never reaches, leaving them orphaned.
224+
// Kill the full descendant tree first so the server exits promptly
225+
// and no processes are left behind.
226+
for (const client of Object.values(state.clients)) {
227+
const pid = (client.transport as any)?.pid
228+
if (typeof pid !== "number") continue
229+
for (const dpid of await descendants(pid)) {
230+
try {
231+
process.kill(dpid, "SIGTERM")
232+
} catch {}
233+
}
234+
}
235+
199236
await Promise.all(
200237
Object.values(state.clients).map((client) =>
201238
client.close().catch((error) => {

0 commit comments

Comments
 (0)