1010 */
1111
1212import { Command } from 'commander' ;
13- import { spawn } from 'child_process' ;
1413import { join , dirname } from 'path' ;
1514import { existsSync } from 'fs' ;
16- import { fileURLToPath } from 'url' ;
15+ import { fileURLToPath , pathToFileURL } from 'url' ;
1716import { findPackageRoot } from '../../init/find-package-root.js' ;
1817
1918/**
@@ -25,14 +24,11 @@ export function createMcpCommand(): Command {
2524 . option ( '--http <port>' , 'Also start HTTP server for AG-UI/A2A protocols' , '0' )
2625 . option ( '--verbose' , 'Enable verbose logging' )
2726 . action ( async ( options ) => {
28- // Set up environment
29- const env : Record < string , string > = {
30- ...process . env as Record < string , string > ,
31- AQE_HTTP_PORT : options . http || '0' ,
32- } ;
33-
27+ // Configure the server via env BEFORE importing the entry — entry.ts
28+ // reads these at startup.
29+ process . env . AQE_HTTP_PORT = options . http || '0' ;
3430 if ( options . verbose ) {
35- env . AQE_VERBOSE = 'true' ;
31+ process . env . AQE_VERBOSE = 'true' ;
3632 }
3733
3834 // Find the MCP entry point
@@ -44,32 +40,31 @@ export function createMcpCommand(): Command {
4440 process . exit ( 1 ) ;
4541 }
4642
47- // Determine how to run the entry point
48- const isBundle = entryPath . endsWith ( '.js' ) ;
49- const command = ' node' ;
50- const args = isBundle
51- ? [ entryPath ]
52- : [ '--import' , 'tsx' , entryPath ] ;
53-
54- // Spawn the MCP server, inheriting stdio for JSON-RPC communication
55- const child = spawn ( command , args , {
56- env ,
57- stdio : 'inherit' ,
58- cwd : process . cwd ( ) ,
59- } ) ;
60-
61- child . on ( 'error' , ( error ) => {
62- console . error ( 'Failed to start MCP server:' , error . message ) ;
43+ // Run the MCP server IN-PROCESS instead of spawning a child (issue #528).
44+ //
45+ // The old path spawned ` node <entry>` with stdio:'inherit'. With a piped
46+ // stdin (any programmatic MCP client), the grandchild saw `stdin-eof`
47+ // immediately after answering `initialize` and the Issue #513 stdin-EOF
48+ // guard in entry.ts shut it down before it could serve `tools/list`.
49+ // Importing the entry runs its top-level `main()`, which starts the
50+ // server on THIS process's real stdio and installs its own
51+ // signal/stdin shutdown handlers. The dynamic import resolves as soon as
52+ // module evaluation kicks off `main()`; the server then keeps the
53+ // process alive on stdin. This matches the working `aqe-mcp` bin, which
54+ // runs the bundle directly.
55+ try {
56+ await import ( pathToFileURL ( entryPath ) . href ) ;
57+ } catch ( error ) {
58+ console . error ( 'Failed to start MCP server:' , ( error as Error ) . message ) ;
6359 process . exit ( 1 ) ;
64- } ) ;
65-
66- child . on ( 'exit' , ( code ) => {
67- process . exit ( code ?? 0 ) ;
68- } ) ;
60+ }
6961
70- // Forward signals to child
71- process . on ( 'SIGINT' , ( ) => child . kill ( 'SIGINT' ) ) ;
72- process . on ( 'SIGTERM' , ( ) => child . kill ( 'SIGTERM' ) ) ;
62+ // entry.ts kicks off `main()` fire-and-forget, so the import above
63+ // resolves while the server's async init is still in flight. Keep this
64+ // action pending forever so the CLI wrapper doesn't return (and exit)
65+ // out from under the server. entry.ts owns shutdown — its signal and
66+ // stdin-EOF handlers call process.exit when the client disconnects.
67+ await new Promise < never > ( ( ) => { /* server owns the process lifecycle */ } ) ;
7368 } ) ;
7469
7570 return cmd ;
0 commit comments