@mrxkun/mcfast-mcp 2.1.0 → 2.1.2

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": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Ultra-fast code editing with fuzzy patching, auto-rollback, and 5 unified tools.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -518,11 +518,23 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
518
518
  });
519
519
 
520
520
  if (!editResult.success) {
521
- // Rollback occurred
521
+ // Determine error type based on result
522
+ let errorType = 'generic';
523
+
524
+ if (editResult.error.includes('ENOENT')) {
525
+ errorType = 'file_not_found';
526
+ } else if (editResult.error.includes('EACCES')) {
527
+ errorType = 'permission_denied';
528
+ } else if (editResult.error.includes('Validation failed')) {
529
+ errorType = 'syntax_error';
530
+ } else if (editResult.error.includes('Failed to apply hunk') || editResult.error.includes('Could not find matching location')) {
531
+ errorType = 'fuzzy_match_failed';
532
+ }
533
+
522
534
  return {
523
535
  content: [{
524
536
  type: "text",
525
- text: formatError('syntax_error', {
537
+ text: formatError(errorType, {
526
538
  filePath,
527
539
  error: editResult.error,
528
540
  backupPath: editResult.backupPath
@@ -6,27 +6,49 @@
6
6
  /**
7
7
  * Calculate Levenshtein distance between two strings
8
8
  * Used for fuzzy matching to find best location for patch
9
+ * Optimized with early termination and length-based shortcuts
9
10
  */
10
- function levenshteinDistance(str1, str2) {
11
+ function levenshteinDistance(str1, str2, maxDistance = Infinity) {
11
12
  const len1 = str1.length;
12
13
  const len2 = str2.length;
13
- const matrix = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0));
14
14
 
15
- for (let i = 0; i <= len1; i++) matrix[i][0] = i;
16
- for (let j = 0; j <= len2; j++) matrix[0][j] = j;
15
+ // Quick optimization: if length difference exceeds maxDistance, return early
16
+ if (Math.abs(len1 - len2) > maxDistance) {
17
+ return maxDistance + 1;
18
+ }
19
+
20
+ // Optimization: swap to ensure str1 is shorter (reduces memory)
21
+ if (len1 > len2) {
22
+ return levenshteinDistance(str2, str1, maxDistance);
23
+ }
24
+
25
+ // Use single array instead of matrix (space optimization)
26
+ let prevRow = Array(len2 + 1).fill(0).map((_, i) => i);
17
27
 
18
28
  for (let i = 1; i <= len1; i++) {
29
+ let currentRow = [i];
30
+ let minInRow = i;
31
+
19
32
  for (let j = 1; j <= len2; j++) {
20
33
  const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
21
- matrix[i][j] = Math.min(
22
- matrix[i - 1][j] + 1, // deletion
23
- matrix[i][j - 1] + 1, // insertion
24
- matrix[i - 1][j - 1] + cost // substitution
34
+ const val = Math.min(
35
+ prevRow[j] + 1, // deletion
36
+ currentRow[j - 1] + 1, // insertion
37
+ prevRow[j - 1] + cost // substitution
25
38
  );
39
+ currentRow.push(val);
40
+ minInRow = Math.min(minInRow, val);
26
41
  }
42
+
43
+ // Early termination: if minimum in row exceeds maxDistance, abort
44
+ if (minInRow > maxDistance) {
45
+ return maxDistance + 1;
46
+ }
47
+
48
+ prevRow = currentRow;
27
49
  }
28
50
 
29
- return matrix[len1][len2];
51
+ return prevRow[len2];
30
52
  }
31
53
 
32
54
  /**
@@ -103,16 +125,40 @@ export function parseDiff(diffText) {
103
125
  * Returns { index, score, lineNumber } or null if no good match
104
126
  */
105
127
  export function findBestMatch(targetLines, patternLines, threshold = 0.8) {
128
+ // Input validation
129
+ if (!targetLines || !patternLines || patternLines.length === 0) {
130
+ return null;
131
+ }
132
+
133
+ if (targetLines.length < patternLines.length) {
134
+ return null;
135
+ }
136
+
137
+ // Performance limit: skip if pattern is too large (>500 lines)
138
+ if (patternLines.length > 500) {
139
+ return null;
140
+ }
141
+
106
142
  let bestMatch = null;
107
143
  let bestScore = 0;
144
+ const maxIterations = Math.min(targetLines.length - patternLines.length + 1, 10000);
108
145
 
109
- // Sliding window search
110
- for (let i = 0; i <= targetLines.length - patternLines.length; i++) {
146
+ // Sliding window search with iteration limit
147
+ for (let i = 0; i < maxIterations; i++) {
111
148
  const window = targetLines.slice(i, i + patternLines.length);
112
149
  const windowText = window.join('\n');
113
150
  const patternText = patternLines.join('\n');
114
151
 
115
- const score = similarityScore(windowText, patternText);
152
+ // Calculate max acceptable distance based on threshold
153
+ const maxLen = Math.max(windowText.length, patternText.length);
154
+ const maxDistance = Math.ceil(maxLen * (1 - threshold));
155
+
156
+ const distance = levenshteinDistance(windowText, patternText, maxDistance);
157
+
158
+ // Skip if distance exceeds threshold
159
+ if (distance > maxDistance) continue;
160
+
161
+ const score = 1.0 - (distance / maxLen);
116
162
 
117
163
  if (score > bestScore && score >= threshold) {
118
164
  bestScore = score;
@@ -121,6 +167,11 @@ export function findBestMatch(targetLines, patternLines, threshold = 0.8) {
121
167
  score: score,
122
168
  lineNumber: i + 1
123
169
  };
170
+
171
+ // Early exit if perfect match found
172
+ if (score >= 0.99) {
173
+ break;
174
+ }
124
175
  }
125
176
  }
126
177
 
@@ -183,7 +234,34 @@ export function applyHunk(fileContent, hunk, threshold = 0.8) {
183
234
  * Main entry point for fuzzy patching
184
235
  */
185
236
  export function applyFuzzyPatch(fileContent, diffText, options = {}) {
237
+ // Input validation
238
+ if (!fileContent || typeof fileContent !== 'string') {
239
+ return {
240
+ success: false,
241
+ content: fileContent || '',
242
+ message: 'Invalid file content provided'
243
+ };
244
+ }
245
+
246
+ if (!diffText || typeof diffText !== 'string') {
247
+ return {
248
+ success: false,
249
+ content: fileContent,
250
+ message: 'Invalid diff text provided'
251
+ };
252
+ }
253
+
186
254
  const threshold = options.threshold || parseFloat(process.env.MCFAST_FUZZY_THRESHOLD || '0.8');
255
+
256
+ // Validate threshold range
257
+ if (threshold < 0 || threshold > 1) {
258
+ return {
259
+ success: false,
260
+ content: fileContent,
261
+ message: 'Threshold must be between 0 and 1'
262
+ };
263
+ }
264
+
187
265
  const hunks = parseDiff(diffText);
188
266
 
189
267
  if (hunks.length === 0) {
@@ -70,7 +70,9 @@ async function cleanupOldBackups(fileName) {
70
70
  */
71
71
  export async function validateJavaScript(filePath) {
72
72
  try {
73
- const { stderr } = await execAsync(`node --check "${filePath}"`, {
73
+ // Escape file path for shell command
74
+ const escapedPath = filePath.replace(/"/g, '\\"');
75
+ const { stderr } = await execAsync(`node --check "${escapedPath}"`, {
74
76
  timeout: 5000
75
77
  });
76
78
 
@@ -89,7 +91,9 @@ export async function validateJavaScript(filePath) {
89
91
  */
90
92
  export async function validatePython(filePath) {
91
93
  try {
92
- await execAsync(`python -m py_compile "${filePath}"`, {
94
+ // Escape file path for shell command
95
+ const escapedPath = filePath.replace(/"/g, '\\"');
96
+ await execAsync(`python -m py_compile "${escapedPath}"`, {
93
97
  timeout: 5000
94
98
  });
95
99
  return { valid: true };