@@ -20,6 +20,7 @@ const SERVER_VERSION = '6.2.0';
2020// Environment variable names for documentation
2121const ENV_PROJECT_ROOT = 'CCW_PROJECT_ROOT' ;
2222const ENV_ALLOWED_DIRS = 'CCW_ALLOWED_DIRS' ;
23+ const STDIO_DISCONNECT_ERROR_CODES = new Set ( [ 'EPIPE' , 'ERR_STREAM_DESTROYED' ] ) ;
2324
2425// Default enabled tools (core set - file operations, core memory, and smart search)
2526const DEFAULT_TOOLS : string [ ] = [ 'write_file' , 'edit_file' , 'read_file' , 'read_many_files' , 'read_outline' , 'core_memory' , 'smart_search' ] ;
@@ -67,6 +68,47 @@ function formatToolResult(result: unknown): string {
6768 return String ( result ) ;
6869}
6970
71+ /**
72+ * Detect broken stdio pipes so orphaned MCP processes can terminate cleanly.
73+ */
74+ function isStdioDisconnectError ( error : unknown ) : error is NodeJS . ErrnoException {
75+ if ( error && typeof error === 'object' ) {
76+ const maybeErrnoError = error as NodeJS . ErrnoException ;
77+ if ( typeof maybeErrnoError . code === 'string' && STDIO_DISCONNECT_ERROR_CODES . has ( maybeErrnoError . code ) ) {
78+ return true ;
79+ }
80+ }
81+
82+ return error instanceof Error && / b r o k e n p i p e / i. test ( error . message ) ;
83+ }
84+
85+ /**
86+ * Best-effort logging for teardown paths where stderr may already be gone.
87+ */
88+ function safeStderrWrite ( message : string ) : void {
89+ try {
90+ if ( process . stderr . destroyed || ! process . stderr . writable ) {
91+ return ;
92+ }
93+
94+ process . stderr . write ( `${ message } \n` ) ;
95+ } catch {
96+ // Ignore logging failures while stdio is tearing down.
97+ }
98+ }
99+
100+ function safeLogError ( prefix : string , error : unknown ) : void {
101+ if ( error instanceof Error ) {
102+ safeStderrWrite ( `${ prefix } : ${ error . message } ` ) ;
103+ if ( error . stack ) {
104+ safeStderrWrite ( error . stack ) ;
105+ }
106+ return ;
107+ }
108+
109+ safeStderrWrite ( `${ prefix } : ${ String ( error ) } ` ) ;
110+ }
111+
70112/**
71113 * Create and configure the MCP server
72114 */
@@ -151,28 +193,77 @@ function createServer(): Server {
151193async function main ( ) : Promise < void > {
152194 const server = createServer ( ) ;
153195 const transport = new StdioServerTransport ( ) ;
196+ let shutdownPromise : Promise < void > | null = null ;
197+
198+ const shutdown = ( reason : string , exitCode = 0 , error ? : unknown ) : Promise < void > => {
199+ if ( shutdownPromise ) {
200+ return shutdownPromise ;
201+ }
202+
203+ if ( error && ! isStdioDisconnectError ( error ) ) {
204+ safeLogError ( `[${ SERVER_NAME } ] ${ reason } ` , error ) ;
205+ }
206+
207+ shutdownPromise = ( async ( ) => {
208+ try {
209+ await server . close ( ) ;
210+ } catch ( closeError ) {
211+ if ( ! isStdioDisconnectError ( closeError ) ) {
212+ safeLogError ( `[${ SERVER_NAME } ] Failed to close server` , closeError ) ;
213+ }
214+ }
215+
216+ process . exit ( exitCode ) ;
217+ } ) ( ) ;
218+
219+ return shutdownPromise ;
220+ } ;
221+
222+ const handleStreamClose = ( streamName : string ) => ( ) => {
223+ void shutdown ( `${ streamName } disconnected` ) ;
224+ } ;
225+
226+ const handleStreamError = ( streamName : string ) = > ( error : unknown ) => {
227+ const exitCode = isStdioDisconnectError ( error ) ? 0 : 1 ;
228+ void shutdown ( `${ streamName } stream error` , exitCode , error ) ;
229+ } ;
154230
155231 // Connect server to transport
156232 await server . connect ( transport ) ;
157233
158- // Error handling - prevent process crashes from closing transport
234+ process . stdin . once ( 'end' , handleStreamClose ( 'stdin' ) ) ;
235+ process . stdin . once ( 'close' , handleStreamClose ( 'stdin' ) ) ;
236+ process . stdin . once ( 'error' , handleStreamError ( 'stdin' ) ) ;
237+ process . stdout . once ( 'close' , handleStreamClose ( 'stdout' ) ) ;
238+ process . stdout . once ( 'error' , handleStreamError ( 'stdout' ) ) ;
239+ process . stderr . once ( 'close' , handleStreamClose ( 'stderr' ) ) ;
240+ process . stderr . once ( 'error' , handleStreamError ( 'stderr' ) ) ;
241+
242+ // Error handling - stdio disconnects should terminate, other errors stay logged.
159243 process . on ( 'uncaughtException' , ( error ) => {
160- console . error ( `[${ SERVER_NAME } ] Uncaught exception:` , error . message ) ;
161- console . error ( error . stack ) ;
244+ if ( isStdioDisconnectError ( error ) ) {
245+ void shutdown ( 'Uncaught stdio disconnect' , 0 , error ) ;
246+ return ;
247+ }
248+
249+ safeLogError ( `[${ SERVER_NAME } ] Uncaught exception` , error ) ;
162250 } ) ;
163251
164252 process . on ( 'unhandledRejection' , ( reason ) => {
165- console . error ( `[${ SERVER_NAME } ] Unhandled rejection:` , reason ) ;
253+ if ( isStdioDisconnectError ( reason ) ) {
254+ void shutdown ( 'Unhandled stdio disconnect' , 0 , reason ) ;
255+ return ;
256+ }
257+
258+ safeLogError ( `[${ SERVER_NAME } ] Unhandled rejection` , reason ) ;
166259 } ) ;
167260
168261 process . on ( 'SIGINT' , async ( ) => {
169- await server . close ( ) ;
170- process . exit ( 0 ) ;
262+ await shutdown ( 'Received SIGINT' ) ;
171263 } ) ;
172264
173265 process . on ( 'SIGTERM' , async ( ) => {
174- await server . close ( ) ;
175- process . exit ( 0 ) ;
266+ await shutdown ( 'Received SIGTERM' ) ;
176267 } ) ;
177268
178269 // Log server start (to stderr to not interfere with stdio protocol)
0 commit comments