@mrxkun/mcfast-mcp 4.0.0 → 4.0.3

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/src/index.js CHANGED
@@ -40,6 +40,7 @@ import {
40
40
  } from './strategies/tree-sitter/refactor.js';
41
41
  import { safeEdit } from './utils/backup.js';
42
42
  import { formatError } from './utils/error-formatter.js';
43
+ import { MemoryEngine } from './memory/index.js';
43
44
 
44
45
  const execAsync = promisify(exec);
45
46
 
@@ -47,6 +48,132 @@ const API_URL = "https://mcfast.vercel.app/api/v1";
47
48
  const TOKEN = process.env.MCFAST_TOKEN;
48
49
  const VERBOSE = process.env.MCFAST_VERBOSE !== 'false'; // Default: true
49
50
 
51
+ // Memory Engine (initialized lazily)
52
+ let memoryEngine = null;
53
+
54
+ async function getMemoryEngine() {
55
+ if (!memoryEngine) {
56
+ memoryEngine = new MemoryEngine({
57
+ apiKey: TOKEN,
58
+ enableSync: true
59
+ });
60
+ await memoryEngine.initialize(process.cwd());
61
+ console.error(`${colors.cyan}[Memory]${colors.reset} Engine initialized`);
62
+ }
63
+ return memoryEngine;
64
+ }
65
+
66
+ /**
67
+ * Search memory for context related to instruction using UltraHybrid search
68
+ */
69
+ async function searchMemoryContext(instruction, maxResults = 5) {
70
+ try {
71
+ const engine = await getMemoryEngine();
72
+
73
+ // Use intelligent search for best context
74
+ const searchResult = await engine.intelligentSearch(instruction, {
75
+ limit: maxResults
76
+ });
77
+
78
+ if (!searchResult.results || searchResult.results.length === 0) {
79
+ return '';
80
+ }
81
+
82
+ let context = '';
83
+
84
+ // Add code context from search results
85
+ context += '\n\n--- MEMORY CONTEXT (Relevant Code) ---\n';
86
+ searchResult.results.forEach((result, idx) => {
87
+ const fileName = result.filePath || result.path || 'unknown';
88
+ const score = result.finalScore || result.score || 0;
89
+ context += `\n[${idx + 1}] ${fileName} (score: ${score.toFixed(2)})\n`;
90
+ if (result.code) {
91
+ context += `${result.code.substring(0, 300)}${result.code.length > 300 ? '...' : ''}\n`;
92
+ }
93
+ });
94
+
95
+ // Add facts if available
96
+ const facts = await engine.searchFacts(instruction, 5);
97
+ if (facts && facts.length > 0) {
98
+ context += '\n--- MEMORY CONTEXT (Facts) ---\n';
99
+ facts.forEach(fact => {
100
+ context += `• ${fact.type}: ${fact.name} (${fact.file_id})\n`;
101
+ });
102
+ }
103
+
104
+ return context;
105
+ } catch (error) {
106
+ console.error(`${colors.yellow}[Memory]${colors.reset} Context search failed: ${error.message}`);
107
+ return '';
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Log edit to memory after successful edit
113
+ */
114
+ async function logEditToMemory({ instruction, files, strategy, success, errorMessage, durationMs }) {
115
+ try {
116
+ const engine = await getMemoryEngine();
117
+ const diffSize = Object.keys(files).reduce((total, fp) => total + (files[fp]?.length || 0), 0);
118
+
119
+ engine.logEditToMemory({
120
+ instruction: instruction.substring(0, 500),
121
+ files: Object.keys(files),
122
+ strategy,
123
+ success,
124
+ diffSize,
125
+ durationMs,
126
+ errorMessage
127
+ });
128
+
129
+ console.error(`${colors.cyan}[Memory]${colors.reset} Edit logged to history`);
130
+ } catch (error) {
131
+ console.error(`${colors.yellow}[Memory]${colors.reset} Log failed: ${error.message}`);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Impact Analysis for rename operations using semantic search
137
+ */
138
+ async function analyzeRenameImpact(symbolName, currentFile = null) {
139
+ try {
140
+ const engine = await getMemoryEngine();
141
+
142
+ // Search for symbol references across codebase
143
+ const searchResult = await engine.intelligentSearch(symbolName, {
144
+ limit: 20,
145
+ currentFile
146
+ });
147
+
148
+ const impactedFiles = new Set();
149
+ const references = [];
150
+
151
+ searchResult.results.forEach(result => {
152
+ const filePath = result.filePath || result.path;
153
+ if (filePath && filePath !== currentFile) {
154
+ impactedFiles.add(filePath);
155
+ references.push({
156
+ file: filePath,
157
+ score: result.finalScore || result.score,
158
+ context: result.code?.substring(0, 150)
159
+ });
160
+ }
161
+ });
162
+
163
+ return {
164
+ impactedFiles: Array.from(impactedFiles),
165
+ references: references.slice(0, 10),
166
+ totalReferences: searchResult.results.length,
167
+ suggestion: impactedFiles.size > 0
168
+ ? `āš ļø Rename will affect ${impactedFiles.size} file(s) with ${references.length} reference(s). Consider using multi-file edit.`
169
+ : null
170
+ };
171
+ } catch (error) {
172
+ console.error(`${colors.yellow}[Memory]${colors.reset} Impact analysis failed: ${error.message}`);
173
+ return { impactedFiles: [], references: [], totalReferences: 0, suggestion: null };
174
+ }
175
+ }
176
+
50
177
  // ANSI Color Codes for Terminal Output
51
178
  const colors = {
52
179
  reset: '\x1b[0m',
@@ -69,6 +196,8 @@ const toolIcons = {
69
196
  read: 'šŸ“–',
70
197
  list_files: 'šŸ“',
71
198
  reapply: 'šŸ”',
199
+ memory_search: '🧠',
200
+ memory_get: 'šŸ“š',
72
201
  // Legacy aliases (backward compatibility)
73
202
  apply_fast: '⚔',
74
203
  apply_search_replace: 'šŸ”„',
@@ -957,11 +1086,41 @@ async function handleReapply({ instruction, files, errorContext = "", attempt =
957
1086
  }
958
1087
 
959
1088
  /**
960
- * UNIFIED HANDLER 1: handleEdit (v2.0)
1089
+ * UNIFIED HANDLER 1: handleEdit (v3.0 - Enhanced with Memory)
961
1090
  * Consolidates: apply_fast + edit_file + apply_search_replace
962
1091
  * Auto-detects best strategy based on input
1092
+ * NEW: Context retrieval, impact analysis, auto-fix references
963
1093
  */
964
1094
  async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1095
+ const editStartTime = Date.now();
1096
+ const firstFile = Object.keys(files)[0];
1097
+
1098
+ // 1. Retrieve memory context for enhanced understanding
1099
+ let memoryContext = '';
1100
+ try {
1101
+ memoryContext = await searchMemoryContext(instruction, 5);
1102
+ if (memoryContext) {
1103
+ console.error(`${colors.cyan}[MEMORY]${colors.reset} Retrieved context for edit`);
1104
+ }
1105
+ } catch (error) {
1106
+ console.error(`${colors.yellow}[MEMORY]${colors.reset} Context retrieval failed: ${error.message}`);
1107
+ }
1108
+
1109
+ // 2. Analyze impact for rename operations
1110
+ let impactAnalysis = null;
1111
+ const renamePattern = instruction.match(/(?:rename|change)\s+(?:function|class|const|let|var|method)\s+(\w+)\s+to\s+(\w+)/i);
1112
+ if (renamePattern) {
1113
+ const symbolName = renamePattern[1];
1114
+ try {
1115
+ impactAnalysis = await analyzeRenameImpact(symbolName, firstFile);
1116
+ if (impactAnalysis.suggestion) {
1117
+ console.error(`${colors.yellow}[IMPACT]${colors.reset} ${impactAnalysis.suggestion}`);
1118
+ }
1119
+ } catch (error) {
1120
+ console.error(`${colors.yellow}[IMPACT]${colors.reset} Analysis failed: ${error.message}`);
1121
+ }
1122
+ }
1123
+
965
1124
  // Check for multi-file edits first
966
1125
  const multiFileEdit = detectCrossFileEdit(instruction, files);
967
1126
 
@@ -986,6 +1145,16 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
986
1145
  }
987
1146
 
988
1147
  if (!result.success) {
1148
+ // Log failed edit to memory
1149
+ await logEditToMemory({
1150
+ instruction,
1151
+ files,
1152
+ strategy: multiFileEdit.type,
1153
+ success: false,
1154
+ errorMessage: result.message || result.error,
1155
+ durationMs: Date.now() - editStartTime
1156
+ });
1157
+
989
1158
  return {
990
1159
  content: [{
991
1160
  type: "text",
@@ -997,6 +1166,15 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
997
1166
  };
998
1167
  }
999
1168
 
1169
+ // Log successful edit to memory
1170
+ await logEditToMemory({
1171
+ instruction,
1172
+ files: result.files || Object.keys(files),
1173
+ strategy: multiFileEdit.type,
1174
+ success: true,
1175
+ durationMs: Date.now() - editStartTime
1176
+ });
1177
+
1000
1178
  return {
1001
1179
  content: [{
1002
1180
  type: "text",
@@ -1005,6 +1183,16 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1005
1183
  };
1006
1184
 
1007
1185
  } catch (error) {
1186
+ // Log failed edit to memory
1187
+ await logEditToMemory({
1188
+ instruction,
1189
+ files,
1190
+ strategy: 'multi_file_error',
1191
+ success: false,
1192
+ errorMessage: error.message,
1193
+ durationMs: Date.now() - editStartTime
1194
+ });
1195
+
1008
1196
  return {
1009
1197
  content: [{
1010
1198
  type: "text",
@@ -1018,13 +1206,12 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1018
1206
  }
1019
1207
 
1020
1208
  const strategy = detectEditStrategy({ instruction, code_edit, files });
1021
-
1022
1209
  console.error(`${colors.cyan}[EDIT STRATEGY]${colors.reset} ${strategy}`);
1023
1210
 
1024
1211
  // Strategy 1: Fuzzy Patch (unified diff format)
1025
1212
  if (strategy === 'fuzzy_patch') {
1026
1213
  const diffText = code_edit || instruction;
1027
- const filePath = Object.keys(files)[0]; // Assume single file for now
1214
+ const filePath = Object.keys(files)[0];
1028
1215
  const fileContent = files[filePath];
1029
1216
 
1030
1217
  try {
@@ -1056,6 +1243,16 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1056
1243
  await fs.writeFile(filePath, patchResult.content, 'utf8');
1057
1244
  });
1058
1245
 
1246
+ // Log edit to memory
1247
+ await logEditToMemory({
1248
+ instruction,
1249
+ files: { [filePath]: fileContent },
1250
+ strategy: 'fuzzy_patch',
1251
+ success: editResult.success,
1252
+ errorMessage: editResult.error,
1253
+ durationMs: Date.now() - editStartTime
1254
+ });
1255
+
1059
1256
  if (!editResult.success) {
1060
1257
  // Determine error type based on result
1061
1258
  let errorType = 'generic';
@@ -1091,6 +1288,16 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1091
1288
  };
1092
1289
 
1093
1290
  } catch (error) {
1291
+ // Log failed edit
1292
+ await logEditToMemory({
1293
+ instruction,
1294
+ files,
1295
+ strategy: 'fuzzy_patch',
1296
+ success: false,
1297
+ errorMessage: error.message,
1298
+ durationMs: Date.now() - editStartTime
1299
+ });
1300
+
1094
1301
  return {
1095
1302
  content: [{
1096
1303
  type: "text",
@@ -1191,6 +1398,15 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1191
1398
  await fs.writeFile(filePath, transformResult.code, 'utf8');
1192
1399
  });
1193
1400
 
1401
+ // Log successful AST refactor
1402
+ await logEditToMemory({
1403
+ instruction,
1404
+ files: { [filePath]: fileContent },
1405
+ strategy: `ast_refactor:${pattern.type}`,
1406
+ success: editResult.success,
1407
+ durationMs: Date.now() - editStartTime
1408
+ });
1409
+
1194
1410
  if (!editResult.success) {
1195
1411
  let errorType = 'generic';
1196
1412
  if (editResult.error.includes('ENOENT')) errorType = 'file_not_found';
@@ -1210,14 +1426,40 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1210
1426
  };
1211
1427
  }
1212
1428
 
1429
+ // Include impact analysis in result if available
1430
+ let resultText = `āœ… AST Refactor Applied Successfully\n\nOperation: ${pattern.type}\nChanges: ${transformResult.count || transformResult.replacements || 0} locations\n\nBackup: ${editResult.backupPath}`;
1431
+
1432
+ if (impactAnalysis && impactAnalysis.suggestion) {
1433
+ resultText += `\n\n${impactAnalysis.suggestion}`;
1434
+ if (impactAnalysis.impactedFiles.length > 0) {
1435
+ resultText += `\n\nšŸ“‹ Affected files:`;
1436
+ impactAnalysis.impactedFiles.slice(0, 5).forEach(f => {
1437
+ resultText += `\n • ${f}`;
1438
+ });
1439
+ if (impactAnalysis.impactedFiles.length > 5) {
1440
+ resultText += `\n ... and ${impactAnalysis.impactedFiles.length - 5} more`;
1441
+ }
1442
+ }
1443
+ }
1444
+
1213
1445
  return {
1214
1446
  content: [{
1215
1447
  type: "text",
1216
- text: `āœ… AST Refactor Applied Successfully\n\nOperation: ${pattern.type}\nChanges: ${transformResult.count || transformResult.replacements || 0} locations\n\nBackup: ${editResult.backupPath}`
1448
+ text: resultText
1217
1449
  }]
1218
1450
  };
1219
1451
 
1220
1452
  } catch (error) {
1453
+ // Log failed edit
1454
+ await logEditToMemory({
1455
+ instruction,
1456
+ files,
1457
+ strategy: `ast_refactor:${pattern?.type || 'unknown'}`,
1458
+ success: false,
1459
+ errorMessage: error.message,
1460
+ durationMs: Date.now() - editStartTime
1461
+ });
1462
+
1221
1463
  return {
1222
1464
  content: [{
1223
1465
  type: "text",
@@ -1234,40 +1476,85 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1234
1476
  if (strategy === 'search_replace') {
1235
1477
  const extracted = extractSearchReplace(instruction);
1236
1478
  if (extracted) {
1237
- return await handleApplyFast({
1479
+ const result = await handleApplyFast({
1238
1480
  instruction: `Replace checking for exact match:\nSEARCH:\n${extracted.search}\n\nREPLACE WITH:\n${extracted.replace}`,
1239
1481
  files,
1240
1482
  dryRun,
1241
1483
  toolName: 'edit'
1242
1484
  });
1485
+
1486
+ // Log to memory
1487
+ await logEditToMemory({
1488
+ instruction,
1489
+ files,
1490
+ strategy: 'search_replace',
1491
+ success: !result.isError,
1492
+ errorMessage: result.isError ? result.content?.[0]?.text : null,
1493
+ durationMs: Date.now() - editStartTime
1494
+ });
1495
+
1496
+ return result;
1243
1497
  }
1244
1498
  }
1245
1499
 
1246
1500
  // Strategy 3: Placeholder Merge (token-efficient)
1247
1501
  if (strategy === 'placeholder_merge' && code_edit) {
1248
- return await handleApplyFast({
1502
+ const result = await handleApplyFast({
1249
1503
  instruction: `${instruction}\n\nUSE PLACEHOLDER MERGE STRATEGY. Code snippet:\n${code_edit}`,
1250
1504
  files,
1251
1505
  dryRun,
1252
1506
  toolName: 'edit'
1253
1507
  });
1508
+
1509
+ // Log to memory
1510
+ await logEditToMemory({
1511
+ instruction,
1512
+ files,
1513
+ strategy: 'placeholder_merge',
1514
+ success: !result.isError,
1515
+ errorMessage: result.isError ? result.content?.[0]?.text : null,
1516
+ durationMs: Date.now() - editStartTime
1517
+ });
1518
+
1519
+ return result;
1254
1520
  }
1255
1521
 
1256
1522
  // Strategy 4: Mercury Intelligent (most flexible, default)
1257
- return await handleApplyFast({
1258
- instruction,
1523
+ // Enhance instruction with memory context
1524
+ let enhancedInstruction = instruction;
1525
+ if (memoryContext) {
1526
+ enhancedInstruction = `${instruction}\n\n--- CONTEXT FROM CODEBASE ---${memoryContext}`;
1527
+ }
1528
+
1529
+ const result = await handleApplyFast({
1530
+ instruction: enhancedInstruction,
1259
1531
  files,
1260
1532
  dryRun,
1261
1533
  toolName: 'edit'
1262
1534
  });
1535
+
1536
+ // Log to memory
1537
+ await logEditToMemory({
1538
+ instruction,
1539
+ files,
1540
+ strategy: 'mercury_intelligent',
1541
+ success: !result.isError,
1542
+ errorMessage: result.isError ? result.content?.[0]?.text : null,
1543
+ durationMs: Date.now() - editStartTime
1544
+ });
1545
+
1546
+ return result;
1263
1547
  }
1264
1548
 
1265
1549
  /**
1266
- * UNIFIED HANDLER 2: handleSearch (v2.0)
1550
+ * UNIFIED HANDLER 2: handleSearch (v3.0 - Enhanced with Memory)
1267
1551
  * Consolidates: search_code + search_code_ai + search_filesystem
1268
1552
  * Auto-detects best strategy based on input
1553
+ * Memory System Integration: Intelligent local-first hybrid search
1269
1554
  */
1270
- async function handleSearch({ query, files, path, mode = 'auto', regex = false, caseSensitive = false, contextLines = 2 }) {
1555
+ async function handleSearch({ query, files, path, mode = 'auto', regex = false, caseSensitive = false, contextLines = 2, maxResults = 10 }) {
1556
+ const searchStartTime = Date.now();
1557
+
1271
1558
  // For regex mode without files, we need to do content-based regex search
1272
1559
  // since fast-glob doesn't support full regex patterns
1273
1560
  if (regex && !files && mode === 'auto') {
@@ -1278,6 +1565,50 @@ async function handleSearch({ query, files, path, mode = 'auto', regex = false,
1278
1565
 
1279
1566
  console.error(`${colors.cyan}[SEARCH STRATEGY]${colors.reset} ${detectedMode}`);
1280
1567
 
1568
+ // Memory System Integration: Try intelligent search for auto/local modes
1569
+ if ((detectedMode === 'auto' || detectedMode === 'local') && !regex) {
1570
+ try {
1571
+ const engine = await getMemoryEngine();
1572
+
1573
+ // Use intelligent search (UltraHybrid + Smart Routing)
1574
+ const searchResult = await engine.intelligentSearch(query, {
1575
+ limit: maxResults,
1576
+ currentFile: files ? Object.keys(files)[0] : null
1577
+ });
1578
+
1579
+ if (searchResult.results && searchResult.results.length > 0) {
1580
+ console.error(`${colors.cyan}[MEMORY]${colors.reset} Found ${searchResult.results.length} results via intelligent search`);
1581
+
1582
+ // Format results
1583
+ let output = `šŸ” Intelligent Search Results for "${query}"\n`;
1584
+ output += `${colors.dim}Method: ${searchResult.metadata?.method || 'intelligent'} | `;
1585
+ output += `Accuracy: ${searchResult.metadata?.accuracy || '90%'} | `;
1586
+ output += `Duration: ${searchResult.metadata?.totalDuration || 'N/A'}${colors.reset}\n\n`;
1587
+
1588
+ searchResult.results.slice(0, maxResults).forEach((result, i) => {
1589
+ const filePath = result.filePath || result.path || 'unknown';
1590
+ const score = result.finalScore || result.score || 0;
1591
+ output += `[${i + 1}] ${colors.yellow}${filePath}${colors.reset}\n`;
1592
+ if (result.code) {
1593
+ output += ` ${result.code.slice(0, 200)}${result.code.length > 200 ? '...' : ''}\n`;
1594
+ }
1595
+ output += ` ${colors.dim}Score: ${score.toFixed(2)}${colors.reset}\n\n`;
1596
+ });
1597
+
1598
+ if (searchResult.results.length > maxResults) {
1599
+ output += `${colors.dim}... and ${searchResult.results.length - maxResults} more results${colors.reset}\n`;
1600
+ }
1601
+
1602
+ return { content: [{ type: "text", text: output }] };
1603
+ } else {
1604
+ console.error(`${colors.yellow}[MEMORY]${colors.reset} No results from intelligent search, falling back...`);
1605
+ }
1606
+ } catch (memoryError) {
1607
+ console.error(`${colors.yellow}[MEMORY]${colors.reset} Intelligent search failed: ${memoryError.message}`);
1608
+ // Continue with normal search strategies
1609
+ }
1610
+ }
1611
+
1281
1612
  // Strategy 1: Local search (if files provided)
1282
1613
  if (detectedMode === 'local' && files) {
1283
1614
  return await handleSearchCode({ query, files, regex, caseSensitive, contextLines });
@@ -1550,6 +1881,12 @@ async function handleWarpgrep({ query, include = ".", isRegex = false, caseSensi
1550
1881
  });
1551
1882
  return { content: [{ type: "text", text: msg }] };
1552
1883
  }
1884
+
1885
+ // Handle regex syntax errors
1886
+ if (execErr.stderr && execErr.stderr.includes('Invalid')) {
1887
+ throw new Error(`Invalid regex syntax: ${execErr.stderr}. Try simplifying your pattern.`);
1888
+ }
1889
+
1553
1890
  throw execErr;
1554
1891
  }
1555
1892
  } catch (error) {
@@ -1569,6 +1906,11 @@ async function handleWarpgrep({ query, include = ".", isRegex = false, caseSensi
1569
1906
  }
1570
1907
  }
1571
1908
 
1909
+ /**
1910
+ * UNIFIED HANDLER 4: handleSearchCode (v2.0)
1911
+ * Renamed from handleSearchCode for consistency
1912
+ * Now supports both single file and batch search modes
1913
+ */
1572
1914
  async function handleSearchCode({ query, files, regex = false, caseSensitive = false, contextLines = 2 }) {
1573
1915
  const start = Date.now();
1574
1916
  try {
@@ -1730,104 +2072,161 @@ async function handleSearchCode({ query, files, regex = false, caseSensitive = f
1730
2072
  }
1731
2073
  }
1732
2074
 
1733
- async function handleListFiles({ path: dirPath = process.cwd(), depth = 5, offset = 0, limit = 500 }) {
1734
- const start = Date.now();
2075
+ /**
2076
+ * UNIFIED HANDLER 5: handleSearchCodeAI (v2.0)
2077
+ * Renamed from handleSearchCodeAI for consistency
2078
+ * Now supports both single file and batch search modes
2079
+ */
2080
+ async function handleSearchCodeAI({ query, files, contextLines = 2 }) {
2081
+ if (!TOKEN) {
2082
+ return {
2083
+ content: [{ type: "text", text: "āŒ Error: MCFAST_TOKEN is missing. Please set it in your MCP config." }],
2084
+ isError: true
2085
+ };
2086
+ }
1735
2087
  try {
1736
- const files = await getFiles(dirPath, depth);
1737
-
1738
- // Apply pagination
1739
- const totalFiles = files.length;
1740
- const paginatedFiles = files.slice(offset, offset + limit);
1741
- const hasMore = offset + limit < totalFiles;
2088
+ const response = await fetch(`${API_URL}/search-ai`, {
2089
+ method: "POST",
2090
+ headers: {
2091
+ "Content-Type": "application/json",
2092
+ "Authorization": `Bearer ${TOKEN}`,
2093
+ },
2094
+ body: JSON.stringify({ query, files, contextLines }),
2095
+ });
1742
2096
 
1743
- // Build output with pagination info
1744
- let output = `šŸ“ Files in ${dirPath} (depth: ${depth}):\n`;
1745
- output += `Showing ${offset + 1}-${Math.min(offset + limit, totalFiles)} of ${totalFiles} files`;
1746
- if (hasMore) {
1747
- output += `. Use offset: ${offset + limit} to see more.`;
2097
+ if (!response.ok) {
2098
+ const errorText = await response.text();
2099
+ return {
2100
+ content: [{ type: "text", text: `Search Error (${response.status}): ${errorText}` }],
2101
+ isError: true,
2102
+ };
1748
2103
  }
1749
- output += `\n\n${paginatedFiles.join('\n')}`;
1750
2104
 
1751
- // Check for large output and add helpful message
1752
- const outputLength = output.length;
1753
- if (outputLength > 50000) {
1754
- output += `\n\nšŸ’” Tip: Output is large. Use depth parameter to limit depth (e.g., depth: 2 or 3) to limit the search scope.`;
2105
+ const data = await response.json();
2106
+
2107
+ // Check for warning (empty file content)
2108
+ if (data.warning) {
2109
+ return {
2110
+ content: [{ type: "text", text: `āš ļø ${data.warning}\n\nThis usually means the AI agent didn't load file contents before searching. The files parameter should contain actual file content, not empty strings.` }],
2111
+ };
1755
2112
  }
1756
2113
 
1757
- reportAudit({
1758
- tool: 'list_files_fast',
1759
- instruction: dirPath,
1760
- status: 'success',
1761
- latency_ms: Date.now() - start,
1762
- files_count: totalFiles,
1763
- result_summary: JSON.stringify(paginatedFiles.slice(0, 500)),
1764
- input_tokens: Math.ceil(dirPath.length / 4),
1765
- output_tokens: Math.ceil(output.length / 4)
1766
- });
2114
+ // Format results nicely
2115
+ let output = `šŸ” Found ${data.totalMatches} matches for "${query}"\n\n`;
2116
+
2117
+ if (data.results.length === 0) {
2118
+ output += "No matches found.";
2119
+ } else {
2120
+ data.results.forEach((result, i) => {
2121
+ output += `šŸ“„ ${result.file}:${result.lineNumber}\n`;
2122
+ result.context.forEach(ctx => {
2123
+ const prefix = ctx.isMatch ? "→ " : " ";
2124
+ output += `${prefix}${ctx.lineNumber}: ${ctx.content}\n`;
2125
+ });
2126
+ if (i < data.results.length - 1) output += "\n";
2127
+ });
2128
+ }
1767
2129
 
1768
2130
  return {
1769
- content: [{ type: "text", text: output }]
2131
+ content: [{ type: "text", text: output }],
1770
2132
  };
1771
2133
 
1772
2134
  } catch (error) {
1773
- // Error recovery: suggest using depth parameter
1774
- let errorMessage = `āŒ Error listing files: ${error.message}`;
1775
- if (error.message.includes('too many files') || error.message.includes('EMFILE') || error.message.includes('max')) {
1776
- errorMessage += `\n\nšŸ’” Tip: Try reducing the depth (e.g., depth: 2 or 3) to limit the search scope.`;
1777
- }
1778
-
1779
- reportAudit({
1780
- tool: 'list_files_fast',
1781
- instruction: dirPath,
1782
- status: 'error',
1783
- error_message: error.message,
1784
- latency_ms: Date.now() - start
1785
- });
1786
2135
  return {
1787
- content: [{ type: "text", text: errorMessage }],
1788
- isError: true
2136
+ content: [{ type: "text", text: `Search Connection Error: ${error.message}` }],
2137
+ isError: true,
1789
2138
  };
1790
2139
  }
1791
2140
  }
1792
2141
 
1793
- async function handleEditFile({ path: filePath, content, instruction = "" }) {
1794
- const start = Date.now();
2142
+ /**
2143
+ * UNIFIED HANDLER 6: handleGetDefinition (v2.0)
2144
+ * Renamed from handleGetDefinition for consistency
2145
+ * Now supports both single file and batch search modes
2146
+ */
2147
+ async function handleGetDefinition({ path: filePath, symbol }) {
2148
+ if (!filePath || !symbol) throw new Error("Missing path or symbol");
2149
+
2150
+ const absolutePath = path.resolve(filePath);
2151
+
2152
+ // Check if path is a directory - search across files in directory
2153
+ let stats;
1795
2154
  try {
1796
- await fs.writeFile(filePath, content, 'utf8');
2155
+ stats = await fs.stat(absolutePath);
2156
+ } catch (e) {
2157
+ throw new Error(`Path does not exist: ${filePath}`);
2158
+ }
1797
2159
 
1798
- const estimatedTokens = Math.ceil(content.length / 4);
2160
+ if (!stats.isFile()) {
2161
+ // It's a directory - search for symbol definition using grep
2162
+ return await handleDefinitionInDirectory({ path: absolutePath, symbol });
2163
+ }
1799
2164
 
1800
- reportAudit({
1801
- tool: 'edit_file',
1802
- instruction: instruction || `Written ${filePath}`,
1803
- status: 'success',
1804
- latency_ms: Date.now() - start,
1805
- files_count: 1,
1806
- input_tokens: estimatedTokens,
1807
- output_tokens: 10 // Minimal output (success message)
1808
- });
2165
+ // It's a file - use tree-sitter to find definition
2166
+ const content = await fs.readFile(filePath, 'utf8');
2167
+ const definitions = await findDefinition(content, filePath, symbol);
1809
2168
 
2169
+ if (definitions.length === 0) {
1810
2170
  return {
1811
- content: [{ type: "text", text: `āœ… File saved successfully: ${filePath}` }]
2171
+ content: [{ type: "text", text: `No definition found for '${symbol}' in ${filePath}` }]
1812
2172
  };
1813
- } catch (error) {
1814
- reportAudit({
1815
- tool: 'edit_file',
1816
- instruction: instruction || `Failed write ${filePath}`,
1817
- status: 'error',
1818
- error_message: error.message,
1819
- latency_ms: Date.now() - start,
1820
- input_tokens: Math.ceil((content?.length || 0) / 4),
1821
- output_tokens: 0
1822
- });
2173
+ }
2174
+
2175
+ const def = definitions[0]; // Take first
2176
+
2177
+ return {
2178
+ content: [{ type: "text", text: `Definition of '${symbol}':\n${filePath}:${def.line}:${def.column} (${def.type || 'unknown'})` }]
2179
+ };
2180
+ }
2181
+
2182
+ /**
2183
+ * UNIFIED HANDLER 7: handleFindReferences (v2.0)
2184
+ * Renamed from handleFindReferences for consistency
2185
+ * Now supports both single file and batch search modes
2186
+ */
2187
+ async function handleFindReferences({ path: filePath, symbol }) {
2188
+ if (!filePath || !symbol) throw new Error("Missing path or symbol");
2189
+
2190
+ const absolutePath = path.resolve(filePath);
2191
+
2192
+ // Check if path is a directory - search across files in directory
2193
+ let stats;
2194
+ try {
2195
+ stats = await fs.stat(absolutePath);
2196
+ } catch (e) {
2197
+ throw new Error(`Path does not exist: ${filePath}`);
2198
+ }
2199
+
2200
+ if (!stats.isFile()) {
2201
+ // It's a directory - search for references using grep
2202
+ return await handleReferencesInDirectory({ path: absolutePath, symbol });
2203
+ }
2204
+
2205
+ // It's a file - use tree-sitter to find references
2206
+ const content = await fs.readFile(filePath, 'utf8');
2207
+ const references = await findReferences(content, filePath, symbol);
2208
+
2209
+ if (references.length === 0) {
1823
2210
  return {
1824
- content: [{ type: "text", text: `āŒ Failed to write file: ${error.message}` }],
1825
- isError: true
2211
+ content: [{ type: "text", text: `No references found for '${symbol}' in ${filePath}` }]
1826
2212
  };
1827
2213
  }
1828
- }
1829
2214
 
2215
+ let output = `References for '${symbol}' in ${filePath} (${references.length}):\n`;
2216
+ references.forEach(ref => {
2217
+ output += `${ref.line}:${ref.column}\n`;
2218
+ });
2219
+
2220
+ return {
2221
+ content: [{ type: "text", text: output }]
2222
+ };
2223
+ }
1830
2224
 
2225
+ /**
2226
+ * UNIFIED HANDLER 8: handleReadFile (v2.0)
2227
+ * Renamed from handleReadFile for consistency
2228
+ * Now supports both single file and batch search modes
2229
+ */
1831
2230
  async function handleReadFile({ path: filePath, start_line, end_line }) {
1832
2231
  const start = Date.now();
1833
2232
  try {
@@ -1880,7 +2279,7 @@ async function handleReadFile({ path: filePath, start_line, end_line }) {
1880
2279
  if (startLine && endLine) {
1881
2280
  lineRangeInfo = `(Lines ${startLine}-${endLine} of ${totalLines})`;
1882
2281
  } else if (startLine) {
1883
- lineRangeInfo = `(Lines ${startLine}-${currentLine} of ${totalLines})`;
2282
+ lineRangeInfo = `(Lines ${startLine}-${currentLine} of ${totalLines} - truncated)`;
1884
2283
  } else {
1885
2284
  lineRangeInfo = `(Lines 1-${lines.length} of ${totalLines} - truncated)`;
1886
2285
  }
@@ -1935,6 +2334,11 @@ async function handleReadFile({ path: filePath, start_line, end_line }) {
1935
2334
  }
1936
2335
  }
1937
2336
 
2337
+ /**
2338
+ * UNIFIED HANDLER 9: handleApplyFast (v2.0)
2339
+ * Renamed from handleApplyFast for consistency
2340
+ * Now supports both single file and batch search modes
2341
+ */
1938
2342
  async function handleApplyFast({ instruction, files, dryRun, toolName }) {
1939
2343
  if (!TOKEN) {
1940
2344
  return {
@@ -2008,103 +2412,76 @@ async function handleApplyFast({ instruction, files, dryRun, toolName }) {
2008
2412
  }
2009
2413
  }
2010
2414
 
2011
- async function handleSearchCodeAI({ query, files, contextLines = 2 }) {
2012
- if (!TOKEN) {
2013
- return {
2014
- content: [{ type: "text", text: "āŒ Error: MCFAST_TOKEN is missing. Please set it in your MCP config." }],
2015
- isError: true
2016
- };
2017
- }
2415
+ /**
2416
+ * UNIFIED HANDLER 10: handleListFiles (v2.0)
2417
+ * Renamed from handleListFiles for consistency
2418
+ * Now supports both single file and batch search modes
2419
+ */
2420
+ async function handleListFiles({ path: dirPath = process.cwd(), depth = 5, offset = 0, limit = 500 }) {
2421
+ const start = Date.now();
2018
2422
  try {
2019
- const response = await fetch(`${API_URL}/search-ai`, {
2020
- method: "POST",
2021
- headers: {
2022
- "Content-Type": "application/json",
2023
- "Authorization": `Bearer ${TOKEN}`,
2024
- },
2025
- body: JSON.stringify({ query, files, contextLines }),
2026
- });
2027
-
2028
- if (!response.ok) {
2029
- const errorText = await response.text();
2030
- return {
2031
- content: [{ type: "text", text: `Search Error (${response.status}): ${errorText}` }],
2032
- isError: true,
2033
- };
2034
- }
2423
+ const files = await getFiles(dirPath, depth);
2035
2424
 
2036
- const data = await response.json();
2425
+ // Apply pagination
2426
+ const totalFiles = files.length;
2427
+ const paginatedFiles = files.slice(offset, offset + limit);
2428
+ const hasMore = offset + limit < totalFiles;
2037
2429
 
2038
- // Check for warning (empty file content)
2039
- if (data.warning) {
2040
- return {
2041
- content: [{ type: "text", text: `āš ļø ${data.warning}\n\nThis usually means the AI agent didn't load file contents before searching. The files parameter should contain actual file content, not empty strings.` }],
2042
- };
2430
+ // Build output with pagination info
2431
+ let output = `šŸ“ Files in ${dirPath} (depth: ${depth}):\n`;
2432
+ output += `Showing ${offset + 1}-${Math.min(offset + limit, totalFiles)} of ${totalFiles} files`;
2433
+ if (hasMore) {
2434
+ output += `. Use offset: ${offset + limit} to see more.`;
2043
2435
  }
2436
+ output += `\n\n${paginatedFiles.join('\n')}`;
2044
2437
 
2045
- // Format results nicely
2046
- let output = `šŸ” Found ${data.totalMatches} matches for "${query}"\n\n`;
2047
-
2048
- if (data.results.length === 0) {
2049
- output += "No matches found.";
2050
- } else {
2051
- data.results.forEach((result, i) => {
2052
- output += `šŸ“„ ${result.file}:${result.lineNumber}\n`;
2053
- result.context.forEach(ctx => {
2054
- const prefix = ctx.isMatch ? "→ " : " ";
2055
- output += `${prefix}${ctx.lineNumber}: ${ctx.content}\n`;
2056
- });
2057
- if (i < data.results.length - 1) output += "\n";
2058
- });
2438
+ // Check for large output and add helpful message
2439
+ const outputLength = output.length;
2440
+ if (outputLength > 50000) {
2441
+ output += `\n\nšŸ’” Tip: Output is large. Use depth parameter to limit depth (e.g., depth: 2 or 3) to limit the search scope.`;
2059
2442
  }
2060
2443
 
2061
- return {
2062
- content: [{ type: "text", text: output }],
2063
- };
2444
+ reportAudit({
2445
+ tool: 'list_files_fast',
2446
+ instruction: dirPath,
2447
+ status: 'success',
2448
+ latency_ms: Date.now() - start,
2449
+ files_count: totalFiles,
2450
+ result_summary: JSON.stringify(paginatedFiles.slice(0, 500)),
2451
+ input_tokens: Math.ceil(dirPath.length / 4),
2452
+ output_tokens: Math.ceil(output.length / 4)
2453
+ });
2064
2454
 
2065
- } catch (error) {
2066
2455
  return {
2067
- content: [{ type: "text", text: `Search Connection Error: ${error.message}` }],
2068
- isError: true,
2456
+ content: [{ type: "text", text: output }]
2069
2457
  };
2070
- }
2071
- }
2072
-
2073
- async function handleGetDefinition({ path: filePath, symbol }) {
2074
- if (!filePath || !symbol) throw new Error("Missing path or symbol");
2075
-
2076
- const absolutePath = path.resolve(filePath);
2077
-
2078
- // Check if path is a directory - search across files in directory
2079
- let stats;
2080
- try {
2081
- stats = await fs.stat(absolutePath);
2082
- } catch (e) {
2083
- throw new Error(`Path does not exist: ${filePath}`);
2084
- }
2085
-
2086
- if (!stats.isFile()) {
2087
- // It's a directory - search for symbol definition using grep
2088
- return await handleDefinitionInDirectory({ path: absolutePath, symbol });
2089
- }
2090
2458
 
2091
- // It's a file - use tree-sitter to find definition
2092
- const content = await fs.readFile(filePath, 'utf8');
2093
- const definitions = await findDefinition(content, filePath, symbol);
2459
+ } catch (error) {
2460
+ // Error recovery: suggest using depth parameter
2461
+ let errorMessage = `āŒ Error listing files: ${error.message}`;
2462
+ if (error.message.includes('too many files') || error.message.includes('EMFILE') || error.message.includes('max')) {
2463
+ errorMessage += `\n\nšŸ’” Tip: Try reducing the depth (e.g., depth: 2 or 3) to limit the search scope.`;
2464
+ }
2094
2465
 
2095
- if (definitions.length === 0) {
2466
+ reportAudit({
2467
+ tool: 'list_files_fast',
2468
+ instruction: dirPath,
2469
+ status: 'error',
2470
+ error_message: error.message,
2471
+ latency_ms: Date.now() - start
2472
+ });
2096
2473
  return {
2097
- content: [{ type: "text", text: `No definition found for '${symbol}' in ${filePath}` }]
2474
+ content: [{ type: "text", text: errorMessage }],
2475
+ isError: true
2098
2476
  };
2099
2477
  }
2100
-
2101
- const def = definitions[0]; // Take first
2102
-
2103
- return {
2104
- content: [{ type: "text", text: `Definition of '${symbol}':\n${filePath}:${def.line}:${def.column} (${def.type || 'unknown'})` }]
2105
- };
2106
2478
  }
2107
2479
 
2480
+ /**
2481
+ * UNIFIED HANDLER 11: handleDefinitionInDirectory (v2.0)
2482
+ * Renamed from handleDefinitionInDirectory for consistency
2483
+ * Now supports both single file and batch search modes
2484
+ */
2108
2485
  async function handleDefinitionInDirectory({ path: dirPath, symbol }) {
2109
2486
  // Search for symbol definition in directory using grep
2110
2487
  // Look for common definition patterns: const/let/var, function, class, interface, type, etc.
@@ -2168,44 +2545,11 @@ async function handleDefinitionInDirectory({ path: dirPath, symbol }) {
2168
2545
  };
2169
2546
  }
2170
2547
 
2171
- async function handleFindReferences({ path: filePath, symbol }) {
2172
- if (!filePath || !symbol) throw new Error("Missing path or symbol");
2173
-
2174
- const absolutePath = path.resolve(filePath);
2175
-
2176
- // Check if path is a directory - search across files in directory
2177
- let stats;
2178
- try {
2179
- stats = await fs.stat(absolutePath);
2180
- } catch (e) {
2181
- throw new Error(`Path does not exist: ${filePath}`);
2182
- }
2183
-
2184
- if (!stats.isFile()) {
2185
- // It's a directory - search for references using grep
2186
- return await handleReferencesInDirectory({ path: absolutePath, symbol });
2187
- }
2188
-
2189
- // It's a file - use tree-sitter to find references
2190
- const content = await fs.readFile(filePath, 'utf8');
2191
- const references = await findReferences(content, filePath, symbol);
2192
-
2193
- if (references.length === 0) {
2194
- return {
2195
- content: [{ type: "text", text: `No references found for '${symbol}' in ${filePath}` }]
2196
- };
2197
- }
2198
-
2199
- let output = `References for '${symbol}' in ${filePath} (${references.length}):\n`;
2200
- references.forEach(ref => {
2201
- output += `${ref.line}:${ref.column}\n`;
2202
- });
2203
-
2204
- return {
2205
- content: [{ type: "text", text: output }]
2206
- };
2207
- }
2208
-
2548
+ /**
2549
+ * UNIFIED HANDLER 12: handleReferencesInDirectory (v2.0)
2550
+ * Renamed from handleReferencesInDirectory for consistency
2551
+ * Now supports both single file and batch search modes
2552
+ */
2209
2553
  async function handleReferencesInDirectory({ path: dirPath, symbol }) {
2210
2554
  // Search for symbol references in directory using grep
2211
2555
  const flags = "-rn";