@mrxkun/mcfast-mcp 4.1.11 → 4.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mrxkun/mcfast-mcp",
3
- "version": "4.1.11",
4
- "description": "Ultra-fast code editing with WASM acceleration, fuzzy patching, multi-layer caching, and 8 unified tools. v4.1.11: Suppress console output to prevent JSON-RPC stream corruption.",
3
+ "version": "4.1.12",
4
+ "description": "Ultra-fast code editing with WASM acceleration, fuzzy patching, multi-layer caching, and 8 unified tools. v4.1.12: Implement proper MCP stdio transport lifecycle and cleanup to prevent zombie processes.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "mcfast-mcp": "src/index.js"
package/src/index.js CHANGED
@@ -757,6 +757,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
757
757
  },
758
758
  required: []
759
759
  }
760
+ },
761
+ // HEALTH CHECK TOOL
762
+ {
763
+ name: "health_check",
764
+ description: "💚 Check MCP server health status and resource usage. Returns: process status, memory engine state, uptime, and system metrics.",
765
+ inputSchema: {
766
+ type: "object",
767
+ properties: {},
768
+ required: []
769
+ }
760
770
  }
761
771
  ],
762
772
  };
@@ -964,6 +974,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
964
974
  result = await projectAnalyzeExecute(args);
965
975
  summary = 'Project analyzed';
966
976
  }
977
+ // HEALTH CHECK TOOL
978
+ else if (name === "health_check") {
979
+ result = await handleHealthCheck();
980
+ summary = 'Health check completed';
981
+ }
967
982
  else {
968
983
  throw new Error(`Tool not found: ${name}`);
969
984
  }
@@ -3352,6 +3367,74 @@ async function handleSelectStrategy({ instruction, files = [] }) {
3352
3367
  }
3353
3368
  }
3354
3369
 
3370
+ /**
3371
+ * HEALTH CHECK HANDLER
3372
+ */
3373
+ async function handleHealthCheck() {
3374
+ const start = Date.now();
3375
+
3376
+ try {
3377
+ const memoryStats = memoryEngine ? await memoryEngine.getStats() : null;
3378
+ const watcherStats = memoryEngine?.watcher ? memoryEngine.watcher.getStats() : null;
3379
+
3380
+ const health = {
3381
+ status: 'healthy',
3382
+ timestamp: Date.now(),
3383
+ pid: process.pid,
3384
+ uptime: process.uptime(),
3385
+ memoryUsage: {
3386
+ rss: Math.round(process.memoryUsage().rss / 1024 / 1024) + ' MB',
3387
+ heapTotal: Math.round(process.memoryUsage().heapTotal / 1024 / 1024) + ' MB',
3388
+ heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + ' MB',
3389
+ external: Math.round(process.memoryUsage().external / 1024 / 1024) + ' MB'
3390
+ },
3391
+ memoryEngine: {
3392
+ ready: memoryEngineReady,
3393
+ initialized: memoryEngine?.isInitialized || false,
3394
+ stats: memoryStats
3395
+ },
3396
+ fileWatcher: watcherStats || {
3397
+ enabled: process.env.MCFAST_FILE_WATCHER !== 'false',
3398
+ status: 'not_running'
3399
+ },
3400
+ latencyMs: Date.now() - start
3401
+ };
3402
+
3403
+ reportAudit({
3404
+ tool: 'health_check',
3405
+ status: 'success',
3406
+ latency_ms: health.latencyMs
3407
+ });
3408
+
3409
+ return {
3410
+ content: [{
3411
+ type: 'text',
3412
+ text: JSON.stringify(health, null, 2)
3413
+ }]
3414
+ };
3415
+
3416
+ } catch (error) {
3417
+ reportAudit({
3418
+ tool: 'health_check',
3419
+ status: 'error',
3420
+ error_message: error.message,
3421
+ latency_ms: Date.now() - start
3422
+ });
3423
+
3424
+ return {
3425
+ content: [{
3426
+ type: 'text',
3427
+ text: JSON.stringify({
3428
+ status: 'error',
3429
+ error: error.message,
3430
+ timestamp: Date.now()
3431
+ })
3432
+ }],
3433
+ isError: true
3434
+ };
3435
+ }
3436
+ }
3437
+
3355
3438
  /**
3356
3439
  * Start Server
3357
3440
  */
@@ -3366,6 +3449,25 @@ if (!lockAcquired) {
3366
3449
 
3367
3450
  const transport = new StdioServerTransport();
3368
3451
 
3452
+ // MCP Stdio Transport Best Practice: Detect client disconnection
3453
+ // https://modelcontextprotocol.info/docs/concepts/transports/
3454
+ process.stdin.on('end', () => {
3455
+ console.error('[MCP] Client disconnected (stdin closed)');
3456
+ gracefulShutdown('stdin.end');
3457
+ });
3458
+
3459
+ process.stdin.on('error', (err) => {
3460
+ if (err.code === 'EPIPE' || err.code === 'ECONNRESET') {
3461
+ console.error('[MCP] Client disconnected (broken pipe)');
3462
+ gracefulShutdown('stdin.error');
3463
+ }
3464
+ });
3465
+
3466
+ process.stdout.on('error', (err) => {
3467
+ console.error('[MCP] stdout error:', err.code);
3468
+ gracefulShutdown('stdout.error');
3469
+ });
3470
+
3369
3471
  // Pre-initialize memory engine in background
3370
3472
  backgroundInitializeMemoryEngine().catch(err => {
3371
3473
  console.error(`${colors.yellow}[Memory]${colors.reset} Background init error: ${err.message}`);
@@ -3376,29 +3478,48 @@ backgroundInitializeMemoryEngine().catch(err => {
3376
3478
  async function gracefulShutdown(signal) {
3377
3479
  console.error(`${colors.yellow}[Shutdown]${colors.reset} Received ${signal}, cleaning up...`);
3378
3480
 
3481
+ const SHUTDOWN_TIMEOUT = 5000; // 5 seconds per MCP best practices
3482
+
3379
3483
  try {
3380
- // Stop memory engine
3381
- if (memoryEngine && memoryEngineReady) {
3382
- await memoryEngine.cleanup();
3383
- }
3384
-
3385
- // Flush audit queue
3386
- const auditQ = getAuditQueue();
3387
- await auditQ.destroy();
3388
-
3389
- // Release lock
3390
- await releaseLock();
3484
+ await Promise.race([
3485
+ (async () => {
3486
+ // Stop memory engine
3487
+ if (memoryEngine && memoryEngineReady) {
3488
+ await memoryEngine.cleanup();
3489
+ }
3490
+
3491
+ // Flush audit queue
3492
+ const auditQ = getAuditQueue();
3493
+ await auditQ.destroy();
3494
+
3495
+ // Release lock
3496
+ await releaseLock();
3497
+ })(),
3498
+ new Promise(resolve => setTimeout(resolve, SHUTDOWN_TIMEOUT))
3499
+ ]);
3391
3500
 
3392
3501
  console.error(`${colors.green}[Shutdown]${colors.reset} Cleanup complete`);
3393
3502
  } catch (error) {
3394
3503
  console.error(`${colors.red}[Shutdown]${colors.reset} Error during cleanup: ${error.message}`);
3395
3504
  }
3396
3505
 
3506
+ // Always exit (even if cleanup fails) to prevent zombie processes
3397
3507
  process.exit(0);
3398
3508
  }
3399
3509
 
3400
3510
  process.on('SIGINT', () => gracefulShutdown('SIGINT'));
3401
3511
  process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
3402
3512
 
3513
+ // Protect against zombie processes - handle unexpected errors
3514
+ process.on('uncaughtException', (err) => {
3515
+ console.error('[MCP] Uncaught exception:', err);
3516
+ gracefulShutdown('uncaughtException');
3517
+ });
3518
+
3519
+ process.on('unhandledRejection', (reason, promise) => {
3520
+ console.error('[MCP] Unhandled rejection:', reason);
3521
+ gracefulShutdown('unhandledRejection');
3522
+ });
3523
+
3403
3524
  await server.connect(transport);
3404
3525
  console.error("mcfast MCP v1.0.0 running on stdio");
@@ -225,23 +225,30 @@ export class MemoryEngine {
225
225
  });
226
226
  }
227
227
 
228
- // Initialize file watcher
229
- this.watcher = new FileWatcher(projectPath, this, {
230
- debounceMs: 1500,
231
- ignored: [
232
- '**/node_modules/**',
233
- '**/.git/**',
234
- '**/dist/**',
235
- '**/build/**',
236
- '**/.mcfast/**',
237
- '**/*.log'
238
- ]
239
- });
240
-
241
- try {
242
- await this.watcher.start();
243
- } catch (watcherError) {
244
- console.error('[MemoryEngine] ⚠️ File watcher failed to start (continuing without file watching):', watcherError.message);
228
+ // Initialize file watcher (configurable via MCFAST_FILE_WATCHER env)
229
+ const ENABLE_FILE_WATCHER = process.env.MCFAST_FILE_WATCHER !== 'false';
230
+
231
+ if (ENABLE_FILE_WATCHER) {
232
+ this.watcher = new FileWatcher(projectPath, this, {
233
+ debounceMs: 1500,
234
+ ignored: [
235
+ '**/node_modules/**',
236
+ '**/.git/**',
237
+ '**/dist/**',
238
+ '**/build/**',
239
+ '**/.mcfast/**',
240
+ '**/*.log'
241
+ ]
242
+ });
243
+
244
+ try {
245
+ await this.watcher.start();
246
+ } catch (watcherError) {
247
+ console.error('[MemoryEngine] ⚠️ File watcher failed to start (continuing without file watching):', watcherError.message);
248
+ this.watcher = null;
249
+ }
250
+ } else {
251
+ console.error('[MemoryEngine] File watcher disabled (MCFAST_FILE_WATCHER=false)');
245
252
  this.watcher = null;
246
253
  }
247
254
 
@@ -453,19 +453,21 @@ export class FileWatcher {
453
453
 
454
454
  console.error('[FileWatcher] Stopping...');
455
455
 
456
- // Stop cleanup interval
456
+ // 1. Stop cleanup interval
457
457
  if (this.cleanupInterval) {
458
458
  clearInterval(this.cleanupInterval);
459
459
  this.cleanupInterval = null;
460
460
  }
461
461
 
462
- // Process remaining updates
463
- if (this.pendingUpdates.size > 0) {
464
- console.error(`[FileWatcher] Processing remaining ${this.pendingUpdates.size} updates...`);
465
- await this.processQueue();
462
+ // 2. Clear pending updates map (free memory)
463
+ this.pendingUpdates.clear();
464
+
465
+ // 3. Cancel debounce timeout if exists
466
+ if (this.flushQueue && this.flushQueue.cancel) {
467
+ this.flushQueue.cancel();
466
468
  }
467
469
 
468
- // Close watcher
470
+ // 4. Close chokidar watcher (critical - prevents persistent file watching)
469
471
  if (this.watcher) {
470
472
  await this.watcher.close();
471
473
  this.watcher = null;