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,291 @@
1
+ // Tool definitions and Schema configuration
2
+ export const toolDefinitions = [
3
+ {
4
+ name: "set_workspace",
5
+ description: "Set the workspace root directory. All relative path operations will be based on this directory. You must call this tool to set the correct workspace before performing any file operations.",
6
+ inputSchema: {
7
+ type: "object",
8
+ properties: {
9
+ path: {
10
+ type: "string",
11
+ description: "The absolute path to the workspace root directory (e.g., the directory opened by VSCode)"
12
+ }
13
+ },
14
+ required: ["path"]
15
+ }
16
+ },
17
+ {
18
+ name: "read_files",
19
+ description: "Read the entire or partial content of the specified file(s). Supports reading single or multiple files at once. The returned content includes line numbers in the format 'line | content'.",
20
+ inputSchema: {
21
+ type: "object",
22
+ properties: {
23
+ path: {
24
+ oneOf: [
25
+ {
26
+ type: "string",
27
+ description: "The path of the single file to read. The path can be an absolute path or a workspace-relative path."
28
+ },
29
+ {
30
+ type: "array",
31
+ items: {
32
+ type: "string"
33
+ },
34
+ description: "An array of file paths to read. Each path can be an absolute path or a workspace-relative path."
35
+ }
36
+ ]
37
+ },
38
+ line_range: {
39
+ type: "string",
40
+ description: "Optional line range, format 'start-end' (only for single file)",
41
+ pattern: "^\\d+-\\d+$"
42
+ }
43
+ },
44
+ required: ["path"]
45
+ }
46
+ },
47
+ {
48
+ name: "write_files",
49
+ description: "Create new files or completely overwrite existing files. Supports single or multiple files at once.",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ path: {
54
+ oneOf: [
55
+ {
56
+ type: "string",
57
+ description: "The path of the single target file. The path can be an absolute path or a workspace-relative path."
58
+ },
59
+ {
60
+ type: "array",
61
+ items: {
62
+ type: "string"
63
+ },
64
+ description: "An array of target file paths. Each path can be an absolute path or a workspace-relative path."
65
+ }
66
+ ]
67
+ },
68
+ content: {
69
+ oneOf: [
70
+ {
71
+ type: "string",
72
+ description: "The full content to write, for single file or same content for all files"
73
+ },
74
+ {
75
+ type: "array",
76
+ items: {
77
+ type: "string"
78
+ },
79
+ description: "An array of content for each file, for different content per file"
80
+ }
81
+ ]
82
+ },
83
+ line_count: {
84
+ oneOf: [
85
+ {
86
+ type: "integer",
87
+ description: "Total number of lines in the file, for single file or same for all files",
88
+ minimum: 0
89
+ },
90
+ {
91
+ type: "array",
92
+ items: {
93
+ type: "integer",
94
+ minimum: 0
95
+ },
96
+ description: "An array of line counts for each file, for different line counts per file"
97
+ }
98
+ ]
99
+ }
100
+ },
101
+ required: ["path", "content", "line_count"]
102
+ }
103
+ },
104
+ {
105
+ name: "list_files",
106
+ description: "List the files and subdirectories in the specified directory.",
107
+ inputSchema: {
108
+ type: "object",
109
+ properties: {
110
+ path: {
111
+ type: "string",
112
+ description: "The directory path to list contents of. The path can be an absolute path or a workspace-relative path."
113
+ },
114
+ recursive: {
115
+ type: "boolean",
116
+ description: "Whether to recursively list subdirectory contents",
117
+ default: false
118
+ }
119
+ },
120
+ required: ["path"]
121
+ }
122
+ },
123
+ {
124
+ name: "insert_contents",
125
+ description: "Insert new content at the specified position in the file. Supports editing single or multiple files at once. Negative line numbers are supported for insertion from the end.",
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: {
129
+ path: {
130
+ oneOf: [
131
+ {
132
+ type: "string",
133
+ description: "The path of the single target file. The path can be an absolute path or a workspace-relative path."
134
+ },
135
+ {
136
+ type: "array",
137
+ items: {
138
+ type: "string"
139
+ },
140
+ description: "An array of target file paths. Each path can be an absolute path or a workspace-relative path."
141
+ }
142
+ ]
143
+ },
144
+ line: {
145
+ oneOf: [
146
+ {
147
+ type: "integer",
148
+ description: "The line number to insert (positive: 1-based, 0: end of file, negative: from end, -1 before last line), for single file"
149
+ },
150
+ {
151
+ type: "array",
152
+ items: {
153
+ type: "integer"
154
+ },
155
+ description: "An array of line numbers for each file (positive: insert, 0: end, negative: from end), for multiple files"
156
+ }
157
+ ]
158
+ },
159
+ content: {
160
+ oneOf: [
161
+ {
162
+ type: "string",
163
+ description: "The content to insert, for single file or same content for all files"
164
+ },
165
+ {
166
+ type: "array",
167
+ items: {
168
+ type: "string"
169
+ },
170
+ description: "An array of content for each file, for different content per file"
171
+ }
172
+ ]
173
+ }
174
+ },
175
+ required: ["path", "line", "content"]
176
+ }
177
+ },
178
+ {
179
+ name: "apply_diffs",
180
+ description: "Perform precise block-based search and replace operations on existing files. Supports single or multiple diff operations on a single file. When processing multiple diffs, the tool automatically handles line number offsets - all start_line values should be based on the original file structure. By default, operates in atomic mode for safe batch operations.",
181
+ inputSchema: {
182
+ type: "object",
183
+ properties: {
184
+ path: {
185
+ type: "string",
186
+ description: "The path of the file to modify. The path can be an absolute path or a workspace-relative path."
187
+ },
188
+ search_content: {
189
+ oneOf: [
190
+ {
191
+ type: "string",
192
+ description: "The original content to match precisely (for single diff)"
193
+ },
194
+ {
195
+ type: "array",
196
+ items: {
197
+ type: "string"
198
+ },
199
+ description: "Array of original content to match precisely (for multiple diffs)"
200
+ }
201
+ ]
202
+ },
203
+ replace_content: {
204
+ oneOf: [
205
+ {
206
+ type: "string",
207
+ description: "The new content to replace with (for single diff)"
208
+ },
209
+ {
210
+ type: "array",
211
+ items: {
212
+ type: "string"
213
+ },
214
+ description: "Array of new content to replace with (for multiple diffs)"
215
+ }
216
+ ]
217
+ },
218
+ start_line: {
219
+ oneOf: [
220
+ {
221
+ type: "integer",
222
+ description: "The starting line number for searching content (for single diff, 1-based line number from original file)",
223
+ minimum: 1
224
+ },
225
+ {
226
+ type: "array",
227
+ items: {
228
+ type: "integer",
229
+ minimum: 1
230
+ },
231
+ description: "Array of starting line numbers for searching content (for multiple diffs, all 1-based line numbers from original file - tool automatically handles line offset adjustments during processing)"
232
+ }
233
+ ]
234
+ },
235
+ atomic: {
236
+ type: "boolean",
237
+ description: "Whether to use atomic mode (all-or-nothing). When true (default), validates all diffs before applying any. When false, applies diffs one by one, continuing on failures.",
238
+ default: true
239
+ },
240
+ trim: {
241
+ type: "boolean",
242
+ description: "Whether to trim whitespace from the beginning and end of each line when comparing search_content with file content. Only affects search and matching - replace_content is inserted exactly as provided. Default is false.",
243
+ default: false
244
+ }
245
+ },
246
+ required: ["path", "search_content", "replace_content", "start_line"]
247
+ }
248
+ },
249
+ {
250
+ name: "search_and_replace",
251
+ description: "Search and replace text or regular expressions in a single file.",
252
+ inputSchema: {
253
+ type: "object",
254
+ properties: {
255
+ path: {
256
+ type: "string",
257
+ description: "The target file path. The path can be an absolute path or a workspace-relative path."
258
+ },
259
+ search: {
260
+ type: "string",
261
+ description: "The text or regular expression to search for"
262
+ },
263
+ replace: {
264
+ type: "string",
265
+ description: "The text to replace with"
266
+ },
267
+ use_regex: {
268
+ type: "boolean",
269
+ description: "Whether to use regular expressions for searching",
270
+ default: false
271
+ },
272
+ ignore_case: {
273
+ type: "boolean",
274
+ description: "Whether to ignore case when searching",
275
+ default: false
276
+ },
277
+ start_line: {
278
+ type: "integer",
279
+ description: "The starting line of the search range",
280
+ minimum: 1
281
+ },
282
+ end_line: {
283
+ type: "integer",
284
+ description: "The ending line of the search range",
285
+ minimum: 1
286
+ }
287
+ },
288
+ required: ["path", "search", "replace"]
289
+ }
290
+ }
291
+ ];
@@ -0,0 +1,238 @@
1
+ import fs from 'fs/promises';
2
+ import { resolve, dirname, isAbsolute, join } from 'path';
3
+ import { existsSync, lstatSync } from 'fs';
4
+ import { cwd } from 'process';
5
+ import pathIsInside from 'path-is-inside';
6
+
7
+ /**
8
+ * General file operation utility class
9
+ */
10
+ export class FileUtils {
11
+
12
+ // Current workspace root directory, initially null, must be set via set_workspace
13
+ static WORKSPACE_ROOT = null; /**
14
+ * Set workspace root directory
15
+ * @param {string} workspaceRoot - New workspace root directory path
16
+ * @throws {Error} If directory does not exist or is not accessible
17
+ */
18
+ static setWorkspaceRoot(workspaceRoot) {
19
+ // Input validation
20
+ if (!workspaceRoot || typeof workspaceRoot !== 'string') {
21
+ throw new Error('Invalid workspace root: must be a non-empty string');
22
+ }
23
+
24
+ // Prevent null bytes and other dangerous characters
25
+ if (workspaceRoot.includes('\0') || workspaceRoot.includes('\x00')) {
26
+ throw new Error('Invalid workspace root: contains null bytes');
27
+ }
28
+
29
+ const resolvedPath = resolve(workspaceRoot);
30
+
31
+ if (!existsSync(resolvedPath)) {
32
+ throw new Error(`Workspace directory does not exist: ${workspaceRoot}`);
33
+ } // Verify it is a directory, not a file
34
+ try {
35
+ const stats = lstatSync(resolvedPath);
36
+ if (!stats.isDirectory()) {
37
+ throw new Error(`Workspace path is not a directory: ${workspaceRoot}`);
38
+ }
39
+ } catch (error) {
40
+ if (error.code !== 'ENOENT') {
41
+ throw new Error(`Cannot access workspace directory: ${error.message}`);
42
+ }
43
+ }
44
+
45
+ FileUtils.WORKSPACE_ROOT = resolvedPath;
46
+ console.error(`Workspace root set to: ${FileUtils.WORKSPACE_ROOT}`);
47
+ }
48
+
49
+ /**
50
+ * Get current workspace root directory
51
+ * @returns {string} Current workspace root directory
52
+ */
53
+ static getWorkspaceRoot() {
54
+ return FileUtils.WORKSPACE_ROOT;
55
+ }
56
+
57
+ /**
58
+ * Securely resolve file path, ensuring it does not go outside the workspace
59
+ * Uses a mature third-party library to prevent path traversal attacks
60
+ * @param {string} filePath - Input file path
61
+ * @returns {string} Secure absolute path
62
+ * @throws {Error} If path tries to access files outside the workspace or workspace is not set
63
+ */
64
+ static getSecurePath(filePath) {
65
+ // First check if workspace is set
66
+ if (!FileUtils.WORKSPACE_ROOT) {
67
+ throw new Error('Workspace not set. Please call set_workspace first to establish a secure workspace root directory.');
68
+ }
69
+
70
+ // Input validation
71
+ if (!filePath || typeof filePath !== 'string') {
72
+ throw new Error('Invalid file path: must be a non-empty string');
73
+ }
74
+
75
+ // Prevent null bytes and other dangerous characters
76
+ if (filePath.includes('\0') || filePath.includes('\x00')) {
77
+ throw new Error('Invalid file path: contains null bytes');
78
+ }
79
+
80
+ let targetPath;
81
+
82
+ if (isAbsolute(filePath)) {
83
+ // Absolute path: use directly
84
+ targetPath = filePath;
85
+ } else {
86
+ // Relative path: join to workspace root
87
+ targetPath = join(FileUtils.WORKSPACE_ROOT, filePath);
88
+ }
89
+
90
+ // Resolve to canonical absolute path, handle symlinks
91
+ let resolvedPath;
92
+ try {
93
+ resolvedPath = resolve(targetPath);
94
+ } catch (error) {
95
+ throw new Error(`Failed to resolve path '${filePath}': ${error.message}`);
96
+ }
97
+
98
+ // Use mature third-party library to check if path is inside workspace
99
+ if (!pathIsInside(resolvedPath, FileUtils.WORKSPACE_ROOT) && resolvedPath !== FileUtils.WORKSPACE_ROOT) {
100
+ throw new Error(`Access denied: Path is outside the workspace boundary`);
101
+ }
102
+
103
+ return resolvedPath;
104
+ }
105
+
106
+ /**
107
+ * Check if file exists
108
+ * @param {string} filePath - File path
109
+ * @returns {boolean} Whether file exists
110
+ */
111
+ static fileExists(filePath) {
112
+ const securePath = FileUtils.getSecurePath(filePath);
113
+ return existsSync(securePath);
114
+ }
115
+
116
+ /**
117
+ * Read file content
118
+ * @param {string} filePath - File path
119
+ * @returns {Promise<string>} File content
120
+ */
121
+ static async readFile(filePath) {
122
+ const securePath = FileUtils.getSecurePath(filePath);
123
+ if (!existsSync(securePath)) {
124
+ throw new Error(`File not found: ${securePath}`);
125
+ }
126
+ return await fs.readFile(securePath, 'utf8');
127
+ }
128
+
129
+ /**
130
+ * Write file content
131
+ * @param {string} filePath - File path
132
+ * @param {string} content - File content
133
+ */
134
+ static async writeFile(filePath, content) {
135
+ const securePath = FileUtils.getSecurePath(filePath);
136
+ const dir = dirname(securePath);
137
+
138
+ // Ensure directory exists
139
+ await fs.mkdir(dir, { recursive: true });
140
+ await fs.writeFile(securePath, content, 'utf8');
141
+ }
142
+
143
+ /**
144
+ * Create MCP standard response format
145
+ * @param {string} text - Response text
146
+ * @returns {Object} MCP response format
147
+ */
148
+ static createResponse(text) {
149
+ return {
150
+ content: [
151
+ {
152
+ type: "text",
153
+ text: text
154
+ }
155
+ ]
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Create MCP error response format
161
+ * @param {string} errorMessage - Error message
162
+ * @returns {Object} MCP error response format
163
+ */
164
+ static createErrorResponse(errorMessage) {
165
+ return {
166
+ isError: true,
167
+ content: [
168
+ {
169
+ type: "text",
170
+ text: `Error: ${errorMessage}`
171
+ }
172
+ ]
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Count number of lines in a string
178
+ * @param {string} content - String content
179
+ * @returns {number} Number of lines
180
+ */
181
+ static countLines(content) {
182
+ return content.split('\n').length;
183
+ }
184
+
185
+ /**
186
+ * Get specified line range from file
187
+ * @param {string} content - File content
188
+ * @param {string} lineRange - Line range, e.g. "1-10"
189
+ * @returns {string} Content of specified range
190
+ */ static getLineRange(content, lineRange) {
191
+ // Validate line range format
192
+ if (!lineRange || !lineRange.includes('-')) {
193
+ throw new Error(`Invalid line range format: ${lineRange}. Expected format: 'start-end'`);
194
+ }
195
+
196
+ const [start, end] = lineRange.split('-').map(Number);
197
+
198
+ // Validate line numbers are valid numbers
199
+ if (isNaN(start) || isNaN(end)) {
200
+ throw new Error(`Invalid line range: ${lineRange}. Start and end must be valid numbers`);
201
+ }
202
+
203
+ // Validate line numbers are positive
204
+ if (start < 1 || end < 1) {
205
+ throw new Error(`Invalid line range: ${lineRange}. Line numbers must be positive`);
206
+ }
207
+
208
+ // Validate start line cannot be greater than end line
209
+ if (start > end) {
210
+ throw new Error(`Invalid line range: ${lineRange}. Start line cannot be greater than end line`);
211
+ }
212
+
213
+ const lines = content.split('\n');
214
+
215
+ // Validate line numbers do not exceed file range
216
+ if (start > lines.length) {
217
+ throw new Error(`Line range ${lineRange} exceeds file length (${lines.length} lines)`);
218
+ }
219
+
220
+ const selectedLines = lines.slice(start - 1, end);
221
+ return selectedLines.join('\n');
222
+ }
223
+
224
+ /**
225
+ * Format content with line numbers
226
+ * @param {string} content - Original content
227
+ * @param {number} startLine - Starting line number, default is 1
228
+ * @returns {string} Formatted content with line numbers
229
+ */
230
+ static formatWithLineNumbers(content, startLine = 1) {
231
+ const lines = content.split('\n');
232
+ const formattedLines = lines.map((line, index) => {
233
+ const lineNumber = startLine + index;
234
+ return `${lineNumber} | ${line}`;
235
+ });
236
+ return formattedLines.join('\n');
237
+ }
238
+ }