@@ -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