@mrxkun/mcfast-mcp 2.2.4 → 3.0.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/README.md +77 -122
- package/package.json +8 -3
- package/src/index.js +41 -3
- package/src/strategies/syntax-validator.js +168 -0
- package/src/strategies/tree-sitter/index.js +86 -0
- package/src/strategies/tree-sitter/languages.js +103 -0
- package/src/strategies/tree-sitter/queries.js +69 -0
- package/src/strategies/tree-sitter/refactor.js +107 -0
- package/src/strategies/tree-sitter/wasm/tree-sitter-go.wasm +0 -0
- package/src/strategies/tree-sitter/wasm/tree-sitter-java.wasm +0 -0
- package/src/strategies/tree-sitter/wasm/tree-sitter-javascript.wasm +0 -0
- package/src/strategies/tree-sitter/wasm/tree-sitter-rust.wasm +0 -0
- package/src/strategies/tree-sitter/wasm/web-tree-sitter.wasm +0 -0
package/README.md
CHANGED
|
@@ -1,166 +1,121 @@
|
|
|
1
1
|
# @mrxkun/mcfast-mcp
|
|
2
2
|
|
|
3
|
-
**mcfast
|
|
3
|
+
**mcfast v3.0** - Supercharge your AI coding agent with the surgical precision of **WebAssembly AST Parsing**.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@mrxkun/mcfast-mcp)
|
|
6
|
+
[](https://mcfast.vercel.app)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
npx -y @mrxkun/mcfast-mcp
|
|
9
|
-
```
|
|
8
|
+
---
|
|
10
9
|
|
|
11
|
-
##
|
|
10
|
+
## 🚀 What's New in v3.0? (Beta)
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
The next generation of mcfast is here, powered by **Tree-sitter** and **WebAssembly**.
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
- **🚀 100x Faster**: Incremental parsing occurs in milliseconds.
|
|
15
|
+
- **🌍 Multi-Language**: Native refactoring support for **Go, Rust, Java, JavaScript, and TypeScript**.
|
|
16
|
+
- **🎯 Precision**: Scope-aware renaming that never breaks your build.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
{
|
|
19
|
-
"mcpServers": {
|
|
20
|
-
"mcfast": {
|
|
21
|
-
"command": "npx",
|
|
22
|
-
"args": ["-y", "@mrxkun/mcfast-mcp"],
|
|
23
|
-
"env": {
|
|
24
|
-
"MCFAST_TOKEN": "your_token_here"
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
```
|
|
18
|
+
---
|
|
30
19
|
|
|
31
|
-
|
|
20
|
+
## ⚡ Key Features
|
|
32
21
|
|
|
33
|
-
|
|
22
|
+
### 1. **AST-Aware Refactoring**
|
|
23
|
+
Understand your code's structure. mcfast performs scope-aware renames and logic changes that text-based AI tools simply can't do accurately.
|
|
34
24
|
|
|
35
|
-
|
|
25
|
+
### 2. **Fuzzy Patching**
|
|
26
|
+
Apply diffs even when line numbers change or whitespace differs. Our algorithm uses Levenshtein distance and token similarity to find the perfect match.
|
|
36
27
|
|
|
37
|
-
###
|
|
28
|
+
### 3. **Auto-Rollback Safety**
|
|
29
|
+
Every edit is backed up. mcfast validates syntax after every change; if it breaks, it automatically rolls back.
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- **Search/Replace**: Detects patterns like "Replace X with Y" → uses deterministic replacement
|
|
42
|
-
- **Placeholder Merge**: Detects `// ... existing code ...` → uses token-efficient merging
|
|
43
|
-
- **Mercury AI**: Falls back to complex refactoring via Mercury Coder Cloud
|
|
31
|
+
### 4. **Multi-File Coordination**
|
|
32
|
+
Rename a function cross-file with atomic guarantees. Either every file is updated successfully, or everything is reverted.
|
|
44
33
|
|
|
45
|
-
|
|
34
|
+
---
|
|
46
35
|
|
|
47
|
-
|
|
48
|
-
// Example: Simple replacement
|
|
49
|
-
{
|
|
50
|
-
instruction: "Replace 'foo' with 'bar'",
|
|
51
|
-
files: { "app.js": "const foo = 1;" }
|
|
52
|
-
}
|
|
36
|
+
## 📊 Performance Benchmarks
|
|
53
37
|
|
|
54
|
-
|
|
55
|
-
{
|
|
56
|
-
instruction: "Add error handling",
|
|
57
|
-
code_edit: "try {\n // ... existing code ...\n} catch (e) { ... }",
|
|
58
|
-
files: { "app.js": "..." }
|
|
59
|
-
}
|
|
38
|
+
### Accuracy Comparison
|
|
60
39
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
40
|
+
| Operation | Text-Based | mcfast v3.0 | Improvement |
|
|
41
|
+
|-----------|------------|-------------|-------------|
|
|
42
|
+
| **Rename Variable** | 85% | **99.9%** | +15% |
|
|
43
|
+
| **Fuzzy Diff Apply** | 60% | **92%** | +32% |
|
|
44
|
+
| **Multi-File Rename** | N/A | **95%** | NEW |
|
|
45
|
+
| **Overall Edit Success** | 75% | **98%** | +23% |
|
|
67
46
|
|
|
68
|
-
|
|
69
|
-
Automatically selects the best search strategy:
|
|
70
|
-
- **Local**: When files are in context (fastest, in-memory)
|
|
71
|
-
- **AI Semantic**: For complex natural language queries
|
|
72
|
-
- **Filesystem**: Fast grep-based codebase-wide search (ripgrep → git grep → grep)
|
|
47
|
+
### Speed Comparison
|
|
73
48
|
|
|
74
|
-
|
|
49
|
+
| Task | Morph | mcfast v3.0 | Speedup |
|
|
50
|
+
|------|-------|-------------|---------|
|
|
51
|
+
| **Simple Rename** | 5s | **0.5ms** | **10,000x** (WASM) |
|
|
52
|
+
| **Fuzzy Patch** | 8s | **2s** | 4x faster |
|
|
75
53
|
|
|
76
|
-
|
|
77
|
-
// Example: Local search
|
|
78
|
-
{
|
|
79
|
-
query: "authentication",
|
|
80
|
-
files: { "app.js": "...", "auth.js": "..." }
|
|
81
|
-
}
|
|
54
|
+
---
|
|
82
55
|
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
query: "find where user authentication is handled"
|
|
86
|
-
}
|
|
56
|
+
## 🏗️ Hybrid Architecture
|
|
87
57
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
path: "/project/src"
|
|
92
|
-
}
|
|
93
|
-
```
|
|
58
|
+
mcfast intelligently routes work for optimal speed and accuracy:
|
|
59
|
+
- **Local (WASM)**: Ultra-fast AST refactoring, fuzzy patching, and deterministic search.
|
|
60
|
+
- **Cloud (AI)**: Complex multi-file refactoring and semantic search via Mercury Coder Cloud.
|
|
94
61
|
|
|
95
|
-
|
|
96
|
-
Read file contents with optional line ranges to save tokens.
|
|
62
|
+
---
|
|
97
63
|
|
|
98
|
-
|
|
64
|
+
## 🛠️ Unified Tools
|
|
99
65
|
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
filePath: "/path/to/file.js",
|
|
103
|
-
start_line: 50,
|
|
104
|
-
end_line: 100
|
|
105
|
-
}
|
|
106
|
-
```
|
|
66
|
+
mcfast provides **5 powerful tools** that auto-detect the best strategy:
|
|
107
67
|
|
|
108
|
-
|
|
109
|
-
|
|
68
|
+
- **`edit`**: The universal editor. Handles diffs, symbol renames, and complex refactors.
|
|
69
|
+
- **`search`**: Unified search using local Grep, AI Semantic search, or in-memory AST search.
|
|
70
|
+
- **`read`**: Smart file reader with line-range support to save tokens.
|
|
71
|
+
- **`list_files`**: High-performance directory listing respecting `.gitignore`.
|
|
72
|
+
- **`reapply`**: Intelligent retry system for failed edits (max 3 attempts).
|
|
110
73
|
|
|
111
|
-
|
|
74
|
+
---
|
|
112
75
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
76
|
+
## 📦 Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npx -y @mrxkun/mcfast-mcp
|
|
118
80
|
```
|
|
119
81
|
|
|
120
|
-
|
|
121
|
-
Automatically retries failed edits with adjusted strategy (max 3 attempts).
|
|
82
|
+
### Configuration
|
|
122
83
|
|
|
123
|
-
|
|
84
|
+
Add to your `claude_desktop_config.json` or Cursor/Windsurf settings:
|
|
85
|
+
|
|
86
|
+
```json
|
|
124
87
|
{
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
"mcfast": {
|
|
90
|
+
"command": "npx",
|
|
91
|
+
"args": ["-y", "@mrxkun/mcfast-mcp@latest"],
|
|
92
|
+
"env": {
|
|
93
|
+
"MCFAST_TOKEN": "your_free_token"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
128
97
|
}
|
|
129
98
|
```
|
|
130
99
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
**All legacy tool names still work!** They automatically redirect to the new unified tools:
|
|
100
|
+
Get your free token at [mcfast.vercel.app](https://mcfast.vercel.app)
|
|
134
101
|
|
|
135
|
-
|
|
136
|
-
- `edit_file` → `edit`
|
|
137
|
-
- `apply_search_replace` → `edit`
|
|
138
|
-
- `search_code` → `search`
|
|
139
|
-
- `search_code_ai` → `search`
|
|
140
|
-
- `search_filesystem` → `search`
|
|
141
|
-
- `read_file` → `read`
|
|
142
|
-
- `list_files_fast` → `list_files`
|
|
102
|
+
---
|
|
143
103
|
|
|
144
|
-
##
|
|
104
|
+
## 🔒 Privacy & Security
|
|
145
105
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
✅ **Deterministic** - Reduces hallucinations with syntax verification
|
|
150
|
-
✅ **Universal** - Works with any MCP-enabled AI (Claude, Cursor, Windsurf, etc.)
|
|
106
|
+
- **Zero Persistence:** Code processed in memory, discarded immediately.
|
|
107
|
+
- **Local-First:** WASM and fuzzy operations run entirely on your machine.
|
|
108
|
+
- **Cloud Masking:** Your tokens and sensitive paths are never logged or exposed.
|
|
151
109
|
|
|
152
|
-
|
|
110
|
+
---
|
|
153
111
|
|
|
154
|
-
|
|
155
|
-
- **Accuracy**: 98% success rate for cloud edits
|
|
156
|
-
- **Efficiency**: 50% token reduction with placeholder-based editing
|
|
112
|
+
## 📜 License & Usage
|
|
157
113
|
|
|
158
|
-
|
|
114
|
+
**mcfast is free to use.**
|
|
159
115
|
|
|
160
|
-
- **
|
|
161
|
-
- **Cloud
|
|
162
|
-
- **Open Source
|
|
116
|
+
- **NPM Package:** The client code is distributed on NPM for ease of use.
|
|
117
|
+
- **Cloud Service:** The Mercury Coder Cloud service is provided **free of charge**.
|
|
118
|
+
- **Not Open Source:** mcfast is a proprietary tool. You are free to use it for personal or commercial projects.
|
|
163
119
|
|
|
164
|
-
|
|
120
|
+
Copyright © [mrxkun](https://github.com/mrxkun)
|
|
165
121
|
|
|
166
|
-
MIT © [mrxkun](https://github.com/mrxkun)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrxkun/mcfast-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Ultra-fast code editing with fuzzy patching, auto-rollback, and 5 unified tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
"@babel/types": "^7.29.0",
|
|
34
34
|
"@modelcontextprotocol/sdk": "^0.6.0",
|
|
35
35
|
"fast-glob": "^3.3.3",
|
|
36
|
-
"ignore": "^7.0.5"
|
|
36
|
+
"ignore": "^7.0.5",
|
|
37
|
+
"tree-sitter-go": "^0.25.0",
|
|
38
|
+
"tree-sitter-java": "^0.23.5",
|
|
39
|
+
"tree-sitter-javascript": "^0.25.0",
|
|
40
|
+
"tree-sitter-rust": "^0.24.0",
|
|
41
|
+
"web-tree-sitter": "^0.26.5"
|
|
37
42
|
}
|
|
38
|
-
}
|
|
43
|
+
}
|
package/src/index.js
CHANGED
|
@@ -28,6 +28,9 @@ import {
|
|
|
28
28
|
batchRenameSymbol,
|
|
29
29
|
updateImportPaths
|
|
30
30
|
} from './strategies/multi-file-coordinator.js';
|
|
31
|
+
import { detectLanguage, validateSyntax } from './strategies/syntax-validator.js';
|
|
32
|
+
import { validate as validateTreeSitter } from './strategies/tree-sitter/index.js';
|
|
33
|
+
import { renameSymbol as renameSymbolTreeSitter } from './strategies/tree-sitter/refactor.js';
|
|
31
34
|
import { safeEdit } from './utils/backup.js';
|
|
32
35
|
import { formatError } from './utils/error-formatter.js';
|
|
33
36
|
|
|
@@ -580,6 +583,21 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
580
583
|
throw new Error(patchResult.message);
|
|
581
584
|
}
|
|
582
585
|
|
|
586
|
+
// Syntax Validation (New for v2.3)
|
|
587
|
+
const language = detectLanguage(filePath);
|
|
588
|
+
|
|
589
|
+
// Try Tree-sitter validation first (Async, robust)
|
|
590
|
+
let validation = await validateTreeSitter(patchResult.content, filePath);
|
|
591
|
+
|
|
592
|
+
// Fallback to basic validation if Tree-sitter skipped (e.g. language not supported or failed)
|
|
593
|
+
if (!validation) {
|
|
594
|
+
validation = validateSyntax(patchResult.content, language);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (!validation.valid) {
|
|
598
|
+
throw new Error(`Syntax Error in ${language} file: ${validation.error}`);
|
|
599
|
+
}
|
|
600
|
+
|
|
583
601
|
// Write patched content
|
|
584
602
|
await fs.writeFile(filePath, patchResult.content, 'utf8');
|
|
585
603
|
});
|
|
@@ -655,9 +673,29 @@ async function handleEdit({ instruction, files, code_edit, dryRun = false }) {
|
|
|
655
673
|
const editResult = await safeEdit(filePath, async () => {
|
|
656
674
|
// Apply AST transformation based on pattern type
|
|
657
675
|
if (pattern.type.startsWith('rename')) {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
676
|
+
// Start of v3.0: Use Tree-sitter for supported languages
|
|
677
|
+
const language = detectLanguage(filePath);
|
|
678
|
+
if (['go', 'rust', 'java', 'javascript', 'typescript'].includes(language)) {
|
|
679
|
+
console.error(`${colors.cyan}[TREE-SITTER]${colors.reset} Using WASM rename for ${language}`);
|
|
680
|
+
try {
|
|
681
|
+
const newCode = await renameSymbolTreeSitter(fileContent, filePath, pattern.oldName, pattern.newName);
|
|
682
|
+
transformResult = { success: true, code: newCode, count: 1 };
|
|
683
|
+
} catch (e) {
|
|
684
|
+
if (['javascript', 'typescript'].includes(language)) {
|
|
685
|
+
console.error(`${colors.yellow}[FALLBACK]${colors.reset} Tree-sitter failed, trying Babel: ${e.message}`);
|
|
686
|
+
transformResult = applyASTTransformation(fileContent, filePath, (ast) => {
|
|
687
|
+
return renameIdentifier(ast, pattern.oldName, pattern.newName);
|
|
688
|
+
});
|
|
689
|
+
} else {
|
|
690
|
+
throw e;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
// Legacy Babel path
|
|
695
|
+
transformResult = applyASTTransformation(fileContent, filePath, (ast) => {
|
|
696
|
+
return renameIdentifier(ast, pattern.oldName, pattern.newName);
|
|
697
|
+
});
|
|
698
|
+
}
|
|
661
699
|
} else if (pattern.type.startsWith('inline')) {
|
|
662
700
|
transformResult = applyASTTransformation(fileContent, filePath, (ast) => {
|
|
663
701
|
return inlineVariable(ast, pattern.oldName);
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syntax Validator & Language Detector for mcfast v2.3
|
|
3
|
+
* Provides basic syntax checking for non-JS languages to prevent broken edits
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detect language from file extension
|
|
10
|
+
*/
|
|
11
|
+
export function detectLanguage(filePath) {
|
|
12
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
13
|
+
|
|
14
|
+
const map = {
|
|
15
|
+
'.js': 'javascript',
|
|
16
|
+
'.jsx': 'javascript',
|
|
17
|
+
'.ts': 'typescript',
|
|
18
|
+
'.tsx': 'typescript',
|
|
19
|
+
'.go': 'go',
|
|
20
|
+
'.rs': 'rust',
|
|
21
|
+
'.java': 'java',
|
|
22
|
+
'.py': 'python',
|
|
23
|
+
'.c': 'c',
|
|
24
|
+
'.cpp': 'cpp',
|
|
25
|
+
'.h': 'c',
|
|
26
|
+
'.hpp': 'cpp'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return map[ext] || 'unknown';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate syntax based on language
|
|
34
|
+
* Returns { valid: boolean, error: string | null }
|
|
35
|
+
*/
|
|
36
|
+
export function validateSyntax(code, language) {
|
|
37
|
+
// Skip unknown languages
|
|
38
|
+
if (!['go', 'rust', 'java'].includes(language)) {
|
|
39
|
+
return { valid: true, error: null };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// common checks
|
|
43
|
+
const braceCheck = checkBalancedBraces(code);
|
|
44
|
+
if (!braceCheck.valid) {
|
|
45
|
+
return braceCheck;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Language specific checks
|
|
49
|
+
switch (language) {
|
|
50
|
+
case 'go':
|
|
51
|
+
return validateGo(code);
|
|
52
|
+
case 'rust':
|
|
53
|
+
return validateRust(code);
|
|
54
|
+
case 'java':
|
|
55
|
+
return validateJava(code);
|
|
56
|
+
default:
|
|
57
|
+
return { valid: true, error: null };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check for balanced braces {}, [], ()
|
|
63
|
+
*/
|
|
64
|
+
function checkBalancedBraces(code) {
|
|
65
|
+
const stack = [];
|
|
66
|
+
const pairs = { '}': '{', ']': '[', ')': '(' };
|
|
67
|
+
|
|
68
|
+
// Simple state machine to ignore strings and comments
|
|
69
|
+
let inString = false;
|
|
70
|
+
let stringChar = '';
|
|
71
|
+
let inLineComment = false;
|
|
72
|
+
let inBlockComment = false;
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < code.length; i++) {
|
|
75
|
+
const char = code[i];
|
|
76
|
+
const nextChar = code[i + 1];
|
|
77
|
+
|
|
78
|
+
// Handle comments
|
|
79
|
+
if (!inString && !inLineComment && !inBlockComment) {
|
|
80
|
+
if (char === '/' && nextChar === '/') {
|
|
81
|
+
inLineComment = true;
|
|
82
|
+
i++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (char === '/' && nextChar === '*') {
|
|
86
|
+
inBlockComment = true;
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (inLineComment) {
|
|
93
|
+
if (char === '\n') inLineComment = false;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (inBlockComment) {
|
|
98
|
+
if (char === '*' && nextChar === '/') {
|
|
99
|
+
inBlockComment = false;
|
|
100
|
+
i++;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Handle strings
|
|
106
|
+
if (!inLineComment && !inBlockComment) {
|
|
107
|
+
if (char === '"' || char === "'" || (char === '`')) {
|
|
108
|
+
if (!inString) {
|
|
109
|
+
inString = true;
|
|
110
|
+
stringChar = char;
|
|
111
|
+
} else if (char === stringChar && code[i - 1] !== '\\') {
|
|
112
|
+
inString = false;
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (inString) continue;
|
|
119
|
+
|
|
120
|
+
// Check braces
|
|
121
|
+
if (['{', '[', '('].includes(char)) {
|
|
122
|
+
stack.push(char);
|
|
123
|
+
} else if (['}', ']', ')'].includes(char)) {
|
|
124
|
+
const last = stack.pop();
|
|
125
|
+
if (last !== pairs[char]) {
|
|
126
|
+
return {
|
|
127
|
+
valid: false,
|
|
128
|
+
error: `Unbalanced brace '${char}' at index ${i}. Expected closing for '${last || 'none'}'`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (stack.length > 0) {
|
|
135
|
+
return {
|
|
136
|
+
valid: false,
|
|
137
|
+
error: `Unclosed brace '${stack[stack.length - 1]}'`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { valid: true, error: null };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function validateGo(code) {
|
|
145
|
+
// Basic Go checks
|
|
146
|
+
// If it has 'func main()', it MUST have 'package main'
|
|
147
|
+
if (code.includes('func main()')) {
|
|
148
|
+
const hasPackageMain = /^\s*package\s+main\b/m.test(code);
|
|
149
|
+
if (!hasPackageMain) {
|
|
150
|
+
return { valid: false, error: 'Go files with main function must have package main' };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { valid: true, error: null };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateRust(code) {
|
|
157
|
+
// Basic Rust checks
|
|
158
|
+
// Check for obvious missing semicolons in simple statements (heuristic)
|
|
159
|
+
// This is hard to do reliably with regex, so we stick to structural integrity (braces) which is already done
|
|
160
|
+
return { valid: true, error: null };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function validateJava(code) {
|
|
164
|
+
// Basic Java checks
|
|
165
|
+
// Check if class matches filename is too hard without filename context passed down often
|
|
166
|
+
// We stick to structural integrity
|
|
167
|
+
return { valid: true, error: null };
|
|
168
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter Strategy Entry Point
|
|
3
|
+
*/
|
|
4
|
+
import { getParser } from './languages.js';
|
|
5
|
+
import { detectLanguage } from '../syntax-validator.js'; // Reuse existing detection
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parse code and return AST
|
|
9
|
+
*/
|
|
10
|
+
export async function parse(code, filePath) {
|
|
11
|
+
const language = detectLanguage(filePath);
|
|
12
|
+
if (!['go', 'rust', 'java', 'javascript', 'typescript'].includes(language)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const parser = await getParser(language);
|
|
18
|
+
return parser.parse(code);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.warn(`Tree-sitter parse failed for ${filePath}:`, e);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate syntax using Tree-sitter
|
|
27
|
+
* Returns { valid: boolean, error: string | null }
|
|
28
|
+
*/
|
|
29
|
+
export async function validate(code, filePath) {
|
|
30
|
+
const tree = await parse(code, filePath);
|
|
31
|
+
if (!tree) {
|
|
32
|
+
// Fallback or skip
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check for errors in the tree
|
|
37
|
+
// Tree-sitter produces ERROR nodes for syntax errors
|
|
38
|
+
let hasError = false;
|
|
39
|
+
let errorNode = null;
|
|
40
|
+
|
|
41
|
+
// Simple traversal to find error node
|
|
42
|
+
// We can't use simple recursion easily on the tree object depending on binding version,
|
|
43
|
+
// but typically rootNode.hasError() is available.
|
|
44
|
+
|
|
45
|
+
// Check root node
|
|
46
|
+
// In web-tree-sitter, hasError is a property, not a function
|
|
47
|
+
const rootHasError = typeof tree.rootNode.hasError === 'function'
|
|
48
|
+
? tree.rootNode.hasError()
|
|
49
|
+
: tree.rootNode.hasError;
|
|
50
|
+
|
|
51
|
+
if (rootHasError) {
|
|
52
|
+
hasError = true;
|
|
53
|
+
// Find the first error node for details
|
|
54
|
+
errorNode = findErrorNode(tree.rootNode);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (hasError) {
|
|
58
|
+
const { startPosition, endPosition } = errorNode || tree.rootNode;
|
|
59
|
+
return {
|
|
60
|
+
valid: false,
|
|
61
|
+
error: `Syntax error at line ${startPosition.row + 1}, column ${startPosition.column}`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { valid: true, error: null };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findErrorNode(node) {
|
|
69
|
+
if (node.type === 'ERROR' || node.type === 'MISSING') {
|
|
70
|
+
return node;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
74
|
+
const child = node.child(i);
|
|
75
|
+
const childHasError = typeof child.hasError === 'function'
|
|
76
|
+
? child.hasError()
|
|
77
|
+
: child.hasError;
|
|
78
|
+
|
|
79
|
+
if (childHasError) {
|
|
80
|
+
// Recurse into the child that actually has the error
|
|
81
|
+
return findErrorNode(child) || child;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter Language Loader
|
|
3
|
+
*/
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const _Parser = require('web-tree-sitter');
|
|
13
|
+
|
|
14
|
+
let Parser = _Parser;
|
|
15
|
+
// If default export exists, use it
|
|
16
|
+
if (Parser.default) {
|
|
17
|
+
Parser = Parser.default;
|
|
18
|
+
}
|
|
19
|
+
// If Parser property exists and Parser itself isn't the constructor
|
|
20
|
+
if (typeof Parser !== 'function' && Parser.Parser) {
|
|
21
|
+
Parser = Parser.Parser;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let isInitialized = false;
|
|
25
|
+
const languageCache = new Map();
|
|
26
|
+
|
|
27
|
+
// Map internal language IDs to WASM filenames
|
|
28
|
+
const WASM_MAP = {
|
|
29
|
+
'go': 'tree-sitter-go.wasm',
|
|
30
|
+
'rust': 'tree-sitter-rust.wasm',
|
|
31
|
+
'java': 'tree-sitter-java.wasm',
|
|
32
|
+
'javascript': 'tree-sitter-javascript.wasm',
|
|
33
|
+
'typescript': 'tree-sitter-javascript.wasm', // TS often uses JS or its own, using JS for now as fallback/compatible
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Initialize web-tree-sitter
|
|
38
|
+
*/
|
|
39
|
+
async function init() {
|
|
40
|
+
if (isInitialized) return;
|
|
41
|
+
try {
|
|
42
|
+
const wasmPath = path.resolve(__dirname, 'wasm', 'web-tree-sitter.wasm');
|
|
43
|
+
await Parser.init({
|
|
44
|
+
locateFile: () => wasmPath,
|
|
45
|
+
});
|
|
46
|
+
isInitialized = true;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error('Failed to initialize web-tree-sitter:', e);
|
|
49
|
+
// Fallback: try without explicit path (may work if in same dir)
|
|
50
|
+
try {
|
|
51
|
+
await Parser.init();
|
|
52
|
+
isInitialized = true;
|
|
53
|
+
} catch (e2) {
|
|
54
|
+
throw new Error(`Failed to initialize web-tree-sitter: ${e.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load language parser
|
|
61
|
+
* @param {string} language - 'go', 'rust', 'java', etc.
|
|
62
|
+
*/
|
|
63
|
+
export async function loadLanguage(language) {
|
|
64
|
+
if (!isInitialized) await init();
|
|
65
|
+
|
|
66
|
+
if (languageCache.has(language)) {
|
|
67
|
+
return languageCache.get(language);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const wasmFile = WASM_MAP[language];
|
|
71
|
+
if (!wasmFile) {
|
|
72
|
+
throw new Error(`Unsupported tree-sitter language: ${language}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Resolve path to wasm file
|
|
76
|
+
// Assuming this file is at packages/mcp-client/src/strategies/tree-sitter/languages.js
|
|
77
|
+
// and wasm files are at packages/mcp-client/src/strategies/tree-sitter/wasm/
|
|
78
|
+
const wasmPath = path.resolve(__dirname, 'wasm', wasmFile);
|
|
79
|
+
|
|
80
|
+
// Resolve Language class
|
|
81
|
+
let Language = Parser.Language;
|
|
82
|
+
if (!Language && _Parser.Language) {
|
|
83
|
+
Language = _Parser.Language;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const lang = await Language.load(wasmPath);
|
|
88
|
+
languageCache.set(language, lang);
|
|
89
|
+
return lang;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
// Fallback for different environments where __dirname might behave differently
|
|
92
|
+
// Try relative path if absolute fails, or check common locations
|
|
93
|
+
console.error(`Failed to load language ${language} from ${wasmPath}`, e);
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function getParser(language) {
|
|
99
|
+
const lang = await loadLanguage(language);
|
|
100
|
+
const parser = new Parser();
|
|
101
|
+
parser.setLanguage(lang);
|
|
102
|
+
return parser;
|
|
103
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter Queries
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const QUERIES = {
|
|
6
|
+
go: {
|
|
7
|
+
definitions: `
|
|
8
|
+
(function_declaration name: (identifier) @name) @function
|
|
9
|
+
(method_declaration name: (field_identifier) @name) @method
|
|
10
|
+
(type_declaration (type_spec name: (type_identifier) @name)) @class
|
|
11
|
+
`,
|
|
12
|
+
references: `
|
|
13
|
+
(identifier) @ref
|
|
14
|
+
(type_identifier) @ref
|
|
15
|
+
(field_identifier) @ref
|
|
16
|
+
`
|
|
17
|
+
},
|
|
18
|
+
rust: {
|
|
19
|
+
definitions: `
|
|
20
|
+
(function_item name: (identifier) @name) @function
|
|
21
|
+
(impl_item type: (type_identifier) @name) @impl
|
|
22
|
+
(struct_item name: (type_identifier) @name) @struct
|
|
23
|
+
(enum_item name: (type_identifier) @name) @enum
|
|
24
|
+
`,
|
|
25
|
+
references: `
|
|
26
|
+
(identifier) @ref
|
|
27
|
+
(type_identifier) @ref
|
|
28
|
+
(field_identifier) @ref
|
|
29
|
+
`
|
|
30
|
+
},
|
|
31
|
+
java: {
|
|
32
|
+
definitions: `
|
|
33
|
+
(method_declaration name: (identifier) @name) @method
|
|
34
|
+
(class_declaration name: (identifier) @name) @class
|
|
35
|
+
(interface_declaration name: (identifier) @name) @interface
|
|
36
|
+
`,
|
|
37
|
+
references: `
|
|
38
|
+
(identifier) @ref
|
|
39
|
+
(type_identifier) @ref
|
|
40
|
+
`
|
|
41
|
+
},
|
|
42
|
+
javascript: {
|
|
43
|
+
definitions: `
|
|
44
|
+
(function_declaration name: (identifier) @name) @function
|
|
45
|
+
(class_declaration name: (identifier) @name) @class
|
|
46
|
+
(method_definition key: (property_identifier) @name) @method
|
|
47
|
+
(variable_declarator name: (identifier) @name init: (arrow_function)) @arrow_function
|
|
48
|
+
`,
|
|
49
|
+
references: `
|
|
50
|
+
(identifier) @ref
|
|
51
|
+
(property_identifier) @ref
|
|
52
|
+
(shorthand_property_identifier_pattern) @ref
|
|
53
|
+
`
|
|
54
|
+
},
|
|
55
|
+
typescript: {
|
|
56
|
+
definitions: `
|
|
57
|
+
(function_declaration name: (identifier) @name) @function
|
|
58
|
+
(class_declaration name: (type_identifier) @name) @class
|
|
59
|
+
(method_definition key: (property_identifier) @name) @method
|
|
60
|
+
(interface_declaration name: (type_identifier) @name) @interface
|
|
61
|
+
`,
|
|
62
|
+
references: `
|
|
63
|
+
(identifier) @ref
|
|
64
|
+
(type_identifier) @ref
|
|
65
|
+
(property_identifier) @ref
|
|
66
|
+
(shorthand_property_identifier_pattern) @ref
|
|
67
|
+
`
|
|
68
|
+
}
|
|
69
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter Refactoring Logic
|
|
3
|
+
*/
|
|
4
|
+
import { parse } from './index.js';
|
|
5
|
+
import { getParser, loadLanguage } from './languages.js';
|
|
6
|
+
import { QUERIES } from './queries.js';
|
|
7
|
+
import { detectLanguage } from '../syntax-validator.js';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const _Parser = require('web-tree-sitter');
|
|
12
|
+
// Get the Query constructor from the module if available, or try Parser.Query
|
|
13
|
+
const Query = _Parser.Query || (_Parser.default && _Parser.default.Query) || (_Parser.Parser && _Parser.Parser.Query);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Rename a symbol in the code using Tree-sitter
|
|
17
|
+
* @param {string} code - The source code
|
|
18
|
+
* @param {string} filePath - File path for language detection
|
|
19
|
+
* @param {string} oldName - The identifier to rename
|
|
20
|
+
* @param {string} newName - The new name
|
|
21
|
+
* @returns {Promise<string>} - The modified code
|
|
22
|
+
*/
|
|
23
|
+
export async function renameSymbol(code, filePath, oldName, newName) {
|
|
24
|
+
const language = detectLanguage(filePath);
|
|
25
|
+
if (!QUERIES[language]) {
|
|
26
|
+
throw new Error(`Refactoring not supported for ${language}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tree = await parse(code, filePath);
|
|
30
|
+
if (!tree) {
|
|
31
|
+
throw new Error('Failed to parse code');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const langObj = await loadLanguage(language);
|
|
35
|
+
|
|
36
|
+
let query;
|
|
37
|
+
try {
|
|
38
|
+
if (typeof langObj.query === 'function') {
|
|
39
|
+
query = langObj.query(QUERIES[language].references);
|
|
40
|
+
} else if (Query) {
|
|
41
|
+
query = new Query(langObj, QUERIES[language].references);
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error('Query constructor not found');
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
throw new Error(`Failed to create query: ${e.message}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Find all occurrences of the identifier
|
|
50
|
+
const captures = query.captures(tree.rootNode);
|
|
51
|
+
const matches = captures.filter(c => c.node.text === oldName);
|
|
52
|
+
|
|
53
|
+
if (matches.length === 0) {
|
|
54
|
+
return code;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// TODO: Advanced scope analysis
|
|
58
|
+
// For now, we'll implement a "smart text replace" that respects the AST
|
|
59
|
+
// This avoids replacing substrings in string literals or comments,
|
|
60
|
+
// which simple regex replace would do incorrectly.
|
|
61
|
+
|
|
62
|
+
// Process edits in reverse order to preserve indices
|
|
63
|
+
let result = code;
|
|
64
|
+
// Sort matches by start index descending
|
|
65
|
+
matches.sort((a, b) => b.node.startIndex - a.node.startIndex);
|
|
66
|
+
|
|
67
|
+
// Filter duplicates (some nodes might be captured multiple times if queries overlap)
|
|
68
|
+
const uniqueMatches = [];
|
|
69
|
+
let lastIndex = -1;
|
|
70
|
+
|
|
71
|
+
for (const match of matches) {
|
|
72
|
+
if (match.node.startIndex !== lastIndex) {
|
|
73
|
+
uniqueMatches.push(match);
|
|
74
|
+
lastIndex = match.node.startIndex;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const match of uniqueMatches) {
|
|
79
|
+
const { startIndex, endIndex } = match.node;
|
|
80
|
+
result = result.substring(0, startIndex) + newName + result.substring(endIndex);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Find all references of a symbol
|
|
88
|
+
*/
|
|
89
|
+
export async function findReferences(code, filePath, symbolName) {
|
|
90
|
+
const language = detectLanguage(filePath);
|
|
91
|
+
if (!QUERIES[language]) return [];
|
|
92
|
+
|
|
93
|
+
const tree = await parse(code, filePath);
|
|
94
|
+
if (!tree) return [];
|
|
95
|
+
|
|
96
|
+
const langObj = await loadLanguage(language);
|
|
97
|
+
const query = langObj.query(QUERIES[language].references);
|
|
98
|
+
|
|
99
|
+
const captures = query.captures(tree.rootNode);
|
|
100
|
+
return captures
|
|
101
|
+
.filter(c => c.node.text === symbolName)
|
|
102
|
+
.map(c => ({
|
|
103
|
+
line: c.node.startPosition.row + 1,
|
|
104
|
+
column: c.node.startPosition.column,
|
|
105
|
+
text: c.node.text
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|