@mrxkun/mcfast-mcp 2.1.2 → 2.2.0
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 +5 -1
- package/src/index.js +89 -1
- package/src/strategies/ast-detector.js +288 -0
- package/src/strategies/edit-strategy.js +10 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrxkun/mcfast-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Ultra-fast code editing with fuzzy patching, auto-rollback, and 5 unified tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"url": "git+https://github.com/ndpmmo/mcfast.git"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@babel/generator": "^7.29.1",
|
|
31
|
+
"@babel/parser": "^7.29.0",
|
|
32
|
+
"@babel/traverse": "^7.29.0",
|
|
33
|
+
"@babel/types": "^7.29.0",
|
|
30
34
|
"@modelcontextprotocol/sdk": "^0.6.0",
|
|
31
35
|
"fast-glob": "^3.3.3",
|
|
32
36
|
"ignore": "^7.0.5"
|
package/src/index.js
CHANGED
|
@@ -16,6 +16,13 @@ import fg from "fast-glob";
|
|
|
16
16
|
import { detectEditStrategy, extractSearchReplace } from './strategies/edit-strategy.js';
|
|
17
17
|
import { detectSearchStrategy } from './strategies/search-strategy.js';
|
|
18
18
|
import { applyFuzzyPatch } from './strategies/fuzzy-patch.js';
|
|
19
|
+
import {
|
|
20
|
+
detectRefactoringPattern,
|
|
21
|
+
parseCode,
|
|
22
|
+
renameIdentifier,
|
|
23
|
+
inlineVariable,
|
|
24
|
+
applyASTTransformation
|
|
25
|
+
} from './strategies/ast-detector.js';
|
|
19
26
|
import { safeEdit } from './utils/backup.js';
|
|
20
27
|
import { formatError } from './utils/error-formatter.js';
|
|
21
28
|
|
|
@@ -565,7 +572,88 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
565
572
|
}
|
|
566
573
|
}
|
|
567
574
|
|
|
568
|
-
// Strategy 2:
|
|
575
|
+
// Strategy 2: AST Refactor (symbol-aware operations)
|
|
576
|
+
if (strategy === 'ast_refactor') {
|
|
577
|
+
const filePath = Object.keys(files)[0];
|
|
578
|
+
const fileContent = files[filePath];
|
|
579
|
+
const pattern = detectRefactoringPattern(instruction);
|
|
580
|
+
|
|
581
|
+
if (!pattern) {
|
|
582
|
+
return {
|
|
583
|
+
content: [{
|
|
584
|
+
type: "text",
|
|
585
|
+
text: formatError('generic', {
|
|
586
|
+
error: 'Could not detect refactoring pattern from instruction'
|
|
587
|
+
})
|
|
588
|
+
}],
|
|
589
|
+
isError: true
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
let transformResult;
|
|
595
|
+
const editResult = await safeEdit(filePath, async () => {
|
|
596
|
+
// Apply AST transformation based on pattern type
|
|
597
|
+
if (pattern.type.startsWith('rename')) {
|
|
598
|
+
transformResult = applyASTTransformation(fileContent, filePath, (ast) => {
|
|
599
|
+
return renameIdentifier(ast, pattern.oldName, pattern.newName);
|
|
600
|
+
});
|
|
601
|
+
} else if (pattern.type.startsWith('inline')) {
|
|
602
|
+
transformResult = applyASTTransformation(fileContent, filePath, (ast) => {
|
|
603
|
+
return inlineVariable(ast, pattern.oldName);
|
|
604
|
+
});
|
|
605
|
+
} else {
|
|
606
|
+
throw new Error(`Unsupported refactoring type: ${pattern.type}`);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (!transformResult.success) {
|
|
610
|
+
throw new Error(transformResult.message);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Write transformed code
|
|
614
|
+
await fs.writeFile(filePath, transformResult.code, 'utf8');
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
if (!editResult.success) {
|
|
618
|
+
let errorType = 'generic';
|
|
619
|
+
if (editResult.error.includes('ENOENT')) errorType = 'file_not_found';
|
|
620
|
+
else if (editResult.error.includes('EACCES')) errorType = 'permission_denied';
|
|
621
|
+
else if (editResult.error.includes('Validation failed')) errorType = 'syntax_error';
|
|
622
|
+
|
|
623
|
+
return {
|
|
624
|
+
content: [{
|
|
625
|
+
type: "text",
|
|
626
|
+
text: formatError(errorType, {
|
|
627
|
+
filePath,
|
|
628
|
+
error: editResult.error,
|
|
629
|
+
backupPath: editResult.backupPath
|
|
630
|
+
})
|
|
631
|
+
}],
|
|
632
|
+
isError: true
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
content: [{
|
|
638
|
+
type: "text",
|
|
639
|
+
text: `✅ AST Refactor Applied Successfully\n\nOperation: ${pattern.type}\nChanges: ${transformResult.count || transformResult.replacements || 0} locations\n\nBackup: ${editResult.backupPath}`
|
|
640
|
+
}]
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
} catch (error) {
|
|
644
|
+
return {
|
|
645
|
+
content: [{
|
|
646
|
+
type: "text",
|
|
647
|
+
text: formatError('generic', {
|
|
648
|
+
error: `AST refactor failed: ${error.message}`
|
|
649
|
+
})
|
|
650
|
+
}],
|
|
651
|
+
isError: true
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Strategy 3: Search/Replace (deterministic, fastest)
|
|
569
657
|
if (strategy === 'search_replace') {
|
|
570
658
|
const extracted = extractSearchReplace(instruction);
|
|
571
659
|
if (extracted) {
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-Aware Detection for mcfast v2.2
|
|
3
|
+
* Enables symbol-aware refactoring using Babel parser
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import parser from '@babel/parser';
|
|
7
|
+
import _traverse from '@babel/traverse';
|
|
8
|
+
import _generate from '@babel/generator';
|
|
9
|
+
import * as t from '@babel/types';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
// Handle default exports from Babel (CommonJS compatibility)
|
|
13
|
+
const traverse = _traverse.default || _traverse;
|
|
14
|
+
const generate = _generate.default || _generate;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse code into AST based on file extension
|
|
18
|
+
*/
|
|
19
|
+
export function parseCode(code, filePath) {
|
|
20
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
21
|
+
const plugins = [];
|
|
22
|
+
|
|
23
|
+
// TypeScript support
|
|
24
|
+
if (['.ts', '.tsx', '.mts', '.cts'].includes(ext)) {
|
|
25
|
+
plugins.push('typescript');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// JSX support
|
|
29
|
+
if (['.jsx', '.tsx'].includes(ext)) {
|
|
30
|
+
plugins.push('jsx');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Common plugins
|
|
34
|
+
plugins.push('decorators-legacy', 'classProperties', 'objectRestSpread');
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return parser.parse(code, {
|
|
38
|
+
sourceType: 'module',
|
|
39
|
+
plugins,
|
|
40
|
+
errorRecovery: true
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
// Fallback: try as script
|
|
44
|
+
try {
|
|
45
|
+
return parser.parse(code, {
|
|
46
|
+
sourceType: 'script',
|
|
47
|
+
plugins,
|
|
48
|
+
errorRecovery: true
|
|
49
|
+
});
|
|
50
|
+
} catch (fallbackError) {
|
|
51
|
+
throw new Error(`Failed to parse ${filePath}: ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Detect refactoring patterns from instruction
|
|
58
|
+
*/
|
|
59
|
+
export function detectRefactoringPattern(instruction) {
|
|
60
|
+
const patterns = {
|
|
61
|
+
rename_variable: /rename\s+(?:variable|var|const|let)\s+["`']?(\w+)["`']?\s+to\s+["`']?(\w+)["`']?/i,
|
|
62
|
+
rename_function: /rename\s+(?:function|func)\s+["`']?(\w+)["`']?\s+to\s+["`']?(\w+)["`']?/i,
|
|
63
|
+
rename_class: /rename\s+class\s+["`']?(\w+)["`']?\s+to\s+["`']?(\w+)["`']?/i,
|
|
64
|
+
rename_any: /rename\s+["`']?(\w+)["`']?\s+to\s+["`']?(\w+)["`']?/i,
|
|
65
|
+
extract_function: /extract\s+(?:function|method)/i,
|
|
66
|
+
inline_variable: /inline\s+(?:variable|var|const|let)\s+["`']?(\w+)["`']?/i,
|
|
67
|
+
inline_function: /inline\s+(?:function|func)\s+["`']?(\w+)["`']?/i
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
for (const [type, pattern] of Object.entries(patterns)) {
|
|
71
|
+
const match = instruction.match(pattern);
|
|
72
|
+
if (match) {
|
|
73
|
+
return {
|
|
74
|
+
type,
|
|
75
|
+
oldName: match[1],
|
|
76
|
+
newName: match[2] || null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Rename identifier in AST
|
|
86
|
+
*/
|
|
87
|
+
export function renameIdentifier(ast, oldName, newName, options = {}) {
|
|
88
|
+
const { scope = 'all' } = options;
|
|
89
|
+
const renamedLocations = [];
|
|
90
|
+
|
|
91
|
+
traverse(ast, {
|
|
92
|
+
Identifier(path) {
|
|
93
|
+
if (path.node.name === oldName) {
|
|
94
|
+
// Check if this is a binding (declaration) or reference
|
|
95
|
+
const binding = path.scope.getBinding(oldName);
|
|
96
|
+
|
|
97
|
+
if (scope === 'all' || (binding && path.isReferencedIdentifier())) {
|
|
98
|
+
path.node.name = newName;
|
|
99
|
+
renamedLocations.push({
|
|
100
|
+
line: path.node.loc?.start.line,
|
|
101
|
+
column: path.node.loc?.start.column,
|
|
102
|
+
type: path.parent.type
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
ast,
|
|
112
|
+
count: renamedLocations.length,
|
|
113
|
+
locations: renamedLocations
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Find all usages of an identifier
|
|
119
|
+
*/
|
|
120
|
+
export function findIdentifierUsages(ast, name) {
|
|
121
|
+
const usages = [];
|
|
122
|
+
|
|
123
|
+
traverse(ast, {
|
|
124
|
+
Identifier(path) {
|
|
125
|
+
if (path.node.name === name) {
|
|
126
|
+
const binding = path.scope.getBinding(name);
|
|
127
|
+
usages.push({
|
|
128
|
+
line: path.node.loc?.start.line,
|
|
129
|
+
column: path.node.loc?.start.column,
|
|
130
|
+
type: path.parent.type,
|
|
131
|
+
isDeclaration: binding && path.isBindingIdentifier(),
|
|
132
|
+
isReference: path.isReferencedIdentifier(),
|
|
133
|
+
scope: path.scope.uid
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return usages;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Extract function from selected code
|
|
144
|
+
*/
|
|
145
|
+
export function extractFunction(ast, startLine, endLine, functionName) {
|
|
146
|
+
let extractedStatements = [];
|
|
147
|
+
let insertionPoint = null;
|
|
148
|
+
|
|
149
|
+
traverse(ast, {
|
|
150
|
+
enter(path) {
|
|
151
|
+
const loc = path.node.loc;
|
|
152
|
+
if (!loc) return;
|
|
153
|
+
|
|
154
|
+
// Find statements within range
|
|
155
|
+
if (loc.start.line >= startLine && loc.end.line <= endLine) {
|
|
156
|
+
if (t.isStatement(path.node) && !path.findParent(p => extractedStatements.includes(p.node))) {
|
|
157
|
+
extractedStatements.push(path.node);
|
|
158
|
+
if (!insertionPoint) {
|
|
159
|
+
insertionPoint = path.getStatementParent();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (extractedStatements.length === 0) {
|
|
167
|
+
return { success: false, message: 'No statements found in range' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Create new function
|
|
171
|
+
const newFunction = t.functionDeclaration(
|
|
172
|
+
t.identifier(functionName),
|
|
173
|
+
[],
|
|
174
|
+
t.blockStatement(extractedStatements)
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Replace extracted code with function call
|
|
178
|
+
const functionCall = t.expressionStatement(
|
|
179
|
+
t.callExpression(t.identifier(functionName), [])
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
success: true,
|
|
184
|
+
newFunction,
|
|
185
|
+
functionCall,
|
|
186
|
+
insertionPoint
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Inline variable (replace all usages with its value)
|
|
192
|
+
*/
|
|
193
|
+
export function inlineVariable(ast, variableName) {
|
|
194
|
+
let variableValue = null;
|
|
195
|
+
let declarationPath = null;
|
|
196
|
+
const replacements = [];
|
|
197
|
+
|
|
198
|
+
// Find variable declaration
|
|
199
|
+
traverse(ast, {
|
|
200
|
+
VariableDeclarator(path) {
|
|
201
|
+
if (path.node.id.name === variableName) {
|
|
202
|
+
variableValue = path.node.init;
|
|
203
|
+
declarationPath = path;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!variableValue) {
|
|
209
|
+
return { success: false, message: `Variable ${variableName} not found` };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Replace all references
|
|
213
|
+
traverse(ast, {
|
|
214
|
+
Identifier(path) {
|
|
215
|
+
if (path.node.name === variableName && path.isReferencedIdentifier()) {
|
|
216
|
+
// Clone the value to avoid reference issues
|
|
217
|
+
const clonedValue = t.cloneNode(variableValue, true);
|
|
218
|
+
path.replaceWith(clonedValue);
|
|
219
|
+
replacements.push({
|
|
220
|
+
line: path.node.loc?.start.line,
|
|
221
|
+
column: path.node.loc?.start.column
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Remove declaration
|
|
228
|
+
if (declarationPath) {
|
|
229
|
+
const parent = declarationPath.parentPath;
|
|
230
|
+
if (parent.node.declarations.length === 1) {
|
|
231
|
+
parent.remove();
|
|
232
|
+
} else {
|
|
233
|
+
declarationPath.remove();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
success: true,
|
|
239
|
+
ast,
|
|
240
|
+
replacements: replacements.length,
|
|
241
|
+
locations: replacements
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Apply AST transformation and generate code
|
|
247
|
+
*/
|
|
248
|
+
export function applyASTTransformation(code, filePath, transformation) {
|
|
249
|
+
try {
|
|
250
|
+
const ast = parseCode(code, filePath);
|
|
251
|
+
const result = transformation(ast);
|
|
252
|
+
|
|
253
|
+
if (!result.success) {
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const output = generate(result.ast || ast, {
|
|
258
|
+
retainLines: true,
|
|
259
|
+
comments: true
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
success: true,
|
|
264
|
+
code: output.code,
|
|
265
|
+
...result
|
|
266
|
+
};
|
|
267
|
+
} catch (error) {
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
message: `AST transformation failed: ${error.message}`
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Check if instruction requires AST-based refactoring
|
|
277
|
+
*/
|
|
278
|
+
export function requiresAST(instruction) {
|
|
279
|
+
const astPatterns = [
|
|
280
|
+
/rename\s+(?:variable|function|class|method)/i,
|
|
281
|
+
/extract\s+(?:function|method)/i,
|
|
282
|
+
/inline\s+(?:variable|function)/i,
|
|
283
|
+
/move\s+(?:function|class)/i,
|
|
284
|
+
/split\s+(?:function|class)/i
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
return astPatterns.some(pattern => pattern.test(instruction));
|
|
288
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { isDiffBasedEdit } from './fuzzy-patch.js';
|
|
7
|
+
import { requiresAST, detectRefactoringPattern } from './ast-detector.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Detect if instruction is a simple search/replace
|
|
@@ -64,7 +65,7 @@ export function hasPlaceholders(codeEdit) {
|
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* Determine the best edit strategy
|
|
67
|
-
* @returns {'fuzzy_patch' | 'search_replace' | 'placeholder_merge' | 'mercury_intelligent'}
|
|
68
|
+
* @returns {'fuzzy_patch' | 'ast_refactor' | 'search_replace' | 'placeholder_merge' | 'mercury_intelligent'}
|
|
68
69
|
*/
|
|
69
70
|
export function detectEditStrategy({ instruction, code_edit, files }) {
|
|
70
71
|
// Priority 1: Fuzzy Patch (unified diff format)
|
|
@@ -72,16 +73,21 @@ export function detectEditStrategy({ instruction, code_edit, files }) {
|
|
|
72
73
|
return 'fuzzy_patch';
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
// Priority 2:
|
|
76
|
+
// Priority 2: AST Refactor (symbol-aware operations)
|
|
77
|
+
if (requiresAST(instruction)) {
|
|
78
|
+
return 'ast_refactor';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Priority 3: Search/Replace (fastest, deterministic)
|
|
76
82
|
if (isSearchReplace(instruction)) {
|
|
77
83
|
return 'search_replace';
|
|
78
84
|
}
|
|
79
85
|
|
|
80
|
-
// Priority
|
|
86
|
+
// Priority 4: Placeholder Merge (token-efficient)
|
|
81
87
|
if (code_edit && hasPlaceholders(code_edit)) {
|
|
82
88
|
return 'placeholder_merge';
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
// Priority
|
|
91
|
+
// Priority 5: Mercury Intelligent (most flexible)
|
|
86
92
|
return 'mercury_intelligent';
|
|
87
93
|
}
|