@mrxkun/mcfast-mcp 4.1.11 → 4.1.13

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.13",
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
@@ -25,13 +25,13 @@ const colors = {
25
25
 
26
26
  // Override console.log - suppress in MCP mode
27
27
  const originalConsoleLog = console.log;
28
- console.log = function(...args) {
28
+ console.log = function (...args) {
29
29
  // Suppressed in MCP mode
30
30
  };
31
31
 
32
32
  // Override console.error - suppress in MCP mode
33
33
  const originalConsoleError = console.error;
34
- console.error = function(...args) {
34
+ console.error = function (...args) {
35
35
  // Suppressed in MCP mode
36
36
  };
37
37
 
@@ -86,70 +86,70 @@ const LOCK_FILE_PATH = path.join(process.cwd(), '.mcfast', '.mcp-instance.lock')
86
86
  let lockFileHandle = null;
87
87
 
88
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}`);
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}`);
127
131
  return false;
128
- }
129
132
  }
130
- console.error(`${colors.red}[Lock]${colors.reset} Failed to acquire lock: ${error.message}`);
131
- return false;
132
- }
133
133
  }
134
134
 
135
135
  async function releaseLock() {
136
- if (lockFileHandle) {
136
+ if (lockFileHandle) {
137
+ try {
138
+ await lockFileHandle.close();
139
+ lockFileHandle = null;
140
+ } catch (e) {
141
+ // Ignore close errors
142
+ }
143
+ }
144
+
137
145
  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
- }
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
153
  }
154
154
 
155
155
  // ============================================================================
@@ -171,36 +171,36 @@ let memoryEngineReady = false;
171
171
  */
172
172
  async function backgroundInitializeMemoryEngine() {
173
173
  if (memoryEngineInitPromise) return memoryEngineInitPromise;
174
-
174
+
175
175
  memoryEngineInitPromise = (async () => {
176
176
  const maxRetries = 2;
177
177
  let lastError = null;
178
-
178
+
179
179
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
180
180
  try {
181
181
  memoryEngine = new MemoryEngine({
182
182
  apiKey: TOKEN,
183
183
  enableSync: true
184
184
  });
185
-
185
+
186
186
  // Use timeout for initialization
187
187
  const initTimeout = new Promise((_, reject) => {
188
188
  setTimeout(() => reject(new Error('Memory engine init timeout')), 25000);
189
189
  });
190
-
190
+
191
191
  await Promise.race([
192
192
  memoryEngine.initialize(process.cwd()),
193
193
  initTimeout
194
194
  ]);
195
-
195
+
196
196
  memoryEngineReady = true;
197
197
  console.error(`${colors.cyan}[Memory]${colors.reset} Engine initialized successfully`);
198
198
  return memoryEngine;
199
-
199
+
200
200
  } catch (error) {
201
201
  lastError = error;
202
202
  console.error(`${colors.yellow}[Memory]${colors.reset} Initialization attempt ${attempt}/${maxRetries} failed: ${error.message}`);
203
-
203
+
204
204
  if (attempt < maxRetries) {
205
205
  // Exponential backoff before retry
206
206
  const backoffMs = 1000 * Math.pow(2, attempt - 1);
@@ -209,7 +209,7 @@ async function backgroundInitializeMemoryEngine() {
209
209
  }
210
210
  }
211
211
  }
212
-
212
+
213
213
  // All retries failed - graceful degradation
214
214
  console.error(`${colors.yellow}[Memory]${colors.reset} All initialization attempts failed. Running in degraded mode.`);
215
215
  console.error(`${colors.yellow}[Memory]${colors.reset} Error: ${lastError?.message || 'Unknown error'}`);
@@ -217,7 +217,7 @@ async function backgroundInitializeMemoryEngine() {
217
217
  memoryEngineReady = false;
218
218
  return null;
219
219
  })();
220
-
220
+
221
221
  return memoryEngineInitPromise;
222
222
  }
223
223
 
@@ -228,7 +228,7 @@ async function getMemoryEngine() {
228
228
  if (memoryEngine && memoryEngineReady) {
229
229
  return memoryEngine;
230
230
  }
231
-
231
+
232
232
  // Wait for initialization with timeout
233
233
  if (memoryEngineInitPromise) {
234
234
  try {
@@ -240,12 +240,12 @@ async function getMemoryEngine() {
240
240
  console.error(`${colors.yellow}[Memory]${colors.reset} Waiting for engine: ${error.message}`);
241
241
  }
242
242
  }
243
-
243
+
244
244
  // If still not ready, try to initialize now
245
245
  if (!memoryEngine) {
246
246
  await backgroundInitializeMemoryEngine();
247
247
  }
248
-
248
+
249
249
  return memoryEngine;
250
250
  }
251
251
 
@@ -255,18 +255,18 @@ async function getMemoryEngine() {
255
255
  async function searchMemoryContext(instruction, maxResults = 5) {
256
256
  try {
257
257
  const engine = await getMemoryEngine();
258
-
258
+
259
259
  // Use intelligent search for best context
260
- const searchResult = await engine.intelligentSearch(instruction, {
261
- limit: maxResults
260
+ const searchResult = await engine.intelligentSearch(instruction, {
261
+ limit: maxResults
262
262
  });
263
-
263
+
264
264
  if (!searchResult.results || searchResult.results.length === 0) {
265
265
  return '';
266
266
  }
267
267
 
268
268
  let context = '';
269
-
269
+
270
270
  // Add code context from search results
271
271
  context += '\n\n--- MEMORY CONTEXT (Relevant Code) ---\n';
272
272
  searchResult.results.forEach((result, idx) => {
@@ -277,7 +277,7 @@ async function searchMemoryContext(instruction, maxResults = 5) {
277
277
  context += `${result.code.substring(0, 300)}${result.code.length > 300 ? '...' : ''}\n`;
278
278
  }
279
279
  });
280
-
280
+
281
281
  // Add facts if available
282
282
  const facts = await engine.searchFacts(instruction, 5);
283
283
  if (facts && facts.length > 0) {
@@ -286,7 +286,7 @@ async function searchMemoryContext(instruction, maxResults = 5) {
286
286
  context += `• ${fact.type}: ${fact.name} (${fact.file_id})\n`;
287
287
  });
288
288
  }
289
-
289
+
290
290
  return context;
291
291
  } catch (error) {
292
292
  console.error(`${colors.yellow}[Memory]${colors.reset} Context search failed: ${error.message}`);
@@ -304,44 +304,44 @@ async function autoTriggerIntelligence(filePath, instruction) {
304
304
  if (process.env.MCFAST_INTELLIGENCE_AUTO !== 'true') {
305
305
  return;
306
306
  }
307
-
307
+
308
308
  const engine = await getMemoryEngine();
309
309
  const cache = getIntelligenceCache();
310
-
310
+
311
311
  // 1. Detect patterns (cached for 5 minutes)
312
312
  const patternCacheKey = { file: filePath, type: 'patterns' };
313
313
  let patterns = cache.get('patterns', patternCacheKey);
314
-
314
+
315
315
  if (!patterns) {
316
316
  patterns = await engine.detectPatterns();
317
317
  if (patterns && patterns.length > 0) {
318
318
  cache.set('patterns', patternCacheKey, patterns);
319
319
  console.error(`${colors.cyan}[Intelligence]${colors.reset} 💡 Detected ${patterns.length} patterns`);
320
-
320
+
321
321
  // Log high-confidence patterns
322
322
  patterns.filter(p => p.confidence > 0.8).forEach(p => {
323
323
  console.error(`${colors.yellow}[Pattern]${colors.reset} ${p.type}: ${p.message}`);
324
324
  });
325
325
  }
326
326
  }
327
-
327
+
328
328
  // 2. Get suggestions (cached for 5 minutes)
329
329
  const suggestionCacheKey = { file: filePath, type: 'suggestions' };
330
330
  let suggestions = cache.get('suggestions', suggestionCacheKey);
331
-
331
+
332
332
  if (!suggestions) {
333
333
  suggestions = await engine.getSuggestions({ currentFile: filePath });
334
334
  if (suggestions && suggestions.length > 0) {
335
335
  cache.set('suggestions', suggestionCacheKey, suggestions);
336
336
  console.error(`${colors.cyan}[Intelligence]${colors.reset} 💡 ${suggestions.length} suggestions available`);
337
-
337
+
338
338
  // Show top suggestions
339
339
  suggestions.slice(0, 3).forEach(s => {
340
340
  console.error(`${colors.yellow}[Suggestion]${colors.reset} ${s.type}: ${s.message}`);
341
341
  });
342
342
  }
343
343
  }
344
-
344
+
345
345
  } catch (error) {
346
346
  // Silent fail - intelligence should not break the tool
347
347
  if (VERBOSE) {
@@ -357,7 +357,7 @@ async function logEditToMemory({ instruction, files, strategy, success, errorMes
357
357
  try {
358
358
  const engine = await getMemoryEngine();
359
359
  const diffSize = Object.keys(files).reduce((total, fp) => total + (files[fp]?.length || 0), 0);
360
-
360
+
361
361
  engine.logEditToMemory({
362
362
  instruction: instruction.substring(0, 500),
363
363
  files: Object.keys(files),
@@ -367,7 +367,7 @@ async function logEditToMemory({ instruction, files, strategy, success, errorMes
367
367
  durationMs,
368
368
  errorMessage
369
369
  });
370
-
370
+
371
371
  console.error(`${colors.cyan}[Memory]${colors.reset} Edit logged to history`);
372
372
  } catch (error) {
373
373
  console.error(`${colors.yellow}[Memory]${colors.reset} Log failed: ${error.message}`);
@@ -380,13 +380,13 @@ async function logEditToMemory({ instruction, files, strategy, success, errorMes
380
380
  async function analyzeRenameImpact(symbolName, currentFile = null) {
381
381
  try {
382
382
  const engine = await getMemoryEngine();
383
-
383
+
384
384
  // Search for symbol references across codebase
385
- const searchResult = await engine.intelligentSearch(symbolName, {
385
+ const searchResult = await engine.intelligentSearch(symbolName, {
386
386
  limit: 20,
387
387
  currentFile
388
388
  });
389
-
389
+
390
390
  const impactedFiles = new Set();
391
391
  const references = [];
392
392
 
@@ -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
  }
@@ -1041,9 +1056,9 @@ async function reportAudit(data) {
1041
1056
  flushInterval: 5000,
1042
1057
  verbose: VERBOSE
1043
1058
  });
1044
-
1059
+
1045
1060
  queue.add(auditData);
1046
-
1061
+
1047
1062
  if (VERBOSE) {
1048
1063
  const stats = queue.getStats();
1049
1064
  console.error(`${colors.dim}[Audit] Queued for batching. Queue size: ${stats.queued}${colors.reset}`);
@@ -1082,9 +1097,9 @@ async function handleRegexSearch({ query, path: searchPath, caseSensitive = fals
1082
1097
  if (!validation.valid) {
1083
1098
  const latency = Date.now() - start;
1084
1099
  return {
1085
- content: [{
1086
- type: "text",
1087
- text: `⚠️ Invalid regex pattern: ${validation.error}\n\nQuery: "${query}"\n\n💡 Tips:\n• Basic regex works: \\bword\\b, [a-z]+, (foo|bar)\n• Avoid lookbehind (?<=...) and lookahead (?=...) - not supported by grep\n• Use word boundaries \\b instead of lookarounds\n• Escape special chars: \\\\, \\+, \\*`
1100
+ content: [{
1101
+ type: "text",
1102
+ text: `⚠️ Invalid regex pattern: ${validation.error}\n\nQuery: "${query}"\n\n💡 Tips:\n• Basic regex works: \\bword\\b, [a-z]+, (foo|bar)\n• Avoid lookbehind (?<=...) and lookahead (?=...) - not supported by grep\n• Use word boundaries \\b instead of lookarounds\n• Escape special chars: \\\\, \\+, \\*`
1088
1103
  }],
1089
1104
  isError: true
1090
1105
  };
@@ -1120,12 +1135,12 @@ async function handleRegexSearch({ query, path: searchPath, caseSensitive = fals
1120
1135
 
1121
1136
  try {
1122
1137
  const { stdout, stderr } = await execAsync(command, { maxBuffer: 10 * 1024 * 1024 }); // 10MB buffer
1123
-
1138
+
1124
1139
  // Check for grep regex errors in stderr
1125
1140
  if (stderr && stderr.includes('Invalid regular expression')) {
1126
1141
  throw new Error(`grep: ${stderr}`);
1127
1142
  }
1128
-
1143
+
1129
1144
  const results = stdout.trim().split('\n').filter(Boolean);
1130
1145
 
1131
1146
  const latency = Date.now() - start;
@@ -1190,12 +1205,12 @@ async function handleRegexSearch({ query, path: searchPath, caseSensitive = fals
1190
1205
  content: [{ type: "text", text: `🔍 Regex Search: No matches found for "${query}" in ${searchPath} (${latency}ms)` }]
1191
1206
  };
1192
1207
  }
1193
-
1208
+
1194
1209
  // Handle regex syntax errors
1195
1210
  if (execErr.stderr && execErr.stderr.includes('Invalid')) {
1196
1211
  throw new Error(`Invalid regex syntax: ${execErr.stderr}. Try simplifying your pattern.`);
1197
1212
  }
1198
-
1213
+
1199
1214
  throw execErr;
1200
1215
  }
1201
1216
  } catch (error) {
@@ -1222,14 +1237,14 @@ async function handleRegexSearch({ query, path: searchPath, caseSensitive = fals
1222
1237
  */
1223
1238
  function validateRegex(pattern) {
1224
1239
  const warnings = [];
1225
-
1240
+
1226
1241
  try {
1227
1242
  // Test if it's valid JavaScript regex
1228
1243
  new RegExp(pattern);
1229
1244
  } catch (e) {
1230
1245
  return { valid: false, error: e.message, warnings };
1231
1246
  }
1232
-
1247
+
1233
1248
  // Check for patterns not supported by basic grep (PCRE-only features)
1234
1249
  const unsupportedPatterns = [
1235
1250
  { regex: /\(\?<[=!]/, name: 'Lookbehind (?<=...) or negative lookbehind (?<!...)' },
@@ -1241,18 +1256,18 @@ function validateRegex(pattern) {
1241
1256
  { regex: /\(\?\d*\)/, name: 'Subroutine calls (?1)' },
1242
1257
  { regex: /\(\?&\w+\)/, name: 'Subroutine calls (?&name)' }
1243
1258
  ];
1244
-
1259
+
1245
1260
  for (const { regex, name } of unsupportedPatterns) {
1246
1261
  if (regex.test(pattern)) {
1247
1262
  warnings.push(`Pattern uses ${name} which may not be supported by grep. Results may be incomplete.`);
1248
1263
  }
1249
1264
  }
1250
-
1265
+
1251
1266
  // Check for potentially dangerous patterns (catastrophic backtracking)
1252
1267
  if (/\(\w+\+\+?\)|\(\w+\*\*?\)\*\*?/.test(pattern)) {
1253
1268
  warnings.push('Pattern may cause performance issues (nested quantifiers)');
1254
1269
  }
1255
-
1270
+
1256
1271
  return { valid: true, warnings };
1257
1272
  }
1258
1273
 
@@ -1282,7 +1297,7 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
1282
1297
  // Convert literal query to glob pattern for file search
1283
1298
  pattern = `**/*${query}*`;
1284
1299
  }
1285
-
1300
+
1286
1301
  // Use fast-glob to find files
1287
1302
  const files = await fg([pattern], {
1288
1303
  cwd,
@@ -1294,7 +1309,7 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
1294
1309
 
1295
1310
  // Also search file contents if it's not a file pattern
1296
1311
  const contentMatches = [];
1297
-
1312
+
1298
1313
  // For simple queries, also search in common code files
1299
1314
  if (!isRegex && query.length > 1) {
1300
1315
  const codeFiles = await fg(['**/*.{js,ts,jsx,tsx,py,go,rs,java,rb,php}'], {
@@ -1302,7 +1317,7 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
1302
1317
  ignore: ['node_modules/**', '.git/**'],
1303
1318
  absolute: false
1304
1319
  });
1305
-
1320
+
1306
1321
  // Search in first 50 files to avoid performance issues
1307
1322
  for (const file of codeFiles.slice(0, 50)) {
1308
1323
  try {
@@ -1310,7 +1325,7 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
1310
1325
  if (content.includes(query)) {
1311
1326
  const lines = content.split('\n');
1312
1327
  const matches = [];
1313
-
1328
+
1314
1329
  lines.forEach((line, idx) => {
1315
1330
  if (line.includes(query)) {
1316
1331
  matches.push({
@@ -1320,7 +1335,7 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
1320
1335
  });
1321
1336
  }
1322
1337
  });
1323
-
1338
+
1324
1339
  if (matches.length > 0) {
1325
1340
  contentMatches.push({
1326
1341
  file,
@@ -1335,7 +1350,7 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
1335
1350
  }
1336
1351
 
1337
1352
  const latency = Date.now() - start;
1338
-
1353
+
1339
1354
  // Format output
1340
1355
  let output = `${colors.bold}Filesystem Search Results${colors.reset}\n\n`;
1341
1356
  output += `Query: "${query}"\n`;
@@ -1381,7 +1396,7 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
1381
1396
  };
1382
1397
  } catch (error) {
1383
1398
  const latency = Date.now() - start;
1384
-
1399
+
1385
1400
  reportAudit({
1386
1401
  tool: 'search_filesystem',
1387
1402
  instruction: `Search: ${query}`,
@@ -1441,7 +1456,7 @@ async function handleReapply({ instruction, files, errorContext = "", attempt =
1441
1456
  */
1442
1457
  async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1443
1458
  const editStartTime = Date.now();
1444
-
1459
+
1445
1460
  // Validate required parameters
1446
1461
  if (!instruction) {
1447
1462
  return {
@@ -1449,16 +1464,16 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1449
1464
  isError: true
1450
1465
  };
1451
1466
  }
1452
-
1467
+
1453
1468
  if (!files || Object.keys(files).length === 0) {
1454
1469
  return {
1455
1470
  content: [{ type: "text", text: "❌ Error: 'files' parameter is required and must contain at least one file" }],
1456
1471
  isError: true
1457
1472
  };
1458
1473
  }
1459
-
1474
+
1460
1475
  const firstFile = Object.keys(files)[0];
1461
-
1476
+
1462
1477
  // 1. Retrieve memory context for enhanced understanding
1463
1478
  let memoryContext = '';
1464
1479
  try {
@@ -1469,7 +1484,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1469
1484
  } catch (error) {
1470
1485
  console.error(`${colors.yellow}[Memory]${colors.reset} Context retrieval failed: ${error.message}`);
1471
1486
  }
1472
-
1487
+
1473
1488
  // 2. Analyze impact for rename operations
1474
1489
  let impactAnalysis = null;
1475
1490
  const renamePattern = instruction.match(/(?:rename|change)\s+(?:function|class|const|let|var|method)\s+(\w+)\s+to\s+(\w+)/i);
@@ -1484,7 +1499,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1484
1499
  console.error(`${colors.yellow}[IMPACT]${colors.reset} Analysis failed: ${error.message}`);
1485
1500
  }
1486
1501
  }
1487
-
1502
+
1488
1503
  // Check for multi-file edits first
1489
1504
  const multiFileEdit = detectCrossFileEdit(instruction, files);
1490
1505
 
@@ -1518,7 +1533,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1518
1533
  errorMessage: result.message || result.error,
1519
1534
  durationMs: Date.now() - editStartTime
1520
1535
  });
1521
-
1536
+
1522
1537
  return {
1523
1538
  content: [{
1524
1539
  type: "text",
@@ -1556,7 +1571,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1556
1571
  errorMessage: error.message,
1557
1572
  durationMs: Date.now() - editStartTime
1558
1573
  });
1559
-
1574
+
1560
1575
  return {
1561
1576
  content: [{
1562
1577
  type: "text",
@@ -1661,7 +1676,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1661
1676
  errorMessage: error.message,
1662
1677
  durationMs: Date.now() - editStartTime
1663
1678
  });
1664
-
1679
+
1665
1680
  return {
1666
1681
  content: [{
1667
1682
  type: "text",
@@ -1792,7 +1807,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1792
1807
 
1793
1808
  // Include impact analysis in result if available
1794
1809
  let resultText = `✅ AST Refactor Applied Successfully\n\nOperation: ${pattern.type}\nChanges: ${transformResult.count || transformResult.replacements || 0} locations\n\nBackup: ${editResult.backupPath}`;
1795
-
1810
+
1796
1811
  if (impactAnalysis && impactAnalysis.suggestion) {
1797
1812
  resultText += `\n\n${impactAnalysis.suggestion}`;
1798
1813
  if (impactAnalysis.impactedFiles.length > 0) {
@@ -1823,7 +1838,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1823
1838
  errorMessage: error.message,
1824
1839
  durationMs: Date.now() - editStartTime
1825
1840
  });
1826
-
1841
+
1827
1842
  return {
1828
1843
  content: [{
1829
1844
  type: "text",
@@ -1846,7 +1861,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1846
1861
  dryRun,
1847
1862
  toolName: 'edit'
1848
1863
  });
1849
-
1864
+
1850
1865
  // Log to memory
1851
1866
  await logEditToMemory({
1852
1867
  instruction,
@@ -1856,7 +1871,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1856
1871
  errorMessage: result.isError ? result.content?.[0]?.text : null,
1857
1872
  durationMs: Date.now() - editStartTime
1858
1873
  });
1859
-
1874
+
1860
1875
  return result;
1861
1876
  }
1862
1877
  }
@@ -1869,7 +1884,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1869
1884
  dryRun,
1870
1885
  toolName: 'edit'
1871
1886
  });
1872
-
1887
+
1873
1888
  // Log to memory
1874
1889
  await logEditToMemory({
1875
1890
  instruction,
@@ -1879,7 +1894,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1879
1894
  errorMessage: result.isError ? result.content?.[0]?.text : null,
1880
1895
  durationMs: Date.now() - editStartTime
1881
1896
  });
1882
-
1897
+
1883
1898
  return result;
1884
1899
  }
1885
1900
 
@@ -1889,14 +1904,14 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1889
1904
  if (memoryContext) {
1890
1905
  enhancedInstruction = `${instruction}\n\n--- CONTEXT FROM CODEBASE ---${memoryContext}`;
1891
1906
  }
1892
-
1907
+
1893
1908
  const result = await handleApplyFast({
1894
1909
  instruction: enhancedInstruction,
1895
1910
  files,
1896
1911
  dryRun,
1897
1912
  toolName: 'edit'
1898
1913
  });
1899
-
1914
+
1900
1915
  // Log to memory
1901
1916
  await logEditToMemory({
1902
1917
  instruction,
@@ -1906,13 +1921,13 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1906
1921
  errorMessage: result.isError ? result.content?.[0]?.text : null,
1907
1922
  durationMs: Date.now() - editStartTime
1908
1923
  });
1909
-
1924
+
1910
1925
  // Auto-trigger intelligence tools in background (non-blocking)
1911
1926
  if (!result.isError) {
1912
1927
  const firstFile = Object.keys(files)[0];
1913
- autoTriggerIntelligence(firstFile, instruction).catch(() => {});
1928
+ autoTriggerIntelligence(firstFile, instruction).catch(() => { });
1914
1929
  }
1915
-
1930
+
1916
1931
  return result;
1917
1932
  }
1918
1933
 
@@ -1924,7 +1939,7 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1924
1939
  */
1925
1940
  async function handleSearch({ query, files, path, mode = 'auto', regex = false, caseSensitive = false, contextLines = 2, maxResults = 10 }) {
1926
1941
  const searchStartTime = Date.now();
1927
-
1942
+
1928
1943
  // For regex mode without files, we need to do content-based regex search
1929
1944
  // since fast-glob doesn't support full regex patterns
1930
1945
  if (regex && !files && mode === 'auto') {
@@ -1939,9 +1954,9 @@ async function handleSearch({ query, files, path, mode = 'auto', regex = false,
1939
1954
  if ((detectedMode === 'auto' || detectedMode === 'local') && !regex) {
1940
1955
  try {
1941
1956
  const engine = await getMemoryEngine();
1942
-
1957
+
1943
1958
  // Use intelligent search (UltraHybrid + Smart Routing)
1944
- const searchResult = await engine.intelligentSearch(query, {
1959
+ const searchResult = await engine.intelligentSearch(query, {
1945
1960
  limit: maxResults,
1946
1961
  currentFile: files ? Object.keys(files)[0] : null
1947
1962
  });
@@ -2017,7 +2032,7 @@ async function handleRead({ filePath, filePaths, start_line, end_line, max_lines
2017
2032
  if (filePaths && Array.isArray(filePaths) && filePaths.length > 0) {
2018
2033
  return await handleBatchRead(filePaths, start_line, end_line, max_lines_per_file);
2019
2034
  }
2020
-
2035
+
2021
2036
  // Single file mode (backward compatible)
2022
2037
  return await handleReadFile({ path: filePath, start_line, end_line });
2023
2038
  }
@@ -2028,50 +2043,50 @@ async function handleRead({ filePath, filePaths, start_line, end_line, max_lines
2028
2043
  async function handleBatchRead(filePaths, start_line, end_line, max_lines_per_file) {
2029
2044
  const start = Date.now();
2030
2045
  const MAX_BATCH_SIZE = 50;
2031
-
2046
+
2032
2047
  if (filePaths.length > MAX_BATCH_SIZE) {
2033
2048
  return {
2034
2049
  content: [{ type: "text", text: `❌ Batch read limit exceeded: ${filePaths.length} files (max ${MAX_BATCH_SIZE})` }],
2035
2050
  isError: true
2036
2051
  };
2037
2052
  }
2038
-
2053
+
2039
2054
  console.error(`${colors.cyan}[BATCH READ]${colors.reset} ${filePaths.length} files`);
2040
-
2055
+
2041
2056
  // Read all files in parallel with Promise.all
2042
2057
  const results = await Promise.all(
2043
2058
  filePaths.map(async (filePath) => {
2044
2059
  try {
2045
- const result = await handleReadFileInternal({
2046
- path: filePath,
2047
- start_line,
2060
+ const result = await handleReadFileInternal({
2061
+ path: filePath,
2062
+ start_line,
2048
2063
  end_line,
2049
- max_lines: max_lines_per_file
2064
+ max_lines: max_lines_per_file
2050
2065
  });
2051
2066
  return { path: filePath, ...result, success: true };
2052
2067
  } catch (error) {
2053
- return {
2054
- path: filePath,
2055
- success: false,
2068
+ return {
2069
+ path: filePath,
2070
+ success: false,
2056
2071
  error: error.message,
2057
2072
  content: `❌ Error reading ${filePath}: ${error.message}`
2058
2073
  };
2059
2074
  }
2060
2075
  })
2061
2076
  );
2062
-
2077
+
2063
2078
  // Calculate stats
2064
2079
  const successful = results.filter(r => r.success).length;
2065
2080
  const failed = results.filter(r => !r.success).length;
2066
2081
  const totalLines = results.reduce((sum, r) => sum + (r.lines || 0), 0);
2067
2082
  const totalSize = results.reduce((sum, r) => sum + (r.size || 0), 0);
2068
-
2083
+
2069
2084
  // Format output
2070
2085
  let output = `📚 Batch Read Complete (${filePaths.length} files)\n`;
2071
2086
  output += `✅ Successful: ${successful} ❌ Failed: ${failed}\n`;
2072
2087
  output += `📊 Total: ${totalLines} lines, ${(totalSize / 1024).toFixed(1)} KB\n`;
2073
2088
  output += `⏱️ Latency: ${Date.now() - start}ms\n\n`;
2074
-
2089
+
2075
2090
  // Add each file's content
2076
2091
  results.forEach((result, index) => {
2077
2092
  output += `[${index + 1}/${filePaths.length}] `;
@@ -2090,7 +2105,7 @@ async function handleBatchRead(filePaths, start_line, end_line, max_lines_per_fi
2090
2105
  }
2091
2106
  output += `\n\n`;
2092
2107
  });
2093
-
2108
+
2094
2109
  // Report audit for batch operation
2095
2110
  reportAudit({
2096
2111
  tool: 'read_file',
@@ -2101,7 +2116,7 @@ async function handleBatchRead(filePaths, start_line, end_line, max_lines_per_fi
2101
2116
  input_tokens: Math.ceil(filePaths.join(',').length / 4),
2102
2117
  output_tokens: Math.ceil(output.length / 4)
2103
2118
  });
2104
-
2119
+
2105
2120
  return {
2106
2121
  content: [{ type: "text", text: output }]
2107
2122
  };
@@ -2119,7 +2134,7 @@ async function handleReadFileInternal({ path: filePath, start_line, end_line, ma
2119
2134
  }
2120
2135
 
2121
2136
  const STREAM_THRESHOLD = 1024 * 1024; // 1MB
2122
-
2137
+
2123
2138
  let startLine = start_line ? parseInt(start_line) : 1;
2124
2139
  let endLine = end_line ? parseInt(end_line) : -1;
2125
2140
  let outputContent;
@@ -2247,12 +2262,12 @@ async function handleWarpgrep({ query, include = ".", isRegex = false, caseSensi
2247
2262
  });
2248
2263
  return { content: [{ type: "text", text: msg }] };
2249
2264
  }
2250
-
2265
+
2251
2266
  // Handle regex syntax errors
2252
2267
  if (execErr.stderr && execErr.stderr.includes('Invalid')) {
2253
2268
  throw new Error(`Invalid regex syntax: ${execErr.stderr}. Try simplifying your pattern.`);
2254
2269
  }
2255
-
2270
+
2256
2271
  throw execErr;
2257
2272
  }
2258
2273
  } catch (error) {
@@ -3078,18 +3093,18 @@ async function handleMemorySearch({ query, type = 'all', maxResults = 6, minScor
3078
3093
  */
3079
3094
  async function handleMemoryGet({ type }) {
3080
3095
  const start = Date.now();
3081
-
3096
+
3082
3097
  // Early return if memory engine not ready
3083
3098
  if (!memoryEngineReady) {
3084
3099
  return {
3085
- content: [{
3086
- type: "text",
3087
- text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
3100
+ content: [{
3101
+ type: "text",
3102
+ text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
3088
3103
  }],
3089
3104
  isError: true
3090
3105
  };
3091
3106
  }
3092
-
3107
+
3093
3108
  try {
3094
3109
  const engine = await getMemoryEngine();
3095
3110
  let output = '';
@@ -3123,12 +3138,12 @@ async function handleMemoryGet({ type }) {
3123
3138
  } else if (type === 'reindex') {
3124
3139
  output = `🔄 Re-indexing Codebase\n\n`;
3125
3140
  output += `Starting re-index in background...\n`;
3126
-
3141
+
3127
3142
  // Trigger re-index in background
3128
3143
  engine.performInitialScan().catch(err => {
3129
3144
  console.error('[Reindex] Error:', err.message);
3130
3145
  });
3131
-
3146
+
3132
3147
  output += `✅ Re-index started. Check stats in a few seconds.\n`;
3133
3148
  output += `Use 'memory_get stats' to check progress.`;
3134
3149
  } else if (type === 'intelligence') {
@@ -3176,18 +3191,18 @@ async function handleMemoryGet({ type }) {
3176
3191
  */
3177
3192
  async function handleDetectPatterns() {
3178
3193
  const start = Date.now();
3179
-
3194
+
3180
3195
  // Early return if memory engine not ready
3181
3196
  if (!memoryEngineReady) {
3182
3197
  return {
3183
- content: [{
3184
- type: "text",
3185
- text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
3198
+ content: [{
3199
+ type: "text",
3200
+ text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
3186
3201
  }],
3187
3202
  isError: true
3188
3203
  };
3189
3204
  }
3190
-
3205
+
3191
3206
  try {
3192
3207
  const engine = await getMemoryEngine();
3193
3208
  const patterns = await engine.detectPatterns();
@@ -3235,18 +3250,18 @@ async function handleDetectPatterns() {
3235
3250
  */
3236
3251
  async function handleGetSuggestions({ currentFile, currentLine }) {
3237
3252
  const start = Date.now();
3238
-
3253
+
3239
3254
  // Early return if memory engine not ready
3240
3255
  if (!memoryEngineReady) {
3241
3256
  return {
3242
- content: [{
3243
- type: "text",
3244
- text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
3257
+ content: [{
3258
+ type: "text",
3259
+ text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
3245
3260
  }],
3246
3261
  isError: true
3247
3262
  };
3248
3263
  }
3249
-
3264
+
3250
3265
  try {
3251
3266
  const engine = await getMemoryEngine();
3252
3267
  const suggestions = await engine.getSuggestions({
@@ -3300,18 +3315,18 @@ async function handleGetSuggestions({ currentFile, currentLine }) {
3300
3315
  */
3301
3316
  async function handleSelectStrategy({ instruction, files = [] }) {
3302
3317
  const start = Date.now();
3303
-
3318
+
3304
3319
  // Early return if memory engine not ready
3305
3320
  if (!memoryEngineReady) {
3306
3321
  return {
3307
- content: [{
3308
- type: "text",
3309
- text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
3322
+ content: [{
3323
+ type: "text",
3324
+ text: `⏳ Memory engine is still initializing. Please try again in a few seconds.`
3310
3325
  }],
3311
3326
  isError: true
3312
3327
  };
3313
3328
  }
3314
-
3329
+
3315
3330
  try {
3316
3331
  const engine = await getMemoryEngine();
3317
3332
  const result = await engine.selectStrategy(instruction, { files });
@@ -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
  */
@@ -3359,13 +3442,28 @@ async function handleSelectStrategy({ instruction, files = [] }) {
3359
3442
  // Acquire lock before starting - prevents multiple instances
3360
3443
  const lockAcquired = await acquireLock();
3361
3444
  if (!lockAcquired) {
3362
- console.error(`${colors.red}[ERROR]${colors.reset} Failed to acquire lock. Another mcfast-mcp instance may be running.`);
3363
- console.error(`${colors.yellow}[HINT]${colors.reset} Delete ${LOCK_FILE_PATH} if you're sure no other instance is running.`);
3364
- process.exit(1);
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);
3365
3448
  }
3366
3449
 
3367
3450
  const transport = new StdioServerTransport();
3368
3451
 
3452
+ // NOTE: Do NOT add process.stdin.on('end') handler here.
3453
+ // VSCode and some MCP clients send ephemeral EOF / half-close signals during
3454
+ // the initialize handshake, which would cause premature gracefulShutdown
3455
+ // before the server has a chance to respond, resulting in the
3456
+ // "Error: calling 'initialize': EOF" error.
3457
+ //
3458
+ // The MCP SDK's StdioServerTransport already manages transport lifecycle.
3459
+ // Only handle unrecoverable stdout errors.
3460
+ process.stdout.on('error', (err) => {
3461
+ if (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED') {
3462
+ console.error('[MCP] stdout closed, shutting down:', err.code);
3463
+ gracefulShutdown('stdout.error');
3464
+ }
3465
+ });
3466
+
3369
3467
  // Pre-initialize memory engine in background
3370
3468
  backgroundInitializeMemoryEngine().catch(err => {
3371
3469
  console.error(`${colors.yellow}[Memory]${colors.reset} Background init error: ${err.message}`);
@@ -3374,31 +3472,50 @@ backgroundInitializeMemoryEngine().catch(err => {
3374
3472
 
3375
3473
  // Graceful shutdown handler
3376
3474
  async function gracefulShutdown(signal) {
3377
- console.error(`${colors.yellow}[Shutdown]${colors.reset} Received ${signal}, cleaning up...`);
3378
-
3379
- 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();
3391
-
3392
- console.error(`${colors.green}[Shutdown]${colors.reset} Cleanup complete`);
3393
- } catch (error) {
3394
- console.error(`${colors.red}[Shutdown]${colors.reset} Error during cleanup: ${error.message}`);
3395
- }
3396
-
3397
- process.exit(0);
3475
+ console.error(`${colors.yellow}[Shutdown]${colors.reset} Received ${signal}, cleaning up...`);
3476
+
3477
+ const SHUTDOWN_TIMEOUT = 5000; // 5 seconds per MCP best practices
3478
+
3479
+ try {
3480
+ await Promise.race([
3481
+ (async () => {
3482
+ // Stop memory engine
3483
+ if (memoryEngine && memoryEngineReady) {
3484
+ await memoryEngine.cleanup();
3485
+ }
3486
+
3487
+ // Flush audit queue
3488
+ const auditQ = getAuditQueue();
3489
+ await auditQ.destroy();
3490
+
3491
+ // Release lock
3492
+ await releaseLock();
3493
+ })(),
3494
+ new Promise(resolve => setTimeout(resolve, SHUTDOWN_TIMEOUT))
3495
+ ]);
3496
+
3497
+ console.error(`${colors.green}[Shutdown]${colors.reset} Cleanup complete`);
3498
+ } catch (error) {
3499
+ console.error(`${colors.red}[Shutdown]${colors.reset} Error during cleanup: ${error.message}`);
3500
+ }
3501
+
3502
+ // Always exit (even if cleanup fails) to prevent zombie processes
3503
+ process.exit(0);
3398
3504
  }
3399
3505
 
3400
3506
  process.on('SIGINT', () => gracefulShutdown('SIGINT'));
3401
3507
  process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
3402
3508
 
3509
+ // Protect against zombie processes - handle unexpected errors
3510
+ process.on('uncaughtException', (err) => {
3511
+ console.error('[MCP] Uncaught exception:', err);
3512
+ gracefulShutdown('uncaughtException');
3513
+ });
3514
+
3515
+ process.on('unhandledRejection', (reason, promise) => {
3516
+ console.error('[MCP] Unhandled rejection:', reason);
3517
+ gracefulShutdown('unhandledRejection');
3518
+ });
3519
+
3403
3520
  await server.connect(transport);
3404
3521
  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;