|
|
@@ -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) => {
|