@@ -288,6 +288,7 @@ const makeProxyCreators = (
288288 { pluginHost } : GlobalOptions = getOptions ( )
289289) : McpToolCreator [ ] => handle . tools . map ( ( tool ) : McpToolCreator => ( ) => {
290290 const name = tool . name ;
291+ const invokeTimeoutMs = Math . max ( 0 , Number ( pluginHost ?. invokeTimeoutMs ) || 0 ) ;
291292
292293 // Rebuild Zod schema from serialized JSON.
293294 const zodSchemaStrict = jsonSchemaToZod ( tool . inputSchema ) ;
@@ -330,7 +331,7 @@ const makeProxyCreators = (
330331 const response = await awaitIpc (
331332 handle . child ,
332333 isInvokeResult ( requestId ) ,
333- pluginHost . invokeTimeoutMs
334+ invokeTimeoutMs
334335 ) ;
335336
336337 if ( 'ok' in response && response . ok === false ) {
@@ -367,7 +368,6 @@ const makeProxyCreators = (
367368 *
368369 * @param {GlobalOptions } options - Global options.
369370 * @param {AppSession } sessionOptions - Session options.
370- * @returns {Promise<void> } Promise that resolves when the host is stopped or noop.
371371 */
372372const sendToolsHostShutdown = async (
373373 { pluginHost } : GlobalOptions = getOptions ( ) ,
@@ -379,64 +379,94 @@ const sendToolsHostShutdown = async (
379379 return ;
380380 }
381381
382- const gracePeriodMs = ( Number . isInteger ( pluginHost ?. gracePeriodMs ) && pluginHost . gracePeriodMs ) || 0 ;
382+ const gracePeriodMs = Math . max ( 0 , Number ( pluginHost ?. gracePeriodMs ) || 0 ) ;
383383 const fallbackGracePeriodMs = gracePeriodMs + 200 ;
384384
385385 const child = handle . child ;
386386 let resolved = false ;
387387 let forceKillPrimary : NodeJS . Timeout | undefined ;
388- let forceKillFallback : NodeJS . Timeout | undefined ;
388+ let forceKillSecondary : NodeJS . Timeout | undefined ;
389+ let resolveIt : ( ( value : PromiseLike < void > | void ) => void ) | undefined ;
389390
390- await new Promise < void > ( resolve => {
391- const resolveOnce = ( ) => {
392- if ( resolved ) {
393- return ;
394- }
391+ // Attempt exit, disconnect, then remove from activeHostsBySession and finally resolve
392+ const shutdownChild = ( ) => {
393+ if ( resolved ) {
394+ return ;
395+ }
395396
396- resolved = true ;
397- child . off ( 'exit' , resolveOnce ) ;
398- child . off ( 'disconnect' , resolveOnce ) ;
397+ resolved = true ;
398+ child . off ( 'exit' , shutdownChild ) ;
399+ child . off ( 'disconnect' , shutdownChild ) ;
399400
400- if ( forceKillPrimary ) {
401- clearTimeout ( forceKillPrimary ) ;
402- }
401+ if ( forceKillPrimary ) {
402+ clearTimeout ( forceKillPrimary ) ;
403+ }
403404
404- if ( forceKillFallback ) {
405- clearTimeout ( forceKillFallback ) ;
406- }
405+ if ( forceKillSecondary ) {
406+ clearTimeout ( forceKillSecondary ) ;
407+ }
408+
409+ try {
410+ ( handle as any ) . closeStderr ( ) ;
411+ log . info ( 'Tools Host stderr reader closed.' ) ;
412+ } catch ( error ) {
413+ log . error ( `Failed to close Tools Host stderr reader: ${ formatUnknownError ( error ) } ` ) ;
414+ }
407415
408- try {
409- handle . closeStderr ?.( ) ;
410- } catch { }
416+ const confirmHandle = activeHostsBySession . get ( sessionId ) ;
411417
418+ if ( confirmHandle ?. child === child ) {
412419 activeHostsBySession . delete ( sessionId ) ;
413- resolve ( ) ;
414- } ;
420+ }
415421
416- try {
417- send ( child , { t : 'shutdown' , id : makeId ( ) } ) ;
418- } catch { }
422+ resolveIt ?.( ) ;
423+ } ;
419424
420- const shutdownChild = ( ) => {
421- try {
422- if ( ! child ?. killed ) {
423- child . kill ( 'SIGKILL' ) ;
424- }
425- } finally {
426- resolveOnce ( ) ;
425+ // Forced shutdown.
426+ const sigkillChild = ( isSecondaryFallback : boolean = false ) => {
427+ try {
428+ if ( ! child ?. killed ) {
429+ log . warn (
430+ `${
431+ ( resolved && 'Already attempted shutdown.' ) || 'Slow shutdown response.'
432+ } ${
433+ ( isSecondaryFallback && 'Secondary' ) || 'Primary'
434+ } fallback force-killing Tools Host child process.`
435+ ) ;
436+ child . kill ( 'SIGKILL' ) ;
427437 }
428- } ;
438+ } catch ( error ) {
439+ log . error ( `Failed to force-kill Tools Host child process: ${ formatUnknownError ( error ) } ` ) ;
440+ }
441+ } ;
442+
443+ // Start the shutdown process
444+ await new Promise < void > ( resolve => {
445+ resolveIt = resolve ;
446+ // Send a shutdown signal to child. We try/catch in case the process is already dead, and
447+ // since we're still following it up with a graceful shutdown, then force-kill.
448+ try {
449+ send ( child , { t : 'shutdown' , id : makeId ( ) } ) ;
450+ } catch ( error ) {
451+ log . error ( `Failed to send shutdown signal to Tools Host child process: ${ formatUnknownError ( error ) } ` ) ;
452+ }
429453
430- // Primary grace period
431- forceKillPrimary = setTimeout ( shutdownChild , gracePeriodMs ) ;
454+ // Set primary timeout for force shutdown
455+ forceKillPrimary = setTimeout ( ( ) => {
456+ sigkillChild ( ) ;
457+ shutdownChild ( ) ;
458+ } , gracePeriodMs ) ;
432459 forceKillPrimary ?. unref ?.( ) ;
433460
434- // Fallback grace period
435- forceKillFallback = setTimeout ( shutdownChild , fallbackGracePeriodMs ) ;
436- forceKillFallback ?. unref ?.( ) ;
461+ // Set fallback timeout for force shutdown
462+ forceKillSecondary = setTimeout ( ( ) => {
463+ sigkillChild ( true ) ;
464+ } , fallbackGracePeriodMs ) ;
465+ forceKillSecondary ?. unref ?.( ) ;
437466
438- child . once ( 'exit' , resolveOnce ) ;
439- child . once ( 'disconnect' , resolveOnce ) ;
467+ // Set up exit/disconnect handlers to resolve
468+ child . once ( 'exit' , shutdownChild ) ;
469+ child . once ( 'disconnect' , shutdownChild ) ;
440470 } ) ;
441471} ;
442472
0 commit comments