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.
@@ -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
+ }