@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.
- package/README.md +117 -100
- package/dist/config.d.ts +0 -1
- package/dist/lib/errors.js +5 -2
- package/dist/lib/file-operations/metadata.js +9 -3
- package/dist/lib/file-operations/search.d.ts +0 -1
- package/dist/lib/file-operations/search.js +5 -12
- package/dist/lib/fs-helpers.js +10 -11
- package/dist/lib/globs.d.ts +2 -0
- package/dist/lib/globs.js +19 -0
- package/dist/lib/zod-codecs.d.ts +2 -0
- package/dist/lib/zod-codecs.js +18 -0
- package/dist/pkg-info.d.ts +1 -0
- package/dist/pkg-info.js +2 -2
- package/dist/prompts.js +3 -3
- package/dist/resources/generated-instructions.js +3 -12
- package/dist/resources/tool-catalog.js +10 -41
- package/dist/resources/tool-info.d.ts +0 -1
- package/dist/resources/tool-info.js +11 -39
- package/dist/resources/workflows.js +8 -1
- package/dist/schemas.d.ts +179 -459
- package/dist/schemas.js +156 -165
- package/dist/server/roots-manager.js +1 -1
- package/dist/tools/apply-patch.js +19 -8
- package/dist/tools/calculate-hash.js +3 -5
- package/dist/tools/create-directory.js +1 -1
- package/dist/tools/delete-file.js +2 -4
- package/dist/tools/diff-files.js +1 -3
- package/dist/tools/edit-file.js +5 -2
- package/dist/tools/list-directory.js +10 -15
- package/dist/tools/move-file.js +14 -26
- package/dist/tools/read-multiple.js +12 -7
- package/dist/tools/read.js +1 -2
- package/dist/tools/replace-in-files.js +58 -94
- package/dist/tools/roots.js +2 -6
- package/dist/tools/search-content.js +150 -186
- package/dist/tools/search-files.js +5 -9
- package/dist/tools/shared.d.ts +7 -0
- package/dist/tools/shared.js +38 -11
- package/dist/tools/stat-many.js +6 -4
- package/dist/tools/stat.js +2 -2
- package/dist/tools/tree.js +1 -1
- package/dist/tools/write-file.js +1 -5
- 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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return
|
|
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 } =
|
|
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:
|
|
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:
|
|
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 } =
|
|
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
|
|
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
|
|
278
|
+
return createRegexReplacementMatcher(caseInsensitiveRegex);
|
|
306
279
|
}
|
|
307
|
-
return
|
|
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
|
|
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
|
|
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.
|
|
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
|
}
|
package/dist/tools/roots.js
CHANGED
|
@@ -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
|
-
|
|
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
|
});
|