@j0hanz/filesystem-mcp 1.1.0 → 1.1.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 CHANGED
@@ -137,6 +137,15 @@ The server communicates via `stdio`. Ensure your MCP client is configured to run
137
137
  | `apply_patch` | Apply unified patch | `path`, `patch` |
138
138
  | `search_and_replace` | Search & replace across files | `filePattern`, `searchPattern`, `replacement` |
139
139
 
140
+ ### Behavioral Notes
141
+
142
+ - `rm` with `recursive: false`:
143
+ - Deletes files and empty directories.
144
+ - Returns `E_INVALID_INPUT` for non-empty directories with guidance to use `recursive: true`.
145
+ - `includeIgnored: false` (default) for navigation/search tools:
146
+ - Excludes common generated/vendor directories such as `node_modules`, `dist`, `.git`, and similar patterns.
147
+ - Set `includeIgnored: true` to include those entries.
148
+
140
149
  ### Resources
141
150
 
142
151
  | URI Pattern | Description |
@@ -252,6 +261,12 @@ args = ["-y", "@j0hanz/filesystem-mcp@latest", "${workspaceFolder}"]
252
261
  - **Path Restrictions**: All file operations are strictly validated against the allowed root directories provided at startup.
253
262
  - **Path Validation**: Uses `isPathWithinDirectories` to prevent path traversal attacks.
254
263
  - **Hidden Files**: Hidden files (starting with `.`) are excluded by default in listings and searches unless explicitly requested.
264
+ - **Ignored Directories**: Ignored directories (for example `node_modules`, `.git`, `dist`) are excluded by default unless `includeIgnored=true`.
265
+
266
+ ## Testing Notes
267
+
268
+ - For protocol-level validation, prefer an MCP SDK client (`listTools`, `listResources`, `listPrompts`, `callTool`, `readResource`) as source-of-truth.
269
+ - Some third-party MCP CLIs may have URI parsing limitations when reading resources; if this happens, verify resource behavior through SDK client calls.
255
270
 
256
271
  ## Development Workflow
257
272
 
@@ -146,24 +146,42 @@ export const KNOWN_BINARY_EXTENSIONS = new Set([
146
146
  '.dat',
147
147
  ]);
148
148
  export const DEFAULT_EXCLUDE_PATTERNS = [
149
+ '**/node_modules',
149
150
  '**/node_modules/**',
151
+ '**/dist',
150
152
  '**/dist/**',
153
+ '**/build',
151
154
  '**/build/**',
155
+ '**/coverage',
152
156
  '**/coverage/**',
157
+ '**/.git',
153
158
  '**/.git/**',
159
+ '**/.vscode',
154
160
  '**/.vscode/**',
161
+ '**/.idea',
155
162
  '**/.idea/**',
156
163
  '**/.DS_Store',
164
+ '**/.next',
157
165
  '**/.next/**',
166
+ '**/.nuxt',
158
167
  '**/.nuxt/**',
168
+ '**/.output',
159
169
  '**/.output/**',
170
+ '**/.svelte-kit',
160
171
  '**/.svelte-kit/**',
172
+ '**/.cache',
161
173
  '**/.cache/**',
174
+ '**/.yarn',
162
175
  '**/.yarn/**',
176
+ '**/jspm_packages',
163
177
  '**/jspm_packages/**',
178
+ '**/bower_components',
164
179
  '**/bower_components/**',
180
+ '**/out',
165
181
  '**/out/**',
182
+ '**/tmp',
166
183
  '**/tmp/**',
184
+ '**/.temp',
167
185
  '**/.temp/**',
168
186
  '**/npm-debug.log',
169
187
  '**/yarn-debug.log',
package/dist/schemas.d.ts CHANGED
@@ -403,6 +403,7 @@ export declare const EditFileOutputSchema: z.ZodObject<{
403
403
  ok: z.ZodBoolean;
404
404
  path: z.ZodOptional<z.ZodString>;
405
405
  appliedEdits: z.ZodOptional<z.ZodNumber>;
406
+ lineRange: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>;
406
407
  unmatchedEdits: z.ZodOptional<z.ZodArray<z.ZodString>>;
407
408
  error: z.ZodOptional<z.ZodObject<{
408
409
  code: z.ZodEnum<{
package/dist/schemas.js CHANGED
@@ -510,6 +510,10 @@ export const EditFileOutputSchema = z.object({
510
510
  ok: z.boolean(),
511
511
  path: z.string().optional(),
512
512
  appliedEdits: z.number().optional(),
513
+ lineRange: z
514
+ .tuple([z.number(), z.number()])
515
+ .optional()
516
+ .describe('Line range modified [start, end] (1-based)'),
513
517
  unmatchedEdits: z
514
518
  .array(z.string())
515
519
  .optional()
@@ -45,7 +45,7 @@ async function handleApplyPatch(args, signal) {
45
45
  autoConvertLineEndings: args.autoConvertLineEndings,
46
46
  });
47
47
  if (patched === false) {
48
- throw new McpError(ErrorCode.E_INVALID_INPUT, `Patch application failed. The file content may have changed or the patch context is insufficient. Try enable fuzzy matching.`);
48
+ throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch application failed. The file content may have changed or patch context is insufficient. Generate a fresh patch via diff_files against the current file, then retry. If differences are minor, enable fuzzy matching (fuzzy=true or fuzzFactor).');
49
49
  }
50
50
  if (args.dryRun) {
51
51
  return buildToolResponse('Dry run successful. Patch can be applied.', {
@@ -19,10 +19,32 @@ const DELETE_FILE_TOOL = {
19
19
  };
20
20
  async function handleDeleteFile(args, signal) {
21
21
  const validPath = await validatePathForWrite(args.path, signal);
22
- await withAbort(fs.rm(validPath, {
23
- recursive: args.recursive,
24
- force: args.ignoreIfNotExists,
25
- }), signal);
22
+ let stats;
23
+ try {
24
+ stats = await withAbort(fs.lstat(validPath), signal);
25
+ }
26
+ catch (error) {
27
+ if (isNodeError(error) &&
28
+ error.code === 'ENOENT' &&
29
+ args.ignoreIfNotExists) {
30
+ return buildToolResponse(`Successfully deleted: ${args.path}`, {
31
+ ok: true,
32
+ path: validPath,
33
+ });
34
+ }
35
+ throw error;
36
+ }
37
+ if (stats.isDirectory() && !args.recursive) {
38
+ // Use rmdir for non-recursive directory deletes so non-empty directories
39
+ // consistently return ENOTEMPTY-style errors with actionable guidance.
40
+ await withAbort(fs.rmdir(validPath), signal);
41
+ }
42
+ else {
43
+ await withAbort(fs.rm(validPath, {
44
+ recursive: args.recursive,
45
+ force: args.ignoreIfNotExists,
46
+ }), signal);
47
+ }
26
48
  return buildToolResponse(`Successfully deleted: ${args.path}`, {
27
49
  ok: true,
28
50
  path: validPath,
@@ -45,6 +67,12 @@ export function registerDeleteFileTool(server, options = {}) {
45
67
  if (error.code === 'ENOTEMPTY') {
46
68
  return buildToolErrorResponse(new Error(`Directory is not empty: ${args.path}. Use recursive: true to delete non-empty directories.`), ErrorCode.E_INVALID_INPUT, args.path);
47
69
  }
70
+ if (error.code === 'EISDIR') {
71
+ return buildToolErrorResponse(new Error(`Path is a directory: ${args.path}. Use recursive: true to delete directories.`), ErrorCode.E_INVALID_INPUT, args.path);
72
+ }
73
+ if (error.code === 'EEXIST') {
74
+ return buildToolErrorResponse(new Error(`Directory is not empty: ${args.path}. Use recursive: true to delete non-empty directories.`), ErrorCode.E_INVALID_INPUT, args.path);
75
+ }
48
76
  if (error.code === 'EPERM' || error.code === 'EACCES') {
49
77
  return buildToolErrorResponse(error, ErrorCode.E_PERMISSION_DENIED, args.path);
50
78
  }
@@ -21,25 +21,45 @@ function applyEdits(content, edits) {
21
21
  let newContent = content;
22
22
  let appliedEdits = 0;
23
23
  const unmatchedEdits = [];
24
+ let minLine;
25
+ let maxLine;
24
26
  for (const edit of edits) {
25
27
  if (!newContent.includes(edit.oldText)) {
26
28
  unmatchedEdits.push(edit.oldText);
27
29
  continue;
28
30
  }
31
+ const index = newContent.indexOf(edit.oldText);
32
+ const linesBefore = newContent.slice(0, index).split('\n').length;
33
+ const newTextLines = edit.newText.split('\n').length;
34
+ const startLine = linesBefore;
35
+ const endLine = linesBefore + newTextLines - 1;
36
+ if (minLine === undefined || startLine < minLine)
37
+ minLine = startLine;
38
+ if (maxLine === undefined || endLine > maxLine)
39
+ maxLine = endLine;
29
40
  newContent = newContent.replace(edit.oldText, edit.newText);
30
41
  appliedEdits += 1;
31
42
  }
32
- return { content: newContent, appliedEdits, unmatchedEdits };
43
+ const result = {
44
+ content: newContent,
45
+ appliedEdits,
46
+ unmatchedEdits,
47
+ };
48
+ if (minLine !== undefined && maxLine !== undefined) {
49
+ result.lineRange = [minLine, maxLine];
50
+ }
51
+ return result;
33
52
  }
34
53
  async function handleEditFile(args, signal) {
35
54
  const validPath = await validateExistingPath(args.path, signal);
36
55
  const content = await fs.readFile(validPath, { encoding: 'utf-8', signal });
37
- const { content: newContent, appliedEdits, unmatchedEdits, } = applyEdits(content, args.edits);
56
+ const { content: newContent, appliedEdits, unmatchedEdits, lineRange, } = applyEdits(content, args.edits);
38
57
  const structured = {
39
58
  ok: true,
40
59
  path: validPath,
41
60
  appliedEdits,
42
61
  ...(unmatchedEdits.length > 0 ? { unmatchedEdits } : {}),
62
+ ...(lineRange ? { lineRange } : {}),
43
63
  };
44
64
  if (args.dryRun) {
45
65
  return buildToolResponse(`Dry run complete. ${appliedEdits} edits would be applied.`, structured);
@@ -68,5 +88,17 @@ export function registerEditFileTool(server, options = {}) {
68
88
  const name = path.basename(args.path);
69
89
  return `🛠 edit: ${name} (${args.edits.length} edits)`;
70
90
  },
91
+ completionMessage: (args, result) => {
92
+ const name = path.basename(args.path);
93
+ if (result.isError)
94
+ return `🛠 edit: ${name} ➟ Failed`;
95
+ const sc = result.structuredContent;
96
+ if (!sc.ok)
97
+ return `🛠 edit: ${name} ➟ Failed`;
98
+ if (sc.lineRange) {
99
+ return `🛠 edit: ${name} ➟ [${sc.lineRange[0]}-${sc.lineRange[1]}]`;
100
+ }
101
+ return `🛠 edit: ${name} ➟ (${sc.appliedEdits ?? 0} edits)`;
102
+ },
71
103
  }));
72
104
  }
@@ -202,7 +202,17 @@ export function registerSearchContentTool(server, options = {}) {
202
202
  });
203
203
  const result = await handleSearchContent(normalizedArgs, extra.signal, options.resourceStore, createProgressReporter(extra));
204
204
  const sc = result.structuredContent;
205
- const suffix = sc.ok && sc.totalMatches ? String(sc.totalMatches) : 'No matches';
205
+ const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
206
+ let suffix;
207
+ if (count === 0) {
208
+ suffix = 'No matches';
209
+ }
210
+ else if (count === 1) {
211
+ suffix = '1 match';
212
+ }
213
+ else {
214
+ suffix = `${count} matches`;
215
+ }
206
216
  const finalCurrent = (sc.filesScanned ?? 0) + 1;
207
217
  notifyProgress(extra, {
208
218
  current: finalCurrent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "mcpName": "io.github.j0hanz/filesystem-mcp",
5
5
  "description": "MCP Server that enables LLMs to interact with the local filesystem.",
6
6
  "type": "module",