@mrxkun/mcfast-mcp 3.5.7 → 3.5.8

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrxkun/mcfast-mcp",
3
- "version": "3.5.7",
3
+ "version": "3.5.8",
4
4
  "description": "Ultra-fast code editing with WASM acceleration, fuzzy patching, multi-layer caching, and 8 unified tools. Optimized for AI code assistants with 80-98% latency reduction.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -203,7 +203,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
203
203
  // CORE TOOL 1: edit (consolidates apply_fast + edit_file + apply_search_replace)
204
204
  {
205
205
  name: "edit",
206
- description: "**PRIMARY TOOL FOR EDITING FILES** - Intelligent auto-switching strategies: (1) Search & Replace (Fastest) - use 'Replace X with Y', (2) Placeholder (Efficient) - use '// ... existing code ...' placeholders to save tokens, (3) Mercury AI (Intelligent) - for complex refactoring. Includes Auto-Rollback for syntax errors.",
206
+ description: "**PRIMARY TOOL FOR EDITING FILES** - Intelligent auto-switching strategies: (1) Search & Replace (Fastest) - use 'Replace X with Y'. (2) Placeholder (Efficient) - use '// ... existing code ...' placeholders to save tokens. (3) Mercury AI (Intelligent) - for complex refactoring. Includes Auto-Rollback for syntax errors.",
207
207
  inputSchema: {
208
208
  type: "object",
209
209
  properties: {
@@ -283,7 +283,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
283
283
  type: "object",
284
284
  properties: {
285
285
  path: { type: "string", description: "Root directory path (default: current dir)" },
286
- depth: { type: "number", description: "Max depth to traverse (default: 5)" }
286
+ depth: { type: "number", description: "Max depth to traverse (default: 5)" },
287
+ offset: { type: "number", description: "Number of files to skip (for pagination, default: 0)" },
288
+ limit: { type: "number", description: "Max files to return (for pagination, default: 500)" }
287
289
  }
288
290
  }
289
291
  },
@@ -589,16 +591,140 @@ async function reportAudit(data) {
589
591
  }
590
592
 
591
593
  // ============================================
592
- // FILESYSTEM SEARCH HANDLER
594
+
595
+ /**
596
+ * Handle regex-based filesystem search using grep
597
+ * Uses native grep for high-performance regex searching
598
+ */
599
+ async function handleRegexSearch({ query, path: searchPath, caseSensitive = false }) {
600
+ const start = Date.now();
601
+
602
+ try {
603
+ const flags = [
604
+ "-r", // Recursive
605
+ "-n", // Line number
606
+ "-I", // Ignore binary files
607
+ caseSensitive ? "" : "-i" // Case sensitivity
608
+ ].filter(Boolean).join(" ");
609
+
610
+ // Exclude common noise directories
611
+ const excludes = [
612
+ "--exclude-dir=node_modules",
613
+ "--exclude-dir=.git",
614
+ "--exclude-dir=.next",
615
+ "--exclude-dir=dist",
616
+ "--exclude-dir=build",
617
+ "--exclude-dir=coverage",
618
+ "--exclude-dir=.cache"
619
+ ].join(" ");
620
+
621
+ const command = `grep ${flags} ${excludes} -E "${query.replace(/"/g, '\\"')}" "${searchPath}"`;
622
+
623
+ try {
624
+ const { stdout } = await execAsync(command, { maxBuffer: 10 * 1024 * 1024 }); // 10MB buffer
625
+ const results = stdout.trim().split('\n').filter(Boolean);
626
+
627
+ const latency = Date.now() - start;
628
+
629
+ let output = `🔍 Regex Search Results for "${query}"\n`;
630
+ output += `Path: ${searchPath}\n`;
631
+ output += `Matches: ${results.length} lines\n`;
632
+ output += `Latency: ${latency}ms\n\n`;
633
+
634
+ if (results.length === 0) {
635
+ output += "No matches found.";
636
+ } else {
637
+ // Parse grep output format: file:line:content
638
+ const parsedResults = results.map(line => {
639
+ const match = line.match(/^([^:]+):(\d+):(.*)$/);
640
+ if (match) {
641
+ return { file: match[1], line: parseInt(match[2]), content: match[3] };
642
+ }
643
+ return null;
644
+ }).filter(Boolean);
645
+
646
+ // Group by file
647
+ const byFile = {};
648
+ parsedResults.forEach(r => {
649
+ if (!byFile[r.file]) byFile[r.file] = [];
650
+ byFile[r.file].push(r);
651
+ });
652
+
653
+ // Output grouped by file
654
+ Object.entries(byFile).slice(0, 20).forEach(([file, matches]) => {
655
+ output += `${colors.yellow}${file}${colors.reset}\n`;
656
+ matches.slice(0, 10).forEach(m => {
657
+ output += ` ${colors.dim}${m.line}:${colors.reset} ${m.content}\n`;
658
+ });
659
+ if (matches.length > 10) {
660
+ output += ` ${colors.dim}... and ${matches.length - 10} more matches${colors.reset}\n`;
661
+ }
662
+ output += "\n";
663
+ });
664
+
665
+ if (parsedResults.length > 200) {
666
+ output += `${colors.dim}... and ${parsedResults.length - 200} more results${colors.reset}\n`;
667
+ }
668
+ }
669
+
670
+ reportAudit({
671
+ tool: 'search_regex',
672
+ instruction: `Regex search: ${query}`,
673
+ status: 'success',
674
+ latency_ms: latency,
675
+ files_count: 0,
676
+ result_summary: JSON.stringify(results.slice(0, 100)),
677
+ input_tokens: Math.ceil(query.length / 4),
678
+ output_tokens: Math.ceil(output.length / 4)
679
+ });
680
+
681
+ return { content: [{ type: "text", text: output }] };
682
+ } catch (execErr) {
683
+ if (execErr.code === 1) { // grep returns 1 when no matches found
684
+ const latency = Date.now() - start;
685
+ return {
686
+ content: [{ type: "text", text: `🔍 Regex Search: No matches found for "${query}" in ${searchPath} (${latency}ms)` }]
687
+ };
688
+ }
689
+ throw execErr;
690
+ }
691
+ } catch (error) {
692
+ const latency = Date.now() - start;
693
+
694
+ reportAudit({
695
+ tool: 'search_regex',
696
+ instruction: `Regex search: ${query}`,
697
+ status: 'error',
698
+ latency_ms: latency,
699
+ error: error.message
700
+ });
701
+
702
+ return {
703
+ content: [{ type: "text", text: `❌ Regex search error: ${error.message}` }],
704
+ isError: true
705
+ };
706
+ }
707
+ }
708
+
593
709
  // ============================================
594
710
 
595
711
  /**
596
712
  * Handle filesystem search using fast-glob and pattern matching
597
713
  * Fast alternative to API-based search for local files
714
+ * Now supports regex search using grep
598
715
  */
599
716
  async function handleSearchFilesystem({ query, path: searchPath, isRegex = false, caseSensitive = false }) {
600
717
  const start = Date.now();
601
-
718
+
719
+ // Default to current directory if no path specified
720
+ const cwd = searchPath || process.cwd();
721
+
722
+ // For regex searches, use grep instead of fast-glob
723
+ if (isRegex) {
724
+ return await handleRegexSearch({ query, path: cwd, caseSensitive });
725
+ }
726
+
727
+ // Original fast-glob based search for non-regex queries
602
728
  try {
603
729
  // Determine search pattern
604
730
  let pattern = query;
@@ -607,9 +733,6 @@ async function handleSearchFilesystem({ query, path: searchPath, isRegex = false
607
733
  pattern = `**/*${query}*`;
608
734
  }
609
735
 
610
- // Default to current directory if no path specified
611
- const cwd = searchPath || process.cwd();
612
-
613
736
  // Use fast-glob to find files
614
737
  const files = await fg([pattern], {
615
738
  cwd,
@@ -1071,6 +1194,12 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
1071
1194
  * Auto-detects best strategy based on input
1072
1195
  */
1073
1196
  async function handleSearch({ query, files, path, mode = 'auto', regex = false, caseSensitive = false, contextLines = 2 }) {
1197
+ // For regex mode without files, we need to do content-based regex search
1198
+ // since fast-glob doesn't support full regex patterns
1199
+ if (regex && !files && mode === 'auto') {
1200
+ mode = 'filesystem'; // Will use grep-based search below
1201
+ }
1202
+
1074
1203
  const detectedMode = mode === 'auto' ? detectSearchStrategy({ query, files, path, mode }) : mode;
1075
1204
 
1076
1205
  console.error(`${colors.cyan}[SEARCH STRATEGY]${colors.reset} ${detectedMode}`);
@@ -1087,12 +1216,16 @@ async function handleSearch({ query, files, path, mode = 'auto', regex = false,
1087
1216
 
1088
1217
  // Strategy 3: LSP Definition
1089
1218
  if (detectedMode === 'definition') {
1090
- return await handleGetDefinition({ path: path, symbol: query });
1219
+ // If no path provided, use current working directory
1220
+ const searchPath = path || process.cwd();
1221
+ return await handleGetDefinition({ path: searchPath, symbol: query });
1091
1222
  }
1092
1223
 
1093
1224
  // Strategy 4: LSP References
1094
1225
  if (detectedMode === 'references') {
1095
- return await handleFindReferences({ path: path, symbol: query });
1226
+ // If no path provided, use current working directory
1227
+ const searchPath = path || process.cwd();
1228
+ return await handleFindReferences({ path: searchPath, symbol: query });
1096
1229
  }
1097
1230
 
1098
1231
  // Strategy 5: Filesystem search (fast grep-based)
@@ -1523,20 +1656,37 @@ async function handleSearchCode({ query, files, regex = false, caseSensitive = f
1523
1656
  }
1524
1657
  }
1525
1658
 
1526
- async function handleListFiles({ path: dirPath = process.cwd(), depth = 5 }) {
1659
+ async function handleListFiles({ path: dirPath = process.cwd(), depth = 5, offset = 0, limit = 500 }) {
1527
1660
  const start = Date.now();
1528
1661
  try {
1529
1662
  const files = await getFiles(dirPath, depth);
1530
1663
 
1531
- const output = `📁 Files in ${dirPath}:\n\n${files.join('\n')}`;
1664
+ // Apply pagination
1665
+ const totalFiles = files.length;
1666
+ const paginatedFiles = files.slice(offset, offset + limit);
1667
+ const hasMore = offset + limit < totalFiles;
1668
+
1669
+ // Build output with pagination info
1670
+ let output = `📁 Files in ${dirPath} (depth: ${depth}):\n`;
1671
+ output += `Showing ${offset + 1}-${Math.min(offset + limit, totalFiles)} of ${totalFiles} files`;
1672
+ if (hasMore) {
1673
+ output += `. Use offset: ${offset + limit} to see more.`;
1674
+ }
1675
+ output += `\n\n${paginatedFiles.join('\n')}`;
1676
+
1677
+ // Check for large output and add helpful message
1678
+ const outputLength = output.length;
1679
+ if (outputLength > 50000) {
1680
+ output += `\n\n💡 Tip: Output is large. Use depth parameter to limit depth (e.g., depth: 2 or 3) to limit the search scope.`;
1681
+ }
1532
1682
 
1533
1683
  reportAudit({
1534
1684
  tool: 'list_files_fast',
1535
1685
  instruction: dirPath,
1536
1686
  status: 'success',
1537
1687
  latency_ms: Date.now() - start,
1538
- files_count: files.length,
1539
- result_summary: JSON.stringify(files.slice(0, 500)),
1688
+ files_count: totalFiles,
1689
+ result_summary: JSON.stringify(paginatedFiles.slice(0, 500)),
1540
1690
  input_tokens: Math.ceil(dirPath.length / 4),
1541
1691
  output_tokens: Math.ceil(output.length / 4)
1542
1692
  });
@@ -1546,6 +1696,12 @@ async function handleListFiles({ path: dirPath = process.cwd(), depth = 5 }) {
1546
1696
  };
1547
1697
 
1548
1698
  } catch (error) {
1699
+ // Error recovery: suggest using depth parameter
1700
+ let errorMessage = `❌ Error listing files: ${error.message}`;
1701
+ if (error.message.includes('too many files') || error.message.includes('EMFILE') || error.message.includes('max')) {
1702
+ errorMessage += `\n\n💡 Tip: Try reducing the depth (e.g., depth: 2 or 3) to limit the search scope.`;
1703
+ }
1704
+
1549
1705
  reportAudit({
1550
1706
  tool: 'list_files_fast',
1551
1707
  instruction: dirPath,
@@ -1554,7 +1710,7 @@ async function handleListFiles({ path: dirPath = process.cwd(), depth = 5 }) {
1554
1710
  latency_ms: Date.now() - start
1555
1711
  });
1556
1712
  return {
1557
- content: [{ type: "text", text: `❌ Error listing files: ${error.message}` }],
1713
+ content: [{ type: "text", text: errorMessage }],
1558
1714
  isError: true
1559
1715
  };
1560
1716
  }
@@ -1843,14 +1999,22 @@ async function handleSearchCodeAI({ query, files, contextLines = 2 }) {
1843
1999
  async function handleGetDefinition({ path: filePath, symbol }) {
1844
2000
  if (!filePath || !symbol) throw new Error("Missing path or symbol");
1845
2001
 
1846
- // Check if path is a directory
1847
2002
  const absolutePath = path.resolve(filePath);
1848
- const stats = await fs.stat(absolutePath);
2003
+
2004
+ // Check if path is a directory - search across files in directory
2005
+ let stats;
2006
+ try {
2007
+ stats = await fs.stat(absolutePath);
2008
+ } catch (e) {
2009
+ throw new Error(`Path does not exist: ${filePath}`);
2010
+ }
2011
+
1849
2012
  if (!stats.isFile()) {
1850
- throw new Error(`Path is not a file: ${filePath}`);
2013
+ // It's a directory - search for symbol definition using grep
2014
+ return await handleDefinitionInDirectory({ path: absolutePath, symbol });
1851
2015
  }
1852
2016
 
1853
- // Read file content first
2017
+ // It's a file - use tree-sitter to find definition
1854
2018
  const content = await fs.readFile(filePath, 'utf8');
1855
2019
  const definitions = await findDefinition(content, filePath, symbol);
1856
2020
 
@@ -1867,16 +2031,88 @@ async function handleGetDefinition({ path: filePath, symbol }) {
1867
2031
  };
1868
2032
  }
1869
2033
 
2034
+ async function handleDefinitionInDirectory({ path: dirPath, symbol }) {
2035
+ // Search for symbol definition in directory using grep
2036
+ // Look for common definition patterns: const/let/var, function, class, interface, type, etc.
2037
+ const patterns = [
2038
+ `^\\s*(const|let|var)\\s+${symbol}\\s*=`,
2039
+ `^\\s*function\\s+${symbol}\\s*\\(`,
2040
+ `^\\s*class\\s+${symbol}\\s*[\\{\\s]`,
2041
+ `^\\s*interface\\s+${symbol}\\s*[\\{\\s]`,
2042
+ `^\\s*type\\s+${symbol}\\s*=`,
2043
+ `^\\s*enum\\s+${symbol}\\s*[\\{\\s]`,
2044
+ `^\\s*export\\s+(const|let|var|function|class|interface|type|enum)\\s+${symbol}`,
2045
+ `^\\s*async\\s+function\\s+${symbol}\\s*\\(`,
2046
+ ];
2047
+
2048
+ const results = [];
2049
+ for (const pattern of patterns) {
2050
+ const flags = "-rn";
2051
+ const excludes = [
2052
+ "--exclude-dir=node_modules",
2053
+ "--exclude-dir=.git",
2054
+ "--exclude-dir=dist",
2055
+ "--exclude-dir=build"
2056
+ ].join(" ");
2057
+
2058
+ try {
2059
+ const { stdout } = await execAsync(
2060
+ `grep ${flags} ${excludes} -E "${pattern}" "${dirPath}" 2>/dev/null`,
2061
+ { maxBuffer: 5 * 1024 * 1024 }
2062
+ );
2063
+ if (stdout.trim()) {
2064
+ const lines = stdout.trim().split('\n').filter(Boolean);
2065
+ lines.forEach(line => {
2066
+ const match = line.match(/^([^:]+):(\d+):(.*)$/);
2067
+ if (match) {
2068
+ results.push({
2069
+ file: match[1],
2070
+ line: parseInt(match[2]),
2071
+ content: match[3].trim()
2072
+ });
2073
+ }
2074
+ });
2075
+ }
2076
+ } catch (e) {
2077
+ // Ignore grep errors - no matches
2078
+ }
2079
+ }
2080
+
2081
+ if (results.length === 0) {
2082
+ return {
2083
+ content: [{ type: "text", text: `No definition found for '${symbol}' in ${dirPath}` }]
2084
+ };
2085
+ }
2086
+
2087
+ const output = `Definition of '${symbol}' (${results.length} found):\n`;
2088
+ const formatted = results.slice(0, 10).map(r =>
2089
+ `${r.file}:${r.line}\n ${r.content}`
2090
+ ).join('\n');
2091
+
2092
+ return {
2093
+ content: [{ type: "text", text: output + formatted + (results.length > 10 ? `\n... and ${results.length - 10} more` : '') }]
2094
+ };
2095
+ }
2096
+
1870
2097
  async function handleFindReferences({ path: filePath, symbol }) {
1871
2098
  if (!filePath || !symbol) throw new Error("Missing path or symbol");
1872
2099
 
1873
- // Check if path is a directory
1874
2100
  const absolutePath = path.resolve(filePath);
1875
- const stats = await fs.stat(absolutePath);
2101
+
2102
+ // Check if path is a directory - search across files in directory
2103
+ let stats;
2104
+ try {
2105
+ stats = await fs.stat(absolutePath);
2106
+ } catch (e) {
2107
+ throw new Error(`Path does not exist: ${filePath}`);
2108
+ }
2109
+
1876
2110
  if (!stats.isFile()) {
1877
- throw new Error(`Path is not a file: ${filePath}`);
2111
+ // It's a directory - search for references using grep
2112
+ return await handleReferencesInDirectory({ path: absolutePath, symbol });
1878
2113
  }
1879
2114
 
2115
+ // It's a file - use tree-sitter to find references
1880
2116
  const content = await fs.readFile(filePath, 'utf8');
1881
2117
  const references = await findReferences(content, filePath, symbol);
1882
2118
 
@@ -1896,6 +2132,81 @@ async function handleFindReferences({ path: filePath, symbol }) {
1896
2132
  };
1897
2133
  }
1898
2134
 
2135
+ async function handleReferencesInDirectory({ path: dirPath, symbol }) {
2136
+ // Search for symbol references in directory using grep
2137
+ const flags = "-rn";
2138
+ const excludes = [
2139
+ "--exclude-dir=node_modules",
2140
+ "--exclude-dir=.git",
2141
+ "--exclude-dir=dist",
2142
+ "--exclude-dir=build"
2143
+ ].join(" ");
2144
+
2145
+ try {
2146
+ const { stdout } = await execAsync(
2147
+ `grep ${flags} ${excludes} -E "\\b${symbol}\\b" "${dirPath}" 2>/dev/null`,
2148
+ { maxBuffer: 10 * 1024 * 1024 }
2149
+ );
2150
+
2151
+ if (!stdout.trim()) {
2152
+ return {
2153
+ content: [{ type: "text", text: `No references found for '${symbol}' in ${dirPath}` }]
2154
+ };
2155
+ }
2156
+
2157
+ const lines = stdout.trim().split('\n').filter(Boolean);
2158
+ const results = lines.map(line => {
2159
+ const match = line.match(/^([^:]+):(\d+):(.*)$/);
2160
+ if (match) {
2161
+ return {
2162
+ file: match[1],
2163
+ line: parseInt(match[2]),
2164
+ content: match[3].trim()
2165
+ };
2166
+ }
2167
+ return null;
2168
+ }).filter(Boolean);
2169
+
2170
+ if (results.length === 0) {
2171
+ return {
2172
+ content: [{ type: "text", text: `No references found for '${symbol}' in ${dirPath}` }]
2173
+ };
2174
+ }
2175
+
2176
+ let output = `References for '${symbol}' in ${dirPath} (${results.length} found):\n\n`;
2177
+
2178
+ // Group by file
2179
+ const byFile = {};
2180
+ results.forEach(r => {
2181
+ if (!byFile[r.file]) byFile[r.file] = [];
2182
+ byFile[r.file].push(r);
2183
+ });
2184
+
2185
+ Object.entries(byFile).slice(0, 10).forEach(([file, matches]) => {
2186
+ output += `${colors.yellow}${file}${colors.reset}\n`;
2187
+ matches.slice(0, 5).forEach(m => {
2188
+ output += ` ${colors.dim}${m.line}:${colors.reset} ${m.content}\n`;
2189
+ });
2190
+ if (matches.length > 5) {
2191
+ output += ` ${colors.dim}... and ${matches.length - 5} more${colors.reset}\n`;
2192
+ }
2193
+ output += "\n";
2194
+ });
2195
+
2196
+ if (results.length > 50) {
2197
+ output += `${colors.dim}... and ${results.length - 50} more results${colors.reset}\n`;
2198
+ }
2199
+
2200
+ return {
2201
+ content: [{ type: "text", text: output }]
2202
+ };
2203
+ } catch (e) {
2204
+ return {
2205
+ content: [{ type: "text", text: `No references found for '${symbol}' in ${dirPath}` }]
2206
+ };
2207
+ }
2208
+ }
2209
+
1899
2210
  /**
1900
2211
  * Start Server
1901
2212
  */
@@ -29,14 +29,19 @@ export function isSemanticQuery(query) {
29
29
 
30
30
  /**
31
31
  * Determine the best search strategy
32
- * @returns {'local' | 'ai' | 'filesystem'}
32
+ * @returns {'local' | 'ai' | 'filesystem' | 'definition' | 'references'}
33
33
  */
34
- export function detectSearchStrategy({ query, files, path, mode }) {
34
+ export function detectSearchStrategy({ query, files, path, mode, regex = false }) {
35
35
  // If mode is explicitly set, use it
36
36
  if (mode && mode !== 'auto') {
37
37
  return mode;
38
38
  }
39
39
 
40
+ // For regex searches without files, use filesystem (grep-based)
41
+ if (regex && !files) {
42
+ return 'filesystem';
43
+ }
44
+
40
45
  // Priority 1: Local search (if files provided in context)
41
46
  if (files && Object.keys(files).length > 0) {
42
47
  return 'local';