fileditor-mcp 1.0.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/LICENSE +21 -0
- package/README.md +128 -0
- package/docs/MCP-INTERFACE.cn.md +976 -0
- package/docs/MCP-INTERFACE.en.md +881 -0
- package/docs/README.cn.md +128 -0
- package/package.json +27 -0
- package/src/handlers/applyDiff.js +298 -0
- package/src/handlers/insertContent.js +179 -0
- package/src/handlers/listFiles.js +65 -0
- package/src/handlers/readFile.js +109 -0
- package/src/handlers/searchAndReplace.js +198 -0
- package/src/handlers/setWorkspace.js +26 -0
- package/src/handlers/writeFile.js +98 -0
- package/src/index.js +20 -0
- package/src/server.js +85 -0
- package/src/tools/toolDefinitions.js +291 -0
- package/src/utils/fileUtils.js +238 -0
- package/test/ApplyDiffHandler.test.js +754 -0
- package/test/InsertContentHandler.test.js +371 -0
- package/test/ListFilesHandler.test.js +302 -0
- package/test/ReadFileHandler.test.js +213 -0
- package/test/SearchAndReplaceHandler.test.js +505 -0
- package/test/SetWorkspaceHandler.test.js +290 -0
- package/test/WriteFileHandler.test.js +289 -0
- package/test/runAllTests.js +233 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { resolve, join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { FileUtils } from '../utils/fileUtils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* list_files tool handler
|
|
8
|
+
*/
|
|
9
|
+
export class ListFilesHandler {
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handle list_files request
|
|
13
|
+
* @param {Object} args - Request parameters
|
|
14
|
+
* @returns {Promise<Object>} Response result
|
|
15
|
+
*/
|
|
16
|
+
static async handle(args) {
|
|
17
|
+
const { path, recursive = false } = args;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const fullPath = FileUtils.getSecurePath(path);
|
|
21
|
+
|
|
22
|
+
if (!existsSync(fullPath)) {
|
|
23
|
+
throw new Error(`Directory not found: ${path}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = [];
|
|
27
|
+
|
|
28
|
+
if (recursive) {
|
|
29
|
+
await ListFilesHandler.listFilesRecursive(fullPath, result, '');
|
|
30
|
+
} else {
|
|
31
|
+
const items = await fs.readdir(fullPath, { withFileTypes: true });
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
const type = item.isDirectory() ? 'directory' : 'file';
|
|
34
|
+
result.push(`${type}: ${item.name}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return FileUtils.createResponse(result.join('\n'));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new Error(`Failed to list files: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Recursively list files
|
|
46
|
+
* @param {string} dirPath - Directory path
|
|
47
|
+
* @param {string[]} result - Result array
|
|
48
|
+
* @param {string} prefix - Path prefix
|
|
49
|
+
*/
|
|
50
|
+
static async listFilesRecursive(dirPath, result, prefix) {
|
|
51
|
+
const items = await fs.readdir(dirPath, { withFileTypes: true });
|
|
52
|
+
|
|
53
|
+
for (const item of items) {
|
|
54
|
+
const itemPath = join(dirPath, item.name);
|
|
55
|
+
const displayPath = prefix ? `${prefix}/${item.name}` : item.name;
|
|
56
|
+
|
|
57
|
+
if (item.isDirectory()) {
|
|
58
|
+
result.push(`directory: ${displayPath}`);
|
|
59
|
+
await ListFilesHandler.listFilesRecursive(itemPath, result, displayPath);
|
|
60
|
+
} else {
|
|
61
|
+
result.push(`file: ${displayPath}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { FileUtils } from '../utils/fileUtils.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* read_file tool handler
|
|
5
|
+
*/
|
|
6
|
+
export class ReadFileHandler {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Handle read_file request
|
|
10
|
+
* @param {Object} args - Request parameters
|
|
11
|
+
* @returns {Promise<Object>} Response result
|
|
12
|
+
*/
|
|
13
|
+
static async handle(args) {
|
|
14
|
+
const { path, line_range } = args;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Check if single file or multiple files
|
|
18
|
+
if (Array.isArray(path)) {
|
|
19
|
+
// Read multiple files
|
|
20
|
+
return await ReadFileHandler.readMultipleFiles(path);
|
|
21
|
+
} else {
|
|
22
|
+
// Read single file
|
|
23
|
+
return await ReadFileHandler.readSingleFile(path, line_range);
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new Error(`Failed to read file(s): ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read a single file
|
|
32
|
+
* @param {string} filePath - File path
|
|
33
|
+
* @param {string} line_range - Line range
|
|
34
|
+
* @returns {Promise<Object>} Response result
|
|
35
|
+
*/
|
|
36
|
+
static async readSingleFile(filePath, line_range) {
|
|
37
|
+
const content = await FileUtils.readFile(filePath);
|
|
38
|
+
|
|
39
|
+
if (line_range) {
|
|
40
|
+
const selectedContent = FileUtils.getLineRange(content, line_range);
|
|
41
|
+
const [startLine] = line_range.split('-').map(Number);
|
|
42
|
+
const formattedContent = FileUtils.formatWithLineNumbers(selectedContent, startLine);
|
|
43
|
+
return FileUtils.createResponse(formattedContent);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const formattedContent = FileUtils.formatWithLineNumbers(content);
|
|
47
|
+
return FileUtils.createResponse(formattedContent);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read multiple files
|
|
52
|
+
* @param {string[]} filePaths - Array of file paths
|
|
53
|
+
* @returns {Promise<Object>} Response result
|
|
54
|
+
*/
|
|
55
|
+
static async readMultipleFiles(filePaths) {
|
|
56
|
+
// Check for empty array
|
|
57
|
+
if (!Array.isArray(filePaths) || filePaths.length === 0) {
|
|
58
|
+
throw new Error('Failed to read any files: No files provided');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const results = [];
|
|
62
|
+
const errors = [];
|
|
63
|
+
|
|
64
|
+
for (const filePath of filePaths) {
|
|
65
|
+
try {
|
|
66
|
+
if (!FileUtils.fileExists(filePath)) {
|
|
67
|
+
errors.push(`File not found: ${filePath}`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const content = await FileUtils.readFile(filePath);
|
|
72
|
+
results.push({
|
|
73
|
+
file: filePath,
|
|
74
|
+
content: content,
|
|
75
|
+
lines: FileUtils.countLines(content)
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
errors.push(`Error reading ${filePath}: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Build return content
|
|
83
|
+
let responseText = '';
|
|
84
|
+
|
|
85
|
+
if (results.length > 0) {
|
|
86
|
+
responseText += `Successfully read ${results.length} file(s):\n\n`;
|
|
87
|
+
|
|
88
|
+
for (const result of results) {
|
|
89
|
+
responseText += `=== ${result.file} (${result.lines} lines) ===\n`;
|
|
90
|
+
const formattedContent = FileUtils.formatWithLineNumbers(result.content);
|
|
91
|
+
responseText += formattedContent;
|
|
92
|
+
responseText += '\n\n';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (errors.length > 0) {
|
|
97
|
+
responseText += `Errors encountered:\n`;
|
|
98
|
+
for (const error of errors) {
|
|
99
|
+
responseText += `- ${error}\n`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (results.length === 0 && errors.length > 0) {
|
|
104
|
+
throw new Error(`Failed to read any files: ${errors.join(', ')}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return FileUtils.createResponse(responseText.trim());
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { FileUtils } from '../utils/fileUtils.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* search_and_replace tool handler
|
|
5
|
+
*/
|
|
6
|
+
export class SearchAndReplaceHandler {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Handle search_and_replace request
|
|
10
|
+
* @param {Object} args - Request parameters
|
|
11
|
+
* @returns {Promise<Object>} Response result
|
|
12
|
+
*/
|
|
13
|
+
static async handle(args) {
|
|
14
|
+
const {
|
|
15
|
+
path,
|
|
16
|
+
search,
|
|
17
|
+
replace,
|
|
18
|
+
use_regex = false,
|
|
19
|
+
ignore_case = false,
|
|
20
|
+
start_line = null,
|
|
21
|
+
end_line = null
|
|
22
|
+
} = args;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Check if file exists
|
|
26
|
+
if (!FileUtils.fileExists(path)) {
|
|
27
|
+
throw new Error(`File not found: ${path}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Read file content
|
|
31
|
+
const content = await FileUtils.readFile(path);
|
|
32
|
+
const lines = content.split('\n'); // Determine search range
|
|
33
|
+
const startIdx = start_line !== null ? Math.max(0, start_line - 1) : 0;
|
|
34
|
+
const endIdx = end_line !== null ? Math.min(lines.length, end_line) : lines.length; // Validate search string
|
|
35
|
+
if (!search) {
|
|
36
|
+
// Empty search string, do not perform any replacement
|
|
37
|
+
let message = `Successfully replaced 0 occurrence(s) in ${path}`;
|
|
38
|
+
if (start_line || end_line) {
|
|
39
|
+
const rangeDesc = start_line && end_line
|
|
40
|
+
? `lines ${start_line}-${end_line}`
|
|
41
|
+
: start_line
|
|
42
|
+
? `from line ${start_line}`
|
|
43
|
+
: `up to line ${end_line}`;
|
|
44
|
+
message += ` (${rangeDesc})`;
|
|
45
|
+
}
|
|
46
|
+
return FileUtils.createResponse(message);
|
|
47
|
+
}
|
|
48
|
+
if (start_line !== null && start_line < 1) {
|
|
49
|
+
throw new Error(`Invalid start_line: ${start_line}. Line numbers start from 1.`);
|
|
50
|
+
}
|
|
51
|
+
if (end_line !== null && end_line < 1) {
|
|
52
|
+
throw new Error(`Invalid end_line: ${end_line}. Line numbers start from 1.`);
|
|
53
|
+
} if (start_line !== null && end_line !== null && start_line > end_line) {
|
|
54
|
+
throw new Error(`start_line (${start_line}) cannot be greater than end_line (${end_line})`);
|
|
55
|
+
}
|
|
56
|
+
if (start_line !== null && start_line > lines.length) {
|
|
57
|
+
throw new Error(`start_line (${start_line}) exceeds file length (${lines.length} lines)`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Perform search and replace
|
|
61
|
+
let replacementCount = 0;
|
|
62
|
+
const searchLines = lines.slice(startIdx, endIdx);
|
|
63
|
+
const beforeLines = lines.slice(0, startIdx);
|
|
64
|
+
const afterLines = lines.slice(endIdx);
|
|
65
|
+
|
|
66
|
+
const processedLines = searchLines.map(line => {
|
|
67
|
+
let newLine;
|
|
68
|
+
|
|
69
|
+
if (use_regex) {
|
|
70
|
+
try {
|
|
71
|
+
// Build regex flags
|
|
72
|
+
let flags = 'g';
|
|
73
|
+
if (ignore_case) {
|
|
74
|
+
flags += 'i';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const regex = new RegExp(search, flags);
|
|
78
|
+
const matches = line.match(regex);
|
|
79
|
+
if (matches) {
|
|
80
|
+
replacementCount += matches.length;
|
|
81
|
+
}
|
|
82
|
+
newLine = line.replace(regex, replace);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new Error(`Invalid regular expression: ${search}. ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
// Simple string replacement
|
|
88
|
+
if (ignore_case) {
|
|
89
|
+
// Case-insensitive string replacement
|
|
90
|
+
const searchLower = search.toLowerCase();
|
|
91
|
+
const lineLower = line.toLowerCase();
|
|
92
|
+
|
|
93
|
+
// Count occurrences
|
|
94
|
+
const occurrences = SearchAndReplaceHandler.countOccurrencesCaseInsensitive(line, search);
|
|
95
|
+
replacementCount += occurrences;
|
|
96
|
+
|
|
97
|
+
// Perform replacement
|
|
98
|
+
newLine = SearchAndReplaceHandler.replaceAllCaseInsensitive(line, search, replace);
|
|
99
|
+
} else {
|
|
100
|
+
// Case-sensitive string replacement
|
|
101
|
+
const occurrences = SearchAndReplaceHandler.countOccurrences(line, search);
|
|
102
|
+
replacementCount += occurrences;
|
|
103
|
+
newLine = line.split(search).join(replace);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return newLine;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Rebuild file content
|
|
111
|
+
const newContent = [...beforeLines, ...processedLines, ...afterLines].join('\n');
|
|
112
|
+
|
|
113
|
+
// Write back to file
|
|
114
|
+
await FileUtils.writeFile(path, newContent);
|
|
115
|
+
|
|
116
|
+
// Build response message
|
|
117
|
+
let message = `Successfully replaced ${replacementCount} occurrence(s) in ${path}`;
|
|
118
|
+
|
|
119
|
+
if (start_line || end_line) {
|
|
120
|
+
const rangeDesc = start_line && end_line
|
|
121
|
+
? `lines ${start_line}-${end_line}`
|
|
122
|
+
: start_line
|
|
123
|
+
? `from line ${start_line}`
|
|
124
|
+
: `up to line ${end_line}`;
|
|
125
|
+
message += ` (${rangeDesc})`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (use_regex) {
|
|
129
|
+
message += ` using regex pattern`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (ignore_case) {
|
|
133
|
+
message += ` (case-insensitive)`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return FileUtils.createResponse(message);
|
|
137
|
+
|
|
138
|
+
} catch (error) {
|
|
139
|
+
throw new Error(`Failed to search and replace: ${error.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Count occurrences of a substring in a string
|
|
145
|
+
* @param {string} text - Main string
|
|
146
|
+
* @param {string} searchText - Substring to search for
|
|
147
|
+
* @returns {number} Number of occurrences
|
|
148
|
+
*/
|
|
149
|
+
static countOccurrences(text, searchText) {
|
|
150
|
+
if (!searchText) return 0;
|
|
151
|
+
return text.split(searchText).length - 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Count occurrences of a substring in a string (case-insensitive)
|
|
156
|
+
* @param {string} text - Main string
|
|
157
|
+
* @param {string} searchText - Substring to search for
|
|
158
|
+
* @returns {number} Number of occurrences
|
|
159
|
+
*/
|
|
160
|
+
static countOccurrencesCaseInsensitive(text, searchText) {
|
|
161
|
+
if (!searchText) return 0;
|
|
162
|
+
const textLower = text.toLowerCase();
|
|
163
|
+
const searchLower = searchText.toLowerCase();
|
|
164
|
+
return textLower.split(searchLower).length - 1;
|
|
165
|
+
} /**
|
|
166
|
+
* Case-insensitive string replacement
|
|
167
|
+
* @param {string} text - Main string
|
|
168
|
+
* @param {string} searchText - Substring to search for
|
|
169
|
+
* @param {string} replaceText - Replacement text
|
|
170
|
+
* @returns {string} Replaced string
|
|
171
|
+
*/
|
|
172
|
+
static replaceAllCaseInsensitive(text, searchText, replaceText) {
|
|
173
|
+
if (!searchText) return text;
|
|
174
|
+
|
|
175
|
+
const searchLower = searchText.toLowerCase();
|
|
176
|
+
const textLower = text.toLowerCase();
|
|
177
|
+
|
|
178
|
+
let result = '';
|
|
179
|
+
let lastIndex = 0;
|
|
180
|
+
|
|
181
|
+
let index = textLower.indexOf(searchLower);
|
|
182
|
+
while (index !== -1) {
|
|
183
|
+
// Add part before match
|
|
184
|
+
result += text.slice(lastIndex, index);
|
|
185
|
+
// Add replacement text
|
|
186
|
+
result += replaceText;
|
|
187
|
+
// Update position
|
|
188
|
+
lastIndex = index + searchText.length;
|
|
189
|
+
// Find next match
|
|
190
|
+
index = textLower.indexOf(searchLower, lastIndex);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Add remaining part
|
|
194
|
+
result += text.slice(lastIndex);
|
|
195
|
+
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { FileUtils } from '../utils/fileUtils.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* set_workspace tool handler
|
|
5
|
+
*/
|
|
6
|
+
export class SetWorkspaceHandler {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Handle set_workspace request
|
|
10
|
+
* @param {Object} args - Request parameters
|
|
11
|
+
* @returns {Promise<Object>} Response result
|
|
12
|
+
*/
|
|
13
|
+
static async handle(args) {
|
|
14
|
+
const { path } = args;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Set new workspace root directory
|
|
18
|
+
FileUtils.setWorkspaceRoot(path);
|
|
19
|
+
|
|
20
|
+
const message = `Successfully set workspace root to: ${FileUtils.getWorkspaceRoot()}`;
|
|
21
|
+
return FileUtils.createResponse(message);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw new Error(`Failed to set workspace: ${error.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { FileUtils } from '../utils/fileUtils.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* write_files tool handler
|
|
5
|
+
*/
|
|
6
|
+
export class WriteFileHandler {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Handle write_files request
|
|
10
|
+
* @param {Object} args - Request parameters
|
|
11
|
+
* @returns {Promise<Object>} Response result
|
|
12
|
+
*/
|
|
13
|
+
static async handle(args) {
|
|
14
|
+
const { path, content, line_count } = args;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Handle single file or multiple files
|
|
18
|
+
const paths = Array.isArray(path) ? path : [path];
|
|
19
|
+
const contents = Array.isArray(content) ? content : [content];
|
|
20
|
+
const lineCounts = Array.isArray(line_count) ? line_count : [line_count];
|
|
21
|
+
|
|
22
|
+
// Validate array length consistency
|
|
23
|
+
if (Array.isArray(path)) {
|
|
24
|
+
if (Array.isArray(content) && contents.length !== paths.length) {
|
|
25
|
+
throw new Error(`Path count (${paths.length}) doesn't match content count (${contents.length})`);
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(line_count) && lineCounts.length !== paths.length) {
|
|
28
|
+
throw new Error(`Path count (${paths.length}) doesn't match line_count array length (${lineCounts.length})`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const results = [];
|
|
33
|
+
const writeOperations = [];
|
|
34
|
+
|
|
35
|
+
// Perform all write operations
|
|
36
|
+
for (let i = 0; i < paths.length; i++) {
|
|
37
|
+
const filePath = paths[i];
|
|
38
|
+
const fileContent = Array.isArray(content) ? contents[i] : content;
|
|
39
|
+
const expectedLines = Array.isArray(line_count) ? lineCounts[i] : line_count;
|
|
40
|
+
|
|
41
|
+
// Execute write operations in parallel
|
|
42
|
+
writeOperations.push(
|
|
43
|
+
FileUtils.writeFile(filePath, fileContent).then(() => {
|
|
44
|
+
// Validate line count
|
|
45
|
+
const actualLines = FileUtils.countLines(fileContent);
|
|
46
|
+
if (actualLines !== expectedLines) {
|
|
47
|
+
console.warn(`Warning: ${filePath} - Expected ${expectedLines} lines, but got ${actualLines} lines`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
path: filePath,
|
|
52
|
+
actualLines,
|
|
53
|
+
expectedLines,
|
|
54
|
+
success: true
|
|
55
|
+
};
|
|
56
|
+
}).catch(error => {
|
|
57
|
+
return {
|
|
58
|
+
path: filePath,
|
|
59
|
+
success: false,
|
|
60
|
+
error: error.message
|
|
61
|
+
};
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Wait for all write operations to complete
|
|
67
|
+
const writeResults = await Promise.all(writeOperations);
|
|
68
|
+
|
|
69
|
+
// Check for failed operations
|
|
70
|
+
const failures = writeResults.filter(result => !result.success);
|
|
71
|
+
if (failures.length > 0) {
|
|
72
|
+
const errorMessages = failures.map(f => `${f.path}: ${f.error}`);
|
|
73
|
+
throw new Error(`Failed to write ${failures.length} file(s): ${errorMessages.join(', ')}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Build success message
|
|
77
|
+
const successResults = writeResults.filter(result => result.success);
|
|
78
|
+
const totalFiles = successResults.length;
|
|
79
|
+
const totalLines = successResults.reduce((sum, result) => sum + result.actualLines, 0);
|
|
80
|
+
|
|
81
|
+
let message;
|
|
82
|
+
if (totalFiles === 1) {
|
|
83
|
+
const result = successResults[0];
|
|
84
|
+
message = `File written successfully: ${result.path} (${result.actualLines} lines)`;
|
|
85
|
+
} else {
|
|
86
|
+
message = `Successfully wrote ${totalFiles} files (${totalLines} total lines):\n` +
|
|
87
|
+
successResults.map(result =>
|
|
88
|
+
` - ${result.path} (${result.actualLines} lines)`
|
|
89
|
+
).join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return FileUtils.createResponse(message);
|
|
93
|
+
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw new Error(`Failed to write file(s): ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { FileEditorMCPServer } from './server.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Entry point for FileEditor MCP server
|
|
6
|
+
*/
|
|
7
|
+
async function main() {
|
|
8
|
+
try {
|
|
9
|
+
const server = new FileEditorMCPServer();
|
|
10
|
+
const transport = new StdioServerTransport();
|
|
11
|
+
|
|
12
|
+
await server.start(transport);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error("Failed to start FileEditor MCP server:", error);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Start the server
|
|
20
|
+
main();
|
package/src/server.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import {
|
|
3
|
+
CallToolRequestSchema,
|
|
4
|
+
ListToolsRequestSchema,
|
|
5
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
|
|
7
|
+
import { toolDefinitions } from './tools/toolDefinitions.js';
|
|
8
|
+
import { SetWorkspaceHandler } from './handlers/setWorkspace.js';
|
|
9
|
+
import { ReadFileHandler } from './handlers/readFile.js';
|
|
10
|
+
import { WriteFileHandler } from './handlers/writeFile.js';
|
|
11
|
+
import { ListFilesHandler } from './handlers/listFiles.js';
|
|
12
|
+
import { InsertContentHandler } from './handlers/insertContent.js';
|
|
13
|
+
import { ApplyDiffHandler } from './handlers/applyDiff.js';
|
|
14
|
+
import { SearchAndReplaceHandler } from './handlers/searchAndReplace.js';
|
|
15
|
+
import { FileUtils } from './utils/fileUtils.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Main class for FileEditor MCP server
|
|
19
|
+
*/
|
|
20
|
+
export class FileEditorMCPServer {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.server = new Server(
|
|
23
|
+
{
|
|
24
|
+
name: "fileditor-mcp",
|
|
25
|
+
version: "1.0.0",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
capabilities: {
|
|
29
|
+
tools: {},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
this.setupHandlers();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set up request handlers
|
|
39
|
+
*/
|
|
40
|
+
setupHandlers() {
|
|
41
|
+
// List available tools
|
|
42
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
43
|
+
return {
|
|
44
|
+
tools: toolDefinitions
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Handle tool calls
|
|
49
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
50
|
+
try {
|
|
51
|
+
const { name, arguments: args } = request.params;
|
|
52
|
+
|
|
53
|
+
switch (name) {
|
|
54
|
+
case "set_workspace":
|
|
55
|
+
return await SetWorkspaceHandler.handle(args);
|
|
56
|
+
case "read_files":
|
|
57
|
+
return await ReadFileHandler.handle(args);
|
|
58
|
+
case "write_files":
|
|
59
|
+
return await WriteFileHandler.handle(args);
|
|
60
|
+
case "list_files":
|
|
61
|
+
return await ListFilesHandler.handle(args);
|
|
62
|
+
case "insert_contents":
|
|
63
|
+
return await InsertContentHandler.handle(args);
|
|
64
|
+
case "apply_diffs":
|
|
65
|
+
return await ApplyDiffHandler.handle(args);
|
|
66
|
+
case "search_and_replace":
|
|
67
|
+
return await SearchAndReplaceHandler.handle(args);
|
|
68
|
+
default:
|
|
69
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return FileUtils.createErrorResponse(error.message);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Start the server
|
|
79
|
+
* @param {Object} transport - Transport object
|
|
80
|
+
*/
|
|
81
|
+
async start(transport) {
|
|
82
|
+
await this.server.connect(transport);
|
|
83
|
+
console.error("FileEditor MCP server running on stdio");
|
|
84
|
+
}
|
|
85
|
+
}
|