@j0hanz/filesystem-mcp 1.11.0 → 1.12.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 (42) hide show
  1. package/dist/completions.js +25 -0
  2. package/dist/lib/constants.d.ts +2 -0
  3. package/dist/lib/constants.js +2 -0
  4. package/dist/lib/file-operations/metadata.js +71 -142
  5. package/dist/lib/file-operations/search.js +119 -19
  6. package/dist/lib/file-operations/traversal.js +45 -102
  7. package/dist/lib/fs-helpers.js +175 -158
  8. package/dist/prompts.d.ts +1 -0
  9. package/dist/prompts.js +58 -0
  10. package/dist/resources/generated-instructions.js +11 -3
  11. package/dist/resources/tool-catalog.js +49 -3
  12. package/dist/resources/tool-info.d.ts +1 -0
  13. package/dist/resources/tool-info.js +139 -8
  14. package/dist/resources.js +2 -0
  15. package/dist/schemas.d.ts +2 -0
  16. package/dist/schemas.js +10 -4
  17. package/dist/server/bootstrap.js +41 -5
  18. package/dist/tools/apply-patch.js +30 -33
  19. package/dist/tools/calculate-hash.js +21 -24
  20. package/dist/tools/contract.d.ts +1 -1
  21. package/dist/tools/create-directory.js +3 -3
  22. package/dist/tools/delete-file.js +2 -2
  23. package/dist/tools/diff-files.js +32 -20
  24. package/dist/tools/edit-file.d.ts +4 -1
  25. package/dist/tools/edit-file.js +172 -104
  26. package/dist/tools/list-directory.js +105 -10
  27. package/dist/tools/move-file.js +3 -3
  28. package/dist/tools/read-multiple.d.ts +1 -1
  29. package/dist/tools/read-multiple.js +123 -99
  30. package/dist/tools/read.js +104 -76
  31. package/dist/tools/replace-in-files.d.ts +5 -2
  32. package/dist/tools/replace-in-files.js +142 -76
  33. package/dist/tools/roots.js +1 -1
  34. package/dist/tools/search-content.js +249 -218
  35. package/dist/tools/shared.js +39 -7
  36. package/dist/tools/stat-many.js +12 -19
  37. package/dist/tools/stat.js +1 -1
  38. package/dist/tools/task-support.d.ts +7 -0
  39. package/dist/tools/task-support.js +140 -101
  40. package/dist/tools/write-file.js +4 -4
  41. package/dist/tools.js +2 -2
  42. package/package.json +1 -1
@@ -1,12 +1,13 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
+ import { Buffer } from 'node:buffer';
3
4
  import { createTwoFilesPatch } from 'diff';
4
5
  import RE2 from 're2';
5
6
  import safeRegex from 'safe-regex2';
6
7
  import { DEFAULT_EXCLUDE_PATTERNS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../lib/constants.js';
7
8
  import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
8
9
  import { globEntries } from '../lib/file-operations/traversal.js';
9
- import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
10
+ import { atomicWriteFile } from '../lib/fs-helpers.js';
10
11
  import { validateExistingPath, validatePathForWrite } from '../lib/paths.js';
11
12
  import { reportPeriodicProgress } from '../lib/utils.js';
12
13
  import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
@@ -57,42 +58,68 @@ function createRegexMatcher(pattern, caseSensitive) {
57
58
  throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
58
59
  }
59
60
  }
60
- function createRegexReplacementMatcher(regex) {
61
- const count = (content) => {
62
- regex.lastIndex = 0;
61
+ class RegexReplacementMatcher {
62
+ regex;
63
+ constructor(regex) {
64
+ this.regex = regex;
65
+ }
66
+ count(content) {
67
+ this.regex.lastIndex = 0;
63
68
  let matchCount = 0;
64
- while (regex.exec(content) !== null) {
69
+ while (this.regex.exec(content) !== null) {
65
70
  matchCount++;
66
- if (regex.lastIndex === 0) {
67
- regex.lastIndex++;
71
+ if (this.regex.lastIndex === 0) {
72
+ this.regex.lastIndex++;
68
73
  }
69
74
  }
70
75
  return matchCount;
71
- };
72
- const replace = (content, replacement) => {
73
- regex.lastIndex = 0;
74
- return content.replace(regex, replacement);
75
- };
76
- return { count, replace };
76
+ }
77
+ replace(content, replacement) {
78
+ this.regex.lastIndex = 0;
79
+ return content.replace(this.regex, replacement);
80
+ }
77
81
  }
78
- function createLiteralReplacementMatcher(searchPattern, caseSensitive) {
79
- if (!caseSensitive) {
80
- const escaped = searchPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
81
- const regex = new RE2(escaped, 'gi');
82
- return createRegexReplacementMatcher(regex);
82
+ class LiteralReplacementMatcher {
83
+ searchPattern;
84
+ caseSensitive;
85
+ patternLength;
86
+ searchBuffer;
87
+ constructor(searchPattern, caseSensitive) {
88
+ this.searchPattern = searchPattern;
89
+ this.caseSensitive = caseSensitive;
90
+ this.patternLength = searchPattern.length;
91
+ this.searchBuffer = caseSensitive
92
+ ? Buffer.from(searchPattern, 'utf8')
93
+ : null;
83
94
  }
84
- const count = (content) => {
95
+ testBuffer(buffer) {
96
+ if (!this.searchBuffer)
97
+ return true;
98
+ return buffer.indexOf(this.searchBuffer) !== -1;
99
+ }
100
+ count(content) {
85
101
  let matchCount = 0;
86
- let pos = content.indexOf(searchPattern);
87
- const patternLength = searchPattern.length;
102
+ let pos = content.indexOf(this.searchPattern);
88
103
  while (pos !== -1) {
89
104
  matchCount++;
90
- pos = content.indexOf(searchPattern, pos + patternLength);
105
+ pos = content.indexOf(this.searchPattern, pos + this.patternLength);
91
106
  }
92
107
  return matchCount;
108
+ }
109
+ replace(content, replacement) {
110
+ return content.replaceAll(this.searchPattern, () => replacement);
111
+ }
112
+ }
113
+ function buildReplacementPlan(content, replacement, matcher) {
114
+ const matchCount = matcher.count(content);
115
+ if (matchCount === 0) {
116
+ return undefined;
117
+ }
118
+ return {
119
+ matchCount,
120
+ originalContent: content,
121
+ updatedContent: matcher.replace(content, replacement),
93
122
  };
94
- const replace = (content, replacement) => content.replaceAll(searchPattern, () => replacement);
95
- return { count, replace };
96
123
  }
97
124
  function formatFileTooLargeError(filePath, size, maxFileSize) {
98
125
  return `File too large: ${filePath} (${size} bytes > ${maxFileSize} bytes)`;
@@ -112,37 +139,29 @@ async function processEntry(entryPath, context) {
112
139
  return;
113
140
  }
114
141
  try {
115
- const stats = await withAbort(fs.stat(validPath), signal);
116
- if (stats.size > maxFileSize) {
117
- summary.failedFiles++;
118
- recordFailure(summary.failures, {
119
- path: validPath,
120
- error: formatFileTooLargeError(validPath, stats.size, maxFileSize),
121
- });
142
+ const plan = await readReplacementPlan(validPath, {
143
+ matcher,
144
+ replacement,
145
+ maxFileSize,
146
+ signal,
147
+ });
148
+ if (!plan) {
122
149
  return;
123
150
  }
124
- const content = await fs.readFile(validPath, {
125
- encoding: 'utf-8',
126
- signal,
151
+ summary.totalMatches += plan.matchCount;
152
+ summary.filesChanged++;
153
+ recordChangedFile(summary, validPath, plan.matchCount);
154
+ await maybeAppendPatchDiff(summary, {
155
+ filePath: validPath,
156
+ originalContent: plan.originalContent,
157
+ updatedContent: plan.updatedContent,
158
+ includeDiff: options.dryRun || options.returnDiff,
127
159
  });
128
- const matchCount = matcher.count(content);
129
- if (matchCount > 0) {
130
- summary.totalMatches += matchCount;
131
- summary.filesChanged++;
132
- recordChangedFile(summary, validPath, matchCount);
133
- const newContent = matcher.replace(content, replacement);
134
- maybeAppendPatchDiff(summary, {
135
- filePath: validPath,
136
- originalContent: content,
137
- updatedContent: newContent,
138
- includeDiff: options.dryRun || options.returnDiff,
160
+ if (!options.dryRun) {
161
+ await atomicWriteFile(validPath, plan.updatedContent, {
162
+ encoding: 'utf-8',
163
+ signal,
139
164
  });
140
- if (!options.dryRun) {
141
- await atomicWriteFile(validPath, newContent, {
142
- encoding: 'utf-8',
143
- signal,
144
- });
145
- }
146
165
  }
147
166
  }
148
167
  catch (error) {
@@ -153,14 +172,51 @@ async function processEntry(entryPath, context) {
153
172
  });
154
173
  }
155
174
  }
156
- function maybeAppendPatchDiff(summary, params) {
175
+ async function readReplacementPlan(validPath, context) {
176
+ let fileHandle;
177
+ try {
178
+ const fd = await fs.open(validPath, 'r');
179
+ fileHandle = fd;
180
+ const stats = await fileHandle.stat();
181
+ if (stats.size > context.maxFileSize) {
182
+ throw new Error(formatFileTooLargeError(validPath, stats.size, context.maxFileSize));
183
+ }
184
+ let content;
185
+ if (context.matcher.testBuffer) {
186
+ const buffer = await fileHandle.readFile({ signal: context.signal });
187
+ if (!context.matcher.testBuffer(buffer)) {
188
+ return undefined;
189
+ }
190
+ content = buffer.toString('utf-8');
191
+ }
192
+ else {
193
+ content = await fileHandle.readFile({
194
+ encoding: 'utf-8',
195
+ signal: context.signal,
196
+ });
197
+ }
198
+ return buildReplacementPlan(content, context.replacement, context.matcher);
199
+ }
200
+ finally {
201
+ if (fileHandle) {
202
+ await fileHandle.close();
203
+ }
204
+ }
205
+ }
206
+ async function maybeAppendPatchDiff(summary, params) {
157
207
  if (!params.includeDiff)
158
208
  return;
159
209
  if (summary.diff.length >= MAX_DIFF_SIZE) {
160
210
  summary.diffTruncated = true;
161
211
  return;
162
212
  }
163
- const patch = createTwoFilesPatch(path.basename(params.filePath), path.basename(params.filePath), params.originalContent, params.updatedContent, 'Original', 'Modified');
213
+ const patch = await new Promise((resolve) => {
214
+ createTwoFilesPatch(path.basename(params.filePath), path.basename(params.filePath), params.originalContent, params.updatedContent, 'Original', 'Modified', {
215
+ callback: (res) => {
216
+ resolve(res ?? '');
217
+ },
218
+ });
219
+ });
164
220
  if (summary.diff.length + patch.length <=
165
221
  MAX_DIFF_SIZE + DIFF_APPEND_BUFFER) {
166
222
  summary.diff += patch;
@@ -233,9 +289,14 @@ function createReplacementRegex(args) {
233
289
  function createReplacementMatcher(args) {
234
290
  const regex = createReplacementRegex(args);
235
291
  if (regex) {
236
- return createRegexReplacementMatcher(regex);
292
+ return new RegexReplacementMatcher(regex);
293
+ }
294
+ if (!args.caseSensitive) {
295
+ const escaped = args.searchPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
296
+ const caseInsensitiveRegex = new RE2(escaped, 'gi');
297
+ return new RegexReplacementMatcher(caseInsensitiveRegex);
237
298
  }
238
- return createLiteralReplacementMatcher(args.searchPattern, args.caseSensitive);
299
+ return new LiteralReplacementMatcher(args.searchPattern, args.caseSensitive);
239
300
  }
240
301
  export async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
241
302
  const maxFileSize = MAX_TEXT_FILE_SIZE;
@@ -283,27 +344,7 @@ export async function handleSearchAndReplace(args, signal, onProgress = () => {
283
344
  throttleModulo: 25,
284
345
  force: true,
285
346
  });
286
- const failureSuffix = summary.failedFiles > 0 ? ` (${summary.failedFiles} failed)` : '';
287
- return buildToolResponse(`Found ${summary.totalMatches} matches in ${summary.filesChanged} files${failureSuffix}.${args.dryRun ? ' (Dry run)' : ''}`, {
288
- ok: true,
289
- matches: summary.totalMatches,
290
- filesChanged: summary.filesChanged,
291
- processedFiles: summary.processedFiles,
292
- ...(summary.failedFiles > 0 ? { failedFiles: summary.failedFiles } : {}),
293
- ...(summary.failures.length > 0 ? { failures: summary.failures } : {}),
294
- ...(summary.changedFiles.length > 0
295
- ? { changedFiles: summary.changedFiles }
296
- : {}),
297
- ...(summary.changedFilesTruncated ? { changedFilesTruncated: true } : {}),
298
- ...((args.dryRun || args.returnDiff) && summary.diff
299
- ? { diff: summary.diff }
300
- : {}),
301
- ...(summary.diffTruncated ? { diffTruncated: true } : {}),
302
- ...(summary.stoppedReason
303
- ? { stoppedReason: summary.stoppedReason }
304
- : {}),
305
- dryRun: args.dryRun,
306
- });
347
+ return buildToolResponse(buildSearchAndReplaceText(summary, args.dryRun), buildSearchAndReplaceStructuredResult(summary, args));
307
348
  }
308
349
  export function registerSearchAndReplaceTool(server, options = {}) {
309
350
  const handler = (args, extra) => executeToolWithDiagnostics({
@@ -350,3 +391,28 @@ export function registerSearchAndReplaceTool(server, options = {}) {
350
391
  return;
351
392
  server.registerTool('search_and_replace', withDefaultIcons({ ...SEARCH_AND_REPLACE_TOOL }, options.iconInfo), validatedHandler);
352
393
  }
394
+ function buildSearchAndReplaceText(summary, dryRun) {
395
+ const failureSuffix = summary.failedFiles > 0 ? ` (${summary.failedFiles} failed)` : '';
396
+ const dryRunSuffix = dryRun ? ' (Dry run)' : '';
397
+ return `Found ${summary.totalMatches} matches in ${summary.filesChanged} files${failureSuffix}.${dryRunSuffix}`;
398
+ }
399
+ function buildSearchAndReplaceStructuredResult(summary, args) {
400
+ return {
401
+ ok: true,
402
+ matches: summary.totalMatches,
403
+ filesChanged: summary.filesChanged,
404
+ processedFiles: summary.processedFiles,
405
+ ...(summary.failedFiles > 0 ? { failedFiles: summary.failedFiles } : {}),
406
+ ...(summary.failures.length > 0 ? { failures: summary.failures } : {}),
407
+ ...(summary.changedFiles.length > 0
408
+ ? { changedFiles: summary.changedFiles }
409
+ : {}),
410
+ ...(summary.changedFilesTruncated ? { changedFilesTruncated: true } : {}),
411
+ ...((args.dryRun || args.returnDiff) && summary.diff
412
+ ? { diff: summary.diff }
413
+ : {}),
414
+ ...(summary.diffTruncated ? { diffTruncated: true } : {}),
415
+ ...(summary.stoppedReason ? { stoppedReason: summary.stoppedReason } : {}),
416
+ dryRun: args.dryRun,
417
+ };
418
+ }
@@ -12,7 +12,7 @@ export const LIST_ALLOWED_DIRECTORIES_TOOL = {
12
12
  outputSchema: ListAllowedDirectoriesOutputSchema,
13
13
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
14
14
  nuances: ['Returns absolute paths of all allowed directories.'],
15
- taskSupport: 'optional',
15
+ taskSupport: 'forbidden',
16
16
  };
17
17
  function buildTextRoots(dirs) {
18
18
  if (dirs.length === 0) {