@j0hanz/filesystem-mcp 1.13.2 → 1.14.1

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.
Files changed (74) hide show
  1. package/README.md +162 -145
  2. package/dist/cli.js +2 -2
  3. package/dist/completions.js +54 -51
  4. package/dist/config.d.ts +13 -14
  5. package/dist/config.js +12 -12
  6. package/dist/index.js +1 -1
  7. package/dist/lib/abort.d.ts +7 -0
  8. package/dist/lib/abort.js +81 -0
  9. package/dist/lib/constants.d.ts +3 -1
  10. package/dist/lib/constants.js +8 -2
  11. package/dist/lib/errors.d.ts +7 -3
  12. package/dist/lib/errors.js +64 -41
  13. package/dist/lib/file-operations/core.d.ts +3 -3
  14. package/dist/lib/file-operations/core.js +23 -20
  15. package/dist/lib/file-operations/metadata.d.ts +2 -2
  16. package/dist/lib/file-operations/metadata.js +69 -22
  17. package/dist/lib/file-operations/search.d.ts +0 -1
  18. package/dist/lib/file-operations/search.js +87 -95
  19. package/dist/lib/file-operations/traversal.js +13 -15
  20. package/dist/lib/fs-helpers.d.ts +3 -10
  21. package/dist/lib/fs-helpers.js +29 -108
  22. package/dist/lib/globs.d.ts +2 -0
  23. package/dist/lib/globs.js +19 -0
  24. package/dist/lib/logger.d.ts +28 -0
  25. package/dist/lib/logger.js +91 -0
  26. package/dist/lib/observability.d.ts +7 -0
  27. package/dist/lib/observability.js +19 -9
  28. package/dist/lib/paths.js +55 -55
  29. package/dist/lib/resource-store.js +4 -4
  30. package/dist/lib/utils.d.ts +0 -12
  31. package/dist/lib/utils.js +0 -13
  32. package/dist/lib/zod-codecs.d.ts +2 -0
  33. package/dist/lib/zod-codecs.js +18 -0
  34. package/dist/pkg-info.d.ts +1 -0
  35. package/dist/pkg-info.js +2 -2
  36. package/dist/prompts.js +3 -3
  37. package/dist/resources/generated-instructions.js +41 -41
  38. package/dist/resources/tool-catalog.js +33 -58
  39. package/dist/resources/tool-info.d.ts +0 -1
  40. package/dist/resources/tool-info.js +44 -67
  41. package/dist/resources/workflows.js +47 -19
  42. package/dist/resources.d.ts +1 -1
  43. package/dist/resources.js +4 -4
  44. package/dist/schemas.d.ts +185 -465
  45. package/dist/schemas.js +174 -206
  46. package/dist/server/bootstrap.d.ts +12 -11
  47. package/dist/server/bootstrap.js +95 -86
  48. package/dist/server/roots-manager.d.ts +5 -2
  49. package/dist/server/roots-manager.js +9 -7
  50. package/dist/server/task-store.d.ts +10 -0
  51. package/dist/server/task-store.js +73 -0
  52. package/dist/tools/apply-patch.js +39 -20
  53. package/dist/tools/calculate-hash.js +14 -27
  54. package/dist/tools/create-directory.js +11 -9
  55. package/dist/tools/delete-file.js +19 -19
  56. package/dist/tools/diff-files.js +16 -18
  57. package/dist/tools/edit-file.js +11 -5
  58. package/dist/tools/list-directory.js +16 -21
  59. package/dist/tools/move-file.js +105 -100
  60. package/dist/tools/read-multiple.js +15 -10
  61. package/dist/tools/read.js +6 -7
  62. package/dist/tools/replace-in-files.js +76 -115
  63. package/dist/tools/roots.js +3 -7
  64. package/dist/tools/search-content.js +158 -203
  65. package/dist/tools/search-files.js +59 -50
  66. package/dist/tools/shared.d.ts +10 -0
  67. package/dist/tools/shared.js +105 -36
  68. package/dist/tools/stat-many.js +15 -9
  69. package/dist/tools/stat.js +6 -6
  70. package/dist/tools/task-support.d.ts +10 -9
  71. package/dist/tools/task-support.js +94 -23
  72. package/dist/tools/tree.js +4 -4
  73. package/dist/tools/write-file.js +11 -12
  74. package/package.json +10 -9
@@ -1,19 +1,16 @@
1
- import * as fs from 'node:fs/promises';
2
- import * as path from 'node:path';
3
- import { AsyncLocalStorage } from 'node:async_hooks';
4
1
  import { Buffer } from 'node:buffer';
5
- import { performance } from 'node:perf_hooks';
2
+ import { open } from 'node:fs/promises';
3
+ import { basename, relative } from 'node:path';
6
4
  import { createTwoFilesPatch } from 'diff';
7
5
  import RE2 from 're2';
8
- import safeRegex from 'safe-regex2';
9
6
  import { DEFAULT_EXCLUDE_PATTERNS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../lib/constants.js';
10
7
  import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
11
8
  import { globEntries } from '../lib/file-operations/traversal.js';
12
9
  import { atomicWriteFile } from '../lib/fs-helpers.js';
10
+ import { Logger } from '../lib/logger.js';
13
11
  import { validateExistingPath, validatePathForWrite } from '../lib/paths.js';
14
- import { reportPeriodicProgress } from '../lib/utils.js';
15
12
  import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
16
- import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, resolveFinalProgressCurrent, resolvePathOrRoot, truncateProgressPattern, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
13
+ import { buildStructuredError, buildToolErrorResponse, buildToolResponse, createToolProgressSession, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, resolveFinalProgressCurrent, resolvePathOrRoot, truncateProgressPattern, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
17
14
  import { registerToolTaskIfAvailable } from './task-support.js';
18
15
  export const SEARCH_AND_REPLACE_TOOL = {
19
16
  name: 'search_and_replace',
@@ -26,12 +23,6 @@ export const SEARCH_AND_REPLACE_TOOL = {
26
23
  outputSchema: SearchAndReplaceOutputSchema,
27
24
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
28
25
  taskSupport: 'optional',
29
- gotchas: [
30
- 'Replaces ALL occurrences — not just the first. Use `edit` for single replacements.',
31
- ],
32
- nuances: [
33
- 'Changed-file sample and failure sample are capped/truncated in output.',
34
- ],
35
26
  };
36
27
  const MAX_FAILURES = 20;
37
28
  const REPLACE_CONCURRENCY = Math.min(PARALLEL_CONCURRENCY, 8);
@@ -44,7 +35,7 @@ function recordFailure(failures, failure) {
44
35
  failures.push(failure);
45
36
  }
46
37
  function recordChangedFile(summary, filePath, matchCount) {
47
- const relativePath = path.relative(summary.root, filePath);
38
+ const relativePath = relative(summary.root, filePath);
48
39
  if (summary.changedFiles.length < MAX_CHANGED_FILES) {
49
40
  summary.changedFiles.push({ path: relativePath, matches: matchCount });
50
41
  return;
@@ -57,67 +48,55 @@ function createRegexMatcher(pattern, caseSensitive) {
57
48
  return new RE2(pattern, flags);
58
49
  }
59
50
  catch (error) {
60
- throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
51
+ throw new McpError(ErrorCode.INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)} (RE2: no lookahead/lookbehind/backrefs)`);
61
52
  }
62
53
  }
63
- class RegexReplacementMatcher {
64
- regex;
65
- constructor(regex) {
66
- this.regex = regex;
67
- }
68
- count(content) {
69
- this.regex.lastIndex = 0;
70
- let matchCount = 0;
71
- while (this.regex.exec(content) !== null) {
72
- matchCount++;
73
- if (this.regex.lastIndex === 0) {
74
- this.regex.lastIndex++;
54
+ function createRegexReplacementMatcher(regex) {
55
+ return {
56
+ testBuffer(buffer) {
57
+ return regex.test(buffer);
58
+ },
59
+ count(content) {
60
+ regex.lastIndex = 0;
61
+ let matchCount = 0;
62
+ while (regex.exec(content) !== null) {
63
+ matchCount++;
64
+ if (regex.lastIndex === 0) {
65
+ regex.lastIndex++;
66
+ }
75
67
  }
76
- }
77
- return matchCount;
78
- }
79
- replace(content, replacement) {
80
- this.regex.lastIndex = 0;
81
- return content.replace(this.regex, replacement);
82
- }
83
- }
84
- class LiteralReplacementMatcher {
85
- searchPattern;
86
- caseSensitive;
87
- patternLength;
88
- searchBuffer;
89
- constructor(searchPattern, caseSensitive) {
90
- this.searchPattern = searchPattern;
91
- this.caseSensitive = caseSensitive;
92
- this.patternLength = searchPattern.length;
93
- this.searchBuffer = caseSensitive
94
- ? Buffer.from(searchPattern, 'utf8')
95
- : null;
96
- }
97
- testBuffer(buffer) {
98
- if (!this.searchBuffer)
99
- return true;
100
- return buffer.indexOf(this.searchBuffer) !== -1;
101
- }
102
- count(content) {
103
- let matchCount = 0;
104
- let pos = content.indexOf(this.searchPattern);
105
- while (pos !== -1) {
106
- matchCount++;
107
- pos = content.indexOf(this.searchPattern, pos + this.patternLength);
108
- }
109
- return matchCount;
110
- }
111
- replace(content, replacement) {
112
- return content.replaceAll(this.searchPattern, () => replacement);
113
- }
68
+ return matchCount;
69
+ },
70
+ replace(content, replacement) {
71
+ regex.lastIndex = 0;
72
+ return content.replace(regex, replacement);
73
+ },
74
+ };
114
75
  }
115
- const replaceContextStorage = new AsyncLocalStorage();
116
- function getReplaceContext() {
117
- const ctx = replaceContextStorage.getStore();
118
- if (!ctx)
119
- throw new Error('Replace context not found in AsyncLocalStorage');
120
- return ctx;
76
+ function createLiteralReplacementMatcher(searchPattern, caseSensitive) {
77
+ const patternLength = searchPattern.length;
78
+ const searchBuffer = caseSensitive
79
+ ? Buffer.from(searchPattern, 'utf8')
80
+ : null;
81
+ return {
82
+ testBuffer(buffer) {
83
+ if (!searchBuffer)
84
+ return true;
85
+ return buffer.indexOf(searchBuffer) !== -1;
86
+ },
87
+ count(content) {
88
+ let matchCount = 0;
89
+ let pos = content.indexOf(searchPattern);
90
+ while (pos !== -1) {
91
+ matchCount++;
92
+ pos = content.indexOf(searchPattern, pos + patternLength);
93
+ }
94
+ return matchCount;
95
+ },
96
+ replace(content, replacement) {
97
+ return content.replaceAll(searchPattern, () => replacement);
98
+ },
99
+ };
121
100
  }
122
101
  function buildReplacementPlan(content, replacement, matcher) {
123
102
  const matchCount = matcher.count(content);
@@ -133,8 +112,8 @@ function buildReplacementPlan(content, replacement, matcher) {
133
112
  function formatFileTooLargeError(filePath, size, maxFileSize) {
134
113
  return `File too large: ${filePath} (${size} bytes > ${maxFileSize} bytes)`;
135
114
  }
136
- async function processEntry(entryPath) {
137
- const { options, signal, summary } = getReplaceContext();
115
+ async function processEntry(entryPath, ctx) {
116
+ const { options, signal, summary } = ctx;
138
117
  let validPath;
139
118
  try {
140
119
  validPath = await validatePathForWrite(entryPath, signal);
@@ -143,12 +122,12 @@ async function processEntry(entryPath) {
143
122
  summary.failedFiles++;
144
123
  recordFailure(summary.failures, {
145
124
  path: entryPath,
146
- error: formatUnknownErrorMessage(error),
125
+ error: buildStructuredError(error, ErrorCode.UNKNOWN, entryPath),
147
126
  });
148
127
  return;
149
128
  }
150
129
  try {
151
- const plan = await readReplacementPlan(validPath);
130
+ const plan = await readReplacementPlan(validPath, ctx);
152
131
  if (!plan) {
153
132
  return;
154
133
  }
@@ -172,15 +151,15 @@ async function processEntry(entryPath) {
172
151
  summary.failedFiles++;
173
152
  recordFailure(summary.failures, {
174
153
  path: validPath,
175
- error: formatUnknownErrorMessage(error),
154
+ error: buildStructuredError(error, ErrorCode.UNKNOWN, validPath),
176
155
  });
177
156
  }
178
157
  }
179
- async function readReplacementPlan(validPath) {
180
- const { matcher, replacement, maxFileSize, signal } = getReplaceContext();
158
+ async function readReplacementPlan(validPath, ctx) {
159
+ const { matcher, replacement, maxFileSize, signal } = ctx;
181
160
  let fileHandle;
182
161
  try {
183
- const fd = await fs.open(validPath, 'r');
162
+ const fd = await open(validPath, 'r');
184
163
  fileHandle = fd;
185
164
  const stats = await fileHandle.stat();
186
165
  if (stats.size > maxFileSize) {
@@ -218,7 +197,7 @@ async function maybeAppendPatchDiff(summary, params) {
218
197
  const patch = await new Promise((resolve) => {
219
198
  // Defer to event loop to avoid blocking on large diffs
220
199
  setImmediate(() => {
221
- createTwoFilesPatch(path.basename(params.filePath), path.basename(params.filePath), params.originalContent, params.updatedContent, 'Original', 'Modified', {
200
+ createTwoFilesPatch(basename(params.filePath), basename(params.filePath), params.originalContent, params.updatedContent, 'Original', 'Modified', {
222
201
  callback: (res) => {
223
202
  resolve(res ?? '');
224
203
  },
@@ -234,7 +213,6 @@ async function maybeAppendPatchDiff(summary, params) {
234
213
  }
235
214
  async function processEntriesConcurrently(entries, options) {
236
215
  const pending = new Set();
237
- const seen = new Set();
238
216
  const { signal, concurrency, maxEntries, onEntry, runEntry } = options;
239
217
  let dispatched = 0;
240
218
  let stoppedByLimit = false;
@@ -250,9 +228,6 @@ async function processEntriesConcurrently(entries, options) {
250
228
  stoppedByLimit = true;
251
229
  break;
252
230
  }
253
- if (seen.has(entry.path))
254
- continue;
255
- seen.add(entry.path);
256
231
  await waitForSlot();
257
232
  onEntry();
258
233
  dispatched++;
@@ -289,22 +264,19 @@ async function resolveSearchRoot(pathValue, signal) {
289
264
  function createReplacementRegex(args) {
290
265
  if (!args.isRegex)
291
266
  return undefined;
292
- if (!safeRegex(args.searchPattern)) {
293
- throw new McpError(ErrorCode.E_INVALID_INPUT, `Unsafe regex pattern: ${args.searchPattern}`);
294
- }
295
267
  return createRegexMatcher(args.searchPattern, args.caseSensitive);
296
268
  }
297
269
  function createReplacementMatcher(args) {
298
270
  const regex = createReplacementRegex(args);
299
271
  if (regex) {
300
- return new RegexReplacementMatcher(regex);
272
+ return createRegexReplacementMatcher(regex);
301
273
  }
302
274
  if (!args.caseSensitive) {
303
275
  const escaped = args.searchPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
304
276
  const caseInsensitiveRegex = new RE2(escaped, 'gi');
305
- return new RegexReplacementMatcher(caseInsensitiveRegex);
277
+ return createRegexReplacementMatcher(caseInsensitiveRegex);
306
278
  }
307
- return new LiteralReplacementMatcher(args.searchPattern, args.caseSensitive);
279
+ return createLiteralReplacementMatcher(args.searchPattern, args.caseSensitive);
308
280
  }
309
281
  async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
310
282
  const maxFileSize = MAX_TEXT_FILE_SIZE;
@@ -314,7 +286,7 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
314
286
  cwd: root,
315
287
  pattern: args.filePattern,
316
288
  excludePatterns: args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS,
317
- includeHidden: args.includeHidden ?? false,
289
+ includeHidden: args.includeHidden,
318
290
  baseNameMatch: false,
319
291
  caseSensitiveMatch: true, // Default to sensitive for file paths
320
292
  followSymbolicLinks: false,
@@ -323,14 +295,11 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
323
295
  suppressErrors: true,
324
296
  });
325
297
  const summary = createReplaceSummary(root);
326
- const timerStartName = `searchAndReplaceStart_${Date.now()}`;
327
- const timerEndName = `searchAndReplaceEnd_${Date.now()}`;
328
- const metricName = `searchAndReplace_${Date.now()}`;
329
- performance.mark(timerStartName);
298
+ const t0 = performance.now();
330
299
  const context = {
331
300
  options: {
332
301
  dryRun: args.dryRun,
333
- returnDiff: args.returnDiff ?? false,
302
+ returnDiff: args.returnDiff,
334
303
  },
335
304
  replacement: args.replacement,
336
305
  matcher,
@@ -338,38 +307,31 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
338
307
  signal,
339
308
  summary,
340
309
  };
341
- const { stoppedByLimit } = await replaceContextStorage.run(context, () => processEntriesConcurrently(entries, {
310
+ const { stoppedByLimit } = await processEntriesConcurrently(entries, {
342
311
  signal,
343
312
  concurrency: REPLACE_CONCURRENCY,
344
313
  ...(args.maxFiles !== undefined ? { maxEntries: args.maxFiles } : {}),
345
314
  onEntry: () => {
346
315
  summary.processedFiles++;
347
- reportPeriodicProgress(onProgress, summary.processedFiles, {
348
- throttleModulo: 25,
349
- });
316
+ onProgress({ current: summary.processedFiles });
350
317
  },
351
- runEntry: processEntry,
352
- }));
353
- performance.mark(timerEndName);
354
- performance.measure(metricName, timerStartName, timerEndName);
355
- const durationMs = performance.getEntriesByName(metricName)[0]?.duration ?? 0;
356
- performance.clearMarks(timerStartName);
357
- performance.clearMarks(timerEndName);
358
- performance.clearMeasures(metricName);
359
- summary.perfTimeMs = durationMs;
318
+ runEntry: (entryPath) => processEntry(entryPath, context),
319
+ });
320
+ summary.perfTimeMs = performance.now() - t0;
360
321
  if (stoppedByLimit) {
361
322
  summary.stoppedReason = 'maxFiles';
362
323
  }
363
- reportPeriodicProgress(onProgress, summary.processedFiles, {
364
- throttleModulo: 25,
365
- force: true,
366
- });
324
+ onProgress({ current: summary.processedFiles });
325
+ if (!args.dryRun && summary.totalMatches > 0) {
326
+ Logger.info(`search_and_replace: ${summary.filesChanged} file(s), ${summary.totalMatches} match(es)`);
327
+ }
367
328
  return buildToolResponse(buildSearchAndReplaceText(summary, args.dryRun), buildSearchAndReplaceStructuredResult(summary, args));
368
329
  }
369
330
  export function registerSearchAndReplaceTool(server, options = {}) {
370
331
  const handler = (args, extra) => executeToolWithDiagnostics({
371
332
  toolName: 'search_and_replace',
372
333
  extra,
334
+ outputSchema: SearchAndReplaceOutputSchema,
373
335
  timedSignal: {},
374
336
  ...(args.path ? { context: { path: args.path } } : {}),
375
337
  run: async (signal) => {
@@ -401,7 +363,7 @@ export function registerSearchAndReplaceTool(server, options = {}) {
401
363
  throw error;
402
364
  }
403
365
  },
404
- onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
366
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
405
367
  });
406
368
  const { isInitialized } = options;
407
369
  const wrappedHandler = wrapToolHandler(handler, {
@@ -437,6 +399,5 @@ function buildSearchAndReplaceStructuredResult(summary, args) {
437
399
  : {}),
438
400
  ...(summary.diffTruncated ? { diffTruncated: true } : {}),
439
401
  ...(summary.stoppedReason ? { stoppedReason: summary.stoppedReason } : {}),
440
- dryRun: args.dryRun,
441
402
  };
442
403
  }
@@ -11,7 +11,6 @@ export const LIST_ALLOWED_DIRECTORIES_TOOL = {
11
11
  inputSchema: ListAllowedDirectoriesInputSchema,
12
12
  outputSchema: ListAllowedDirectoriesOutputSchema,
13
13
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
14
- nuances: ['Returns absolute paths of all allowed directories.'],
15
14
  taskSupport: 'forbidden',
16
15
  };
17
16
  function buildTextRoots(dirs) {
@@ -28,8 +27,6 @@ function handleListAllowedDirectories() {
28
27
  const structured = {
29
28
  ok: true,
30
29
  directories: dirs,
31
- rootsCount: dirs.length,
32
- hasMultipleRoots: dirs.length > 1,
33
30
  };
34
31
  return buildToolResponse(buildTextRoots(dirs), structured);
35
32
  }
@@ -37,8 +34,9 @@ export function registerListAllowedDirectoriesTool(server, options = {}) {
37
34
  const handler = (_args, extra) => executeToolWithDiagnostics({
38
35
  toolName: 'roots',
39
36
  extra,
37
+ outputSchema: ListAllowedDirectoriesOutputSchema,
40
38
  run: () => handleListAllowedDirectories(),
41
- onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN),
39
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN),
42
40
  });
43
41
  const wrappedHandler = wrapToolHandler(handler, {
44
42
  guard: options.isInitialized,
@@ -47,9 +45,7 @@ export function registerListAllowedDirectoriesTool(server, options = {}) {
47
45
  if (result.isError)
48
46
  return `≣ roots • failed`;
49
47
  const sc = result.structuredContent;
50
- if (!sc.ok)
51
- return `≣ roots • failed`;
52
- const count = sc.rootsCount ?? 0;
48
+ const count = sc.directories?.length ?? 0;
53
49
  return `≣ roots • ${count} ${count === 1 ? 'root' : 'roots'}`;
54
50
  },
55
51
  });