@j0hanz/filesystem-mcp 1.13.2 → 1.14.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.
Files changed (43) hide show
  1. package/README.md +117 -100
  2. package/dist/config.d.ts +0 -1
  3. package/dist/lib/errors.js +5 -2
  4. package/dist/lib/file-operations/metadata.js +9 -3
  5. package/dist/lib/file-operations/search.d.ts +0 -1
  6. package/dist/lib/file-operations/search.js +5 -12
  7. package/dist/lib/fs-helpers.js +10 -11
  8. package/dist/lib/globs.d.ts +2 -0
  9. package/dist/lib/globs.js +19 -0
  10. package/dist/lib/zod-codecs.d.ts +2 -0
  11. package/dist/lib/zod-codecs.js +18 -0
  12. package/dist/pkg-info.d.ts +1 -0
  13. package/dist/pkg-info.js +2 -2
  14. package/dist/prompts.js +3 -3
  15. package/dist/resources/generated-instructions.js +3 -12
  16. package/dist/resources/tool-catalog.js +10 -41
  17. package/dist/resources/tool-info.d.ts +0 -1
  18. package/dist/resources/tool-info.js +11 -39
  19. package/dist/resources/workflows.js +8 -1
  20. package/dist/schemas.d.ts +179 -459
  21. package/dist/schemas.js +156 -165
  22. package/dist/server/roots-manager.js +1 -1
  23. package/dist/tools/apply-patch.js +19 -8
  24. package/dist/tools/calculate-hash.js +3 -5
  25. package/dist/tools/create-directory.js +1 -1
  26. package/dist/tools/delete-file.js +2 -4
  27. package/dist/tools/diff-files.js +1 -3
  28. package/dist/tools/edit-file.js +5 -2
  29. package/dist/tools/list-directory.js +10 -15
  30. package/dist/tools/move-file.js +14 -26
  31. package/dist/tools/read-multiple.js +12 -7
  32. package/dist/tools/read.js +1 -2
  33. package/dist/tools/replace-in-files.js +58 -94
  34. package/dist/tools/roots.js +2 -6
  35. package/dist/tools/search-content.js +150 -186
  36. package/dist/tools/search-files.js +5 -9
  37. package/dist/tools/shared.d.ts +7 -0
  38. package/dist/tools/shared.js +38 -11
  39. package/dist/tools/stat-many.js +6 -4
  40. package/dist/tools/stat.js +2 -2
  41. package/dist/tools/tree.js +1 -1
  42. package/dist/tools/write-file.js +1 -5
  43. package/package.json +2 -1
@@ -1,8 +1,6 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
- import { AsyncLocalStorage } from 'node:async_hooks';
4
3
  import { Buffer } from 'node:buffer';
5
- import { performance } from 'node:perf_hooks';
6
4
  import { createTwoFilesPatch } from 'diff';
7
5
  import RE2 from 're2';
8
6
  import safeRegex from 'safe-regex2';
@@ -13,7 +11,7 @@ import { atomicWriteFile } from '../lib/fs-helpers.js';
13
11
  import { validateExistingPath, validatePathForWrite } from '../lib/paths.js';
14
12
  import { reportPeriodicProgress } from '../lib/utils.js';
15
13
  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';
14
+ import { buildStructuredError, buildToolErrorResponse, buildToolResponse, createToolProgressSession, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, resolveFinalProgressCurrent, resolvePathOrRoot, truncateProgressPattern, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
17
15
  import { registerToolTaskIfAvailable } from './task-support.js';
18
16
  export const SEARCH_AND_REPLACE_TOOL = {
19
17
  name: 'search_and_replace',
@@ -26,12 +24,6 @@ export const SEARCH_AND_REPLACE_TOOL = {
26
24
  outputSchema: SearchAndReplaceOutputSchema,
27
25
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
28
26
  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
27
  };
36
28
  const MAX_FAILURES = 20;
37
29
  const REPLACE_CONCURRENCY = Math.min(PARALLEL_CONCURRENCY, 8);
@@ -60,64 +52,49 @@ function createRegexMatcher(pattern, caseSensitive) {
60
52
  throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
61
53
  }
62
54
  }
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++;
55
+ function createRegexReplacementMatcher(regex) {
56
+ return {
57
+ count(content) {
58
+ regex.lastIndex = 0;
59
+ let matchCount = 0;
60
+ while (regex.exec(content) !== null) {
61
+ matchCount++;
62
+ if (regex.lastIndex === 0) {
63
+ regex.lastIndex++;
64
+ }
75
65
  }
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
- }
66
+ return matchCount;
67
+ },
68
+ replace(content, replacement) {
69
+ regex.lastIndex = 0;
70
+ return content.replace(regex, replacement);
71
+ },
72
+ };
114
73
  }
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;
74
+ function createLiteralReplacementMatcher(searchPattern, caseSensitive) {
75
+ const patternLength = searchPattern.length;
76
+ const searchBuffer = caseSensitive
77
+ ? Buffer.from(searchPattern, 'utf8')
78
+ : null;
79
+ return {
80
+ testBuffer(buffer) {
81
+ if (!searchBuffer)
82
+ return true;
83
+ return buffer.indexOf(searchBuffer) !== -1;
84
+ },
85
+ count(content) {
86
+ let matchCount = 0;
87
+ let pos = content.indexOf(searchPattern);
88
+ while (pos !== -1) {
89
+ matchCount++;
90
+ pos = content.indexOf(searchPattern, pos + patternLength);
91
+ }
92
+ return matchCount;
93
+ },
94
+ replace(content, replacement) {
95
+ return content.replaceAll(searchPattern, () => replacement);
96
+ },
97
+ };
121
98
  }
122
99
  function buildReplacementPlan(content, replacement, matcher) {
123
100
  const matchCount = matcher.count(content);
@@ -133,8 +110,8 @@ function buildReplacementPlan(content, replacement, matcher) {
133
110
  function formatFileTooLargeError(filePath, size, maxFileSize) {
134
111
  return `File too large: ${filePath} (${size} bytes > ${maxFileSize} bytes)`;
135
112
  }
136
- async function processEntry(entryPath) {
137
- const { options, signal, summary } = getReplaceContext();
113
+ async function processEntry(entryPath, ctx) {
114
+ const { options, signal, summary } = ctx;
138
115
  let validPath;
139
116
  try {
140
117
  validPath = await validatePathForWrite(entryPath, signal);
@@ -143,12 +120,12 @@ async function processEntry(entryPath) {
143
120
  summary.failedFiles++;
144
121
  recordFailure(summary.failures, {
145
122
  path: entryPath,
146
- error: formatUnknownErrorMessage(error),
123
+ error: buildStructuredError(error, ErrorCode.E_UNKNOWN, entryPath),
147
124
  });
148
125
  return;
149
126
  }
150
127
  try {
151
- const plan = await readReplacementPlan(validPath);
128
+ const plan = await readReplacementPlan(validPath, ctx);
152
129
  if (!plan) {
153
130
  return;
154
131
  }
@@ -172,12 +149,12 @@ async function processEntry(entryPath) {
172
149
  summary.failedFiles++;
173
150
  recordFailure(summary.failures, {
174
151
  path: validPath,
175
- error: formatUnknownErrorMessage(error),
152
+ error: buildStructuredError(error, ErrorCode.E_UNKNOWN, validPath),
176
153
  });
177
154
  }
178
155
  }
179
- async function readReplacementPlan(validPath) {
180
- const { matcher, replacement, maxFileSize, signal } = getReplaceContext();
156
+ async function readReplacementPlan(validPath, ctx) {
157
+ const { matcher, replacement, maxFileSize, signal } = ctx;
181
158
  let fileHandle;
182
159
  try {
183
160
  const fd = await fs.open(validPath, 'r');
@@ -234,7 +211,6 @@ async function maybeAppendPatchDiff(summary, params) {
234
211
  }
235
212
  async function processEntriesConcurrently(entries, options) {
236
213
  const pending = new Set();
237
- const seen = new Set();
238
214
  const { signal, concurrency, maxEntries, onEntry, runEntry } = options;
239
215
  let dispatched = 0;
240
216
  let stoppedByLimit = false;
@@ -250,9 +226,6 @@ async function processEntriesConcurrently(entries, options) {
250
226
  stoppedByLimit = true;
251
227
  break;
252
228
  }
253
- if (seen.has(entry.path))
254
- continue;
255
- seen.add(entry.path);
256
229
  await waitForSlot();
257
230
  onEntry();
258
231
  dispatched++;
@@ -297,14 +270,14 @@ function createReplacementRegex(args) {
297
270
  function createReplacementMatcher(args) {
298
271
  const regex = createReplacementRegex(args);
299
272
  if (regex) {
300
- return new RegexReplacementMatcher(regex);
273
+ return createRegexReplacementMatcher(regex);
301
274
  }
302
275
  if (!args.caseSensitive) {
303
276
  const escaped = args.searchPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
304
277
  const caseInsensitiveRegex = new RE2(escaped, 'gi');
305
- return new RegexReplacementMatcher(caseInsensitiveRegex);
278
+ return createRegexReplacementMatcher(caseInsensitiveRegex);
306
279
  }
307
- return new LiteralReplacementMatcher(args.searchPattern, args.caseSensitive);
280
+ return createLiteralReplacementMatcher(args.searchPattern, args.caseSensitive);
308
281
  }
309
282
  async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
310
283
  const maxFileSize = MAX_TEXT_FILE_SIZE;
@@ -323,10 +296,7 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
323
296
  suppressErrors: true,
324
297
  });
325
298
  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);
299
+ const t0 = performance.now();
330
300
  const context = {
331
301
  options: {
332
302
  dryRun: args.dryRun,
@@ -338,7 +308,7 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
338
308
  signal,
339
309
  summary,
340
310
  };
341
- const { stoppedByLimit } = await replaceContextStorage.run(context, () => processEntriesConcurrently(entries, {
311
+ const { stoppedByLimit } = await processEntriesConcurrently(entries, {
342
312
  signal,
343
313
  concurrency: REPLACE_CONCURRENCY,
344
314
  ...(args.maxFiles !== undefined ? { maxEntries: args.maxFiles } : {}),
@@ -348,15 +318,9 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
348
318
  throttleModulo: 25,
349
319
  });
350
320
  },
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;
321
+ runEntry: (entryPath) => processEntry(entryPath, context),
322
+ });
323
+ summary.perfTimeMs = performance.now() - t0;
360
324
  if (stoppedByLimit) {
361
325
  summary.stoppedReason = 'maxFiles';
362
326
  }
@@ -370,6 +334,7 @@ export function registerSearchAndReplaceTool(server, options = {}) {
370
334
  const handler = (args, extra) => executeToolWithDiagnostics({
371
335
  toolName: 'search_and_replace',
372
336
  extra,
337
+ outputSchema: SearchAndReplaceOutputSchema,
373
338
  timedSignal: {},
374
339
  ...(args.path ? { context: { path: args.path } } : {}),
375
340
  run: async (signal) => {
@@ -437,6 +402,5 @@ function buildSearchAndReplaceStructuredResult(summary, args) {
437
402
  : {}),
438
403
  ...(summary.diffTruncated ? { diffTruncated: true } : {}),
439
404
  ...(summary.stoppedReason ? { stoppedReason: summary.stoppedReason } : {}),
440
- dryRun: args.dryRun,
441
405
  };
442
406
  }
@@ -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,6 +34,7 @@ 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
39
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN),
42
40
  });
@@ -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
  });