@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.
- package/dist/completions.js +25 -0
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/file-operations/metadata.js +71 -142
- package/dist/lib/file-operations/search.js +119 -19
- package/dist/lib/file-operations/traversal.js +45 -102
- package/dist/lib/fs-helpers.js +175 -158
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.js +58 -0
- package/dist/resources/generated-instructions.js +11 -3
- package/dist/resources/tool-catalog.js +49 -3
- package/dist/resources/tool-info.d.ts +1 -0
- package/dist/resources/tool-info.js +139 -8
- package/dist/resources.js +2 -0
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.js +10 -4
- package/dist/server/bootstrap.js +41 -5
- package/dist/tools/apply-patch.js +30 -33
- package/dist/tools/calculate-hash.js +21 -24
- package/dist/tools/contract.d.ts +1 -1
- package/dist/tools/create-directory.js +3 -3
- package/dist/tools/delete-file.js +2 -2
- package/dist/tools/diff-files.js +32 -20
- package/dist/tools/edit-file.d.ts +4 -1
- package/dist/tools/edit-file.js +172 -104
- package/dist/tools/list-directory.js +105 -10
- package/dist/tools/move-file.js +3 -3
- package/dist/tools/read-multiple.d.ts +1 -1
- package/dist/tools/read-multiple.js +123 -99
- package/dist/tools/read.js +104 -76
- package/dist/tools/replace-in-files.d.ts +5 -2
- package/dist/tools/replace-in-files.js +142 -76
- package/dist/tools/roots.js +1 -1
- package/dist/tools/search-content.js +249 -218
- package/dist/tools/shared.js +39 -7
- package/dist/tools/stat-many.js +12 -19
- package/dist/tools/stat.js +1 -1
- package/dist/tools/task-support.d.ts +7 -0
- package/dist/tools/task-support.js +140 -101
- package/dist/tools/write-file.js +4 -4
- package/dist/tools.js +2 -2
- 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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/tools/roots.js
CHANGED
|
@@ -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: '
|
|
15
|
+
taskSupport: 'forbidden',
|
|
16
16
|
};
|
|
17
17
|
function buildTextRoots(dirs) {
|
|
18
18
|
if (dirs.length === 0) {
|