@mrxkun/mcfast-mcp 4.1.10 → 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.10",
4
- "description": "Ultra-fast code editing with WASM acceleration, fuzzy patching, multi-layer caching, and 8 unified tools. v4.1.10: 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
@@ -75,9 +75,87 @@ import { getAuditQueue } from './utils/audit-queue.js';
75
75
  import { getIntelligenceCache } from './utils/intelligence-cache.js';
76
76
  import { getContextPrefetcher } from './utils/context-prefetcher.js';
77
77
  import { parallelMemorySearch } from './utils/parallel-search.js';
78
+ import { execute as projectAnalyzeExecute } from './tools/project_analyze.js';
78
79
 
79
80
  const execAsync = promisify(exec);
80
81
 
82
+ // ============================================================================
83
+ // LOCK FILE MECHANISM - Prevent multiple instances running simultaneously
84
+ // ============================================================================
85
+ const LOCK_FILE_PATH = path.join(process.cwd(), '.mcfast', '.mcp-instance.lock');
86
+ let lockFileHandle = null;
87
+
88
+ async function acquireLock() {
89
+ try {
90
+ const lockDir = path.dirname(LOCK_FILE_PATH);
91
+ await fs.mkdir(lockDir, { recursive: true });
92
+
93
+ lockFileHandle = await fs.open(LOCK_FILE_PATH, 'wx');
94
+
95
+ // Write current PID
96
+ await lockFileHandle.write(`${process.pid}\n${Date.now()}\n`, 0);
97
+ await lockFileHandle.sync();
98
+
99
+ // Cleanup on exit
100
+ process.on('beforeExit', releaseLock);
101
+ process.on('SIGINT', releaseLock);
102
+ process.on('SIGTERM', releaseLock);
103
+ process.on('uncaughtException', releaseLock);
104
+
105
+ console.error(`${colors.cyan}[Lock]${colors.reset} Acquired lock file: ${LOCK_FILE_PATH}`);
106
+ return true;
107
+ } catch (error) {
108
+ if (error.code === 'EEXIST') {
109
+ // Lock file exists, check if it's stale
110
+ try {
111
+ const lockContent = await fs.readFile(LOCK_FILE_PATH, 'utf-8');
112
+ const [pid, timestamp] = lockContent.trim().split('\n');
113
+ const lockAge = Date.now() - parseInt(timestamp);
114
+
115
+ // Check if process is still running (stale lock if > 5 minutes)
116
+ if (lockAge > 5 * 60 * 1000) {
117
+ console.error(`${colors.yellow}[Lock]${colors.reset} Stale lock detected (age: ${Math.round(lockAge / 1000)}s), removing...`);
118
+ await fs.unlink(LOCK_FILE_PATH);
119
+ return acquireLock(); // Retry
120
+ }
121
+
122
+ console.error(`${colors.red}[Lock]${colors.reset} Another instance is already running (PID: ${pid})`);
123
+ console.error(`${colors.yellow}[Lock]${colors.reset} To force start, delete: ${LOCK_FILE_PATH}`);
124
+ return false;
125
+ } catch (readError) {
126
+ console.error(`${colors.red}[Lock]${colors.reset} Failed to read lock file: ${readError.message}`);
127
+ return false;
128
+ }
129
+ }
130
+ console.error(`${colors.red}[Lock]${colors.reset} Failed to acquire lock: ${error.message}`);
131
+ return false;
132
+ }
133
+ }
134
+
135
+ async function releaseLock() {
136
+ if (lockFileHandle) {
137
+ try {
138
+ await lockFileHandle.close();
139
+ lockFileHandle = null;
140
+ } catch (e) {
141
+ // Ignore close errors
142
+ }
143
+ }
144
+
145
+ try {
146
+ await fs.unlink(LOCK_FILE_PATH);
147
+ console.error(`${colors.cyan}[Lock]${colors.reset} Released lock file`);
148
+ } catch (error) {
149
+ if (error.code !== 'ENOENT') {
150
+ console.error(`${colors.yellow}[Lock]${colors.reset} Failed to release lock: ${error.message}`);
151
+ }
152
+ }
153
+ }
154
+
155
+ // ============================================================================
156
+ // END LOCK FILE MECHANISM
157
+ // ============================================================================
158
+
81
159
  const API_URL = "https://mcfast.vercel.app/api/v1";
82
160
  const TOKEN = process.env.MCFAST_TOKEN;
83
161
  const VERBOSE = process.env.MCFAST_VERBOSE !== 'false'; // Default: true
@@ -95,28 +173,49 @@ async function backgroundInitializeMemoryEngine() {
95
173
  if (memoryEngineInitPromise) return memoryEngineInitPromise;
96
174
 
97
175
  memoryEngineInitPromise = (async () => {
98
- try {
99
- memoryEngine = new MemoryEngine({
100
- apiKey: TOKEN,
101
- enableSync: true
102
- });
103
- // Use a shorter timeout for initialization
104
- const initTimeout = new Promise((_, reject) => {
105
- setTimeout(() => reject(new Error('Memory engine init timeout')), 25000);
106
- });
107
-
108
- await Promise.race([
109
- memoryEngine.initialize(process.cwd()),
110
- initTimeout
111
- ]);
112
-
113
- memoryEngineReady = true;
114
- console.error(`${colors.cyan}[Memory]${colors.reset} Engine initialized successfully`);
115
- } catch (error) {
116
- console.error(`${colors.yellow}[Memory]${colors.reset} Engine initialization failed: ${error.message}`);
117
- // Don't throw - allow partial initialization
118
- memoryEngineReady = false;
176
+ const maxRetries = 2;
177
+ let lastError = null;
178
+
179
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
180
+ try {
181
+ memoryEngine = new MemoryEngine({
182
+ apiKey: TOKEN,
183
+ enableSync: true
184
+ });
185
+
186
+ // Use timeout for initialization
187
+ const initTimeout = new Promise((_, reject) => {
188
+ setTimeout(() => reject(new Error('Memory engine init timeout')), 25000);
189
+ });
190
+
191
+ await Promise.race([
192
+ memoryEngine.initialize(process.cwd()),
193
+ initTimeout
194
+ ]);
195
+
196
+ memoryEngineReady = true;
197
+ console.error(`${colors.cyan}[Memory]${colors.reset} Engine initialized successfully`);
198
+ return memoryEngine;
199
+
200
+ } catch (error) {
201
+ lastError = error;
202
+ console.error(`${colors.yellow}[Memory]${colors.reset} Initialization attempt ${attempt}/${maxRetries} failed: ${error.message}`);
203
+
204
+ if (attempt < maxRetries) {
205
+ // Exponential backoff before retry
206
+ const backoffMs = 1000 * Math.pow(2, attempt - 1);
207
+ console.error(`${colors.yellow}[Memory]${colors.reset} Retrying in ${backoffMs}ms...`);
208
+ await new Promise(resolve => setTimeout(resolve, backoffMs));
209
+ }
210
+ }
119
211
  }
212
+
213
+ // All retries failed - graceful degradation
214
+ console.error(`${colors.yellow}[Memory]${colors.reset} All initialization attempts failed. Running in degraded mode.`);
215
+ console.error(`${colors.yellow}[Memory]${colors.reset} Error: ${lastError?.message || 'Unknown error'}`);
216
+ console.error(`${colors.yellow}[Memory]${colors.reset} Tools requiring memory will be unavailable.`);
217
+ memoryEngineReady = false;
218
+ return null;
120
219
  })();
121
220
 
122
221
  return memoryEngineInitPromise;
@@ -645,6 +744,29 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
645
744
  },
646
745
  required: ["instruction"]
647
746
  }
747
+ },
748
+ // PROJECT ANALYSIS TOOL
749
+ {
750
+ name: "project_analyze",
751
+ description: "🔍 Analyze project structure using AI (Mercury). Returns: Project Overview, Technologies, API Endpoints, Main Features. Auto-updates MEMORY.md. Requires MCFAST_TOKEN.",
752
+ inputSchema: {
753
+ type: "object",
754
+ properties: {
755
+ force: { type: "boolean", description: "Force re-analysis (ignore cache)" },
756
+ updateMemory: { type: "boolean", description: "Update MEMORY.md with results (default: true)" }
757
+ },
758
+ required: []
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
+ }
648
770
  }
649
771
  ],
650
772
  };
@@ -847,6 +969,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
847
969
  result = await handleSelectStrategy(args);
848
970
  summary = 'Strategy selected';
849
971
  }
972
+ // PROJECT ANALYSIS TOOL
973
+ else if (name === "project_analyze") {
974
+ result = await projectAnalyzeExecute(args);
975
+ summary = 'Project analyzed';
976
+ }
977
+ // HEALTH CHECK TOOL
978
+ else if (name === "health_check") {
979
+ result = await handleHealthCheck();
980
+ summary = 'Health check completed';
981
+ }
850
982
  else {
851
983
  throw new Error(`Tool not found: ${name}`);
852
984
  }
@@ -3235,14 +3367,158 @@ async function handleSelectStrategy({ instruction, files = [] }) {
3235
3367
  }
3236
3368
  }
3237
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
+
3238
3438
  /**
3239
3439
  * Start Server
3240
3440
  */
3441
+
3442
+ // Acquire lock before starting - prevents multiple instances
3443
+ const lockAcquired = await acquireLock();
3444
+ if (!lockAcquired) {
3445
+ console.error(`${colors.red}[ERROR]${colors.reset} Failed to acquire lock. Another mcfast-mcp instance may be running.`);
3446
+ console.error(`${colors.yellow}[HINT]${colors.reset} Delete ${LOCK_FILE_PATH} if you're sure no other instance is running.`);
3447
+ process.exit(1);
3448
+ }
3449
+
3241
3450
  const transport = new StdioServerTransport();
3242
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
+
3243
3471
  // Pre-initialize memory engine in background
3244
3472
  backgroundInitializeMemoryEngine().catch(err => {
3245
3473
  console.error(`${colors.yellow}[Memory]${colors.reset} Background init error: ${err.message}`);
3474
+ // Don't exit - continue without memory
3475
+ });
3476
+
3477
+ // Graceful shutdown handler
3478
+ async function gracefulShutdown(signal) {
3479
+ console.error(`${colors.yellow}[Shutdown]${colors.reset} Received ${signal}, cleaning up...`);
3480
+
3481
+ const SHUTDOWN_TIMEOUT = 5000; // 5 seconds per MCP best practices
3482
+
3483
+ try {
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
+ ]);
3500
+
3501
+ console.error(`${colors.green}[Shutdown]${colors.reset} Cleanup complete`);
3502
+ } catch (error) {
3503
+ console.error(`${colors.red}[Shutdown]${colors.reset} Error during cleanup: ${error.message}`);
3504
+ }
3505
+
3506
+ // Always exit (even if cleanup fails) to prevent zombie processes
3507
+ process.exit(0);
3508
+ }
3509
+
3510
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
3511
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
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');
3246
3522
  });
3247
3523
 
3248
3524
  await server.connect(transport);