@j0hanz/filesystem-mcp 1.11.1 → 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/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/resources/tool-info.js +23 -19
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +8 -4
- package/dist/server/bootstrap.js +39 -4
- package/dist/tools/apply-patch.js +24 -30
- package/dist/tools/calculate-hash.js +21 -24
- package/dist/tools/diff-files.js +26 -18
- package/dist/tools/edit-file.d.ts +4 -1
- package/dist/tools/edit-file.js +163 -112
- package/dist/tools/list-directory.js +105 -10
- package/dist/tools/read-multiple.d.ts +1 -1
- package/dist/tools/read-multiple.js +101 -65
- package/dist/tools/read.js +103 -75
- package/dist/tools/replace-in-files.js +85 -38
- package/dist/tools/search-content.js +245 -216
- package/dist/tools/shared.js +35 -4
- package/dist/tools/task-support.d.ts +7 -0
- package/dist/tools/task-support.js +137 -100
- 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,57 @@ 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;
|
|
94
|
+
}
|
|
95
|
+
testBuffer(buffer) {
|
|
96
|
+
if (!this.searchBuffer)
|
|
97
|
+
return true;
|
|
98
|
+
return buffer.indexOf(this.searchBuffer) !== -1;
|
|
83
99
|
}
|
|
84
|
-
|
|
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;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
}
|
|
109
|
+
replace(content, replacement) {
|
|
110
|
+
return content.replaceAll(this.searchPattern, () => replacement);
|
|
111
|
+
}
|
|
96
112
|
}
|
|
97
113
|
function buildReplacementPlan(content, replacement, matcher) {
|
|
98
114
|
const matchCount = matcher.count(content);
|
|
@@ -135,7 +151,7 @@ async function processEntry(entryPath, context) {
|
|
|
135
151
|
summary.totalMatches += plan.matchCount;
|
|
136
152
|
summary.filesChanged++;
|
|
137
153
|
recordChangedFile(summary, validPath, plan.matchCount);
|
|
138
|
-
maybeAppendPatchDiff(summary, {
|
|
154
|
+
await maybeAppendPatchDiff(summary, {
|
|
139
155
|
filePath: validPath,
|
|
140
156
|
originalContent: plan.originalContent,
|
|
141
157
|
updatedContent: plan.updatedContent,
|
|
@@ -157,24 +173,50 @@ async function processEntry(entryPath, context) {
|
|
|
157
173
|
}
|
|
158
174
|
}
|
|
159
175
|
async function readReplacementPlan(validPath, context) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
}
|
|
163
204
|
}
|
|
164
|
-
const content = await fs.readFile(validPath, {
|
|
165
|
-
encoding: 'utf-8',
|
|
166
|
-
signal: context.signal,
|
|
167
|
-
});
|
|
168
|
-
return buildReplacementPlan(content, context.replacement, context.matcher);
|
|
169
205
|
}
|
|
170
|
-
function maybeAppendPatchDiff(summary, params) {
|
|
206
|
+
async function maybeAppendPatchDiff(summary, params) {
|
|
171
207
|
if (!params.includeDiff)
|
|
172
208
|
return;
|
|
173
209
|
if (summary.diff.length >= MAX_DIFF_SIZE) {
|
|
174
210
|
summary.diffTruncated = true;
|
|
175
211
|
return;
|
|
176
212
|
}
|
|
177
|
-
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
|
+
});
|
|
178
220
|
if (summary.diff.length + patch.length <=
|
|
179
221
|
MAX_DIFF_SIZE + DIFF_APPEND_BUFFER) {
|
|
180
222
|
summary.diff += patch;
|
|
@@ -247,9 +289,14 @@ function createReplacementRegex(args) {
|
|
|
247
289
|
function createReplacementMatcher(args) {
|
|
248
290
|
const regex = createReplacementRegex(args);
|
|
249
291
|
if (regex) {
|
|
250
|
-
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);
|
|
251
298
|
}
|
|
252
|
-
return
|
|
299
|
+
return new LiteralReplacementMatcher(args.searchPattern, args.caseSensitive);
|
|
253
300
|
}
|
|
254
301
|
export async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
255
302
|
const maxFileSize = MAX_TEXT_FILE_SIZE;
|