@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrxkun/mcfast-mcp",
3
- "version": "2.1.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: Search/Replace (deterministic, fastest)
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: Search/Replace (fastest, deterministic)
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 3: Placeholder Merge (token-efficient)
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 4: Mercury Intelligent (most flexible)
91
+ // Priority 5: Mercury Intelligent (most flexible)
86
92
  return 'mercury_intelligent';
87
93
  }