@j0hanz/filesystem-mcp 1.9.1 → 1.11.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 +10 -10
- package/dist/completions.js +140 -115
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/file-operations/metadata.d.ts +5 -1
- package/dist/lib/file-operations/metadata.js +14 -1
- package/dist/lib/file-operations/search.d.ts +2 -0
- package/dist/lib/file-operations/search.js +59 -27
- package/dist/lib/fs-helpers.d.ts +3 -1
- package/dist/lib/fs-helpers.js +63 -0
- package/dist/lib/paths.js +79 -53
- package/dist/lib/resource-store.d.ts +2 -0
- package/dist/lib/resource-store.js +58 -17
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.js +51 -0
- package/dist/resources/generated-instructions.js +36 -9
- package/dist/resources/tool-catalog.js +30 -7
- package/dist/resources/tool-info.d.ts +4 -0
- package/dist/resources/tool-info.js +21 -3
- package/dist/resources/workflows.js +17 -5
- package/dist/schemas.d.ts +36 -1
- package/dist/schemas.js +73 -3
- package/dist/server/bootstrap.js +85 -65
- package/dist/tools/apply-patch.js +135 -31
- package/dist/tools/calculate-hash.js +13 -8
- package/dist/tools/create-directory.js +14 -3
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/diff-files.js +26 -8
- package/dist/tools/edit-file.js +11 -8
- package/dist/tools/list-directory.js +1 -6
- package/dist/tools/move-file.js +39 -7
- package/dist/tools/read-multiple.js +9 -1
- package/dist/tools/read.js +38 -6
- package/dist/tools/replace-in-files.js +72 -25
- package/dist/tools/roots.js +1 -0
- package/dist/tools/search-content.js +76 -48
- package/dist/tools/search-files.js +6 -7
- package/dist/tools/shared.d.ts +2 -1
- package/dist/tools/shared.js +36 -20
- package/dist/tools/stat-many.js +1 -1
- package/dist/tools/stat.js +4 -0
- package/dist/tools/task-support.js +4 -12
- package/dist/tools/tree.js +4 -0
- package/dist/tools/write-file.js +4 -2
- package/package.json +17 -8
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://lmstudio.ai/install-mcp?name=filesystem-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBqMGhhbnovZmlsZXN5c3RlbS1tY3BAbGF0ZXN0Il19) [](https://cursor.com/en/install-mcp?name=filesystem-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBqMGhhbnovZmlsZXN5c3RlbS1tY3BAbGF0ZXN0Il19) [](https://block.github.io/goose/extension?cmd=npx&arg=-y&arg=%40j0hanz%2Ffilesystem-mcp%40latest&id=%40j0hanz%2Ffilesystem-mcp&name=filesystem-mcp&description=MCP%20Server%20that%20enables%20LLMs%20to%20interact%20with%20the%20local%20filesystem.)
|
|
8
8
|
|
|
9
|
-
MCP
|
|
9
|
+
A local filesystem MCP server that lets LLMs and AI agents read, write, search, diff, patch, and manage files safely and efficiently. Built for reliable, structured, and controlled filesystem interaction.
|
|
10
10
|
|
|
11
11
|
## Overview
|
|
12
12
|
|
|
@@ -484,7 +484,7 @@ Make precise, targeted edits to source files. Use `edit` for surgical replacemen
|
|
|
484
484
|
|
|
485
485
|
### Diff and Patch Workflow
|
|
486
486
|
|
|
487
|
-
Compare file versions and apply patches. Generate a unified diff with `diff_files`, preview with `apply_patch(dryRun: true)`, then apply.
|
|
487
|
+
Compare file versions and apply patches. Generate a unified diff with `diff_files`, preview with `apply_patch(dryRun: true)`, then apply. Supports both single-file and multi-file patches (best-effort per file with per-file `results[]`).
|
|
488
488
|
|
|
489
489
|
**Relevant tools:** `diff_files`, `apply_patch`
|
|
490
490
|
|
|
@@ -762,15 +762,15 @@ Generate a unified diff between two files. Output feeds directly into `apply_pat
|
|
|
762
762
|
|
|
763
763
|
#### `apply_patch`
|
|
764
764
|
|
|
765
|
-
Apply a unified diff patch to
|
|
765
|
+
Apply a unified diff patch to one or more files. Single-file: throws on failure. Multi-file: best-effort per file with `results[]`. Workflow: `diff_files` -> `apply_patch(dryRun)` -> `apply_patch`.
|
|
766
766
|
|
|
767
|
-
| Parameter | Type | Required | Description
|
|
768
|
-
| ------------------------ | ------- | -------- |
|
|
769
|
-
| `path` | string | **yes** | Path to file
|
|
770
|
-
| `patch` | string | **yes** | Unified diff with `@@` hunk headers
|
|
771
|
-
| `fuzzFactor` | integer | no | Max fuzzy mismatches per hunk (0-20)
|
|
772
|
-
| `autoConvertLineEndings` | boolean | no | Auto-convert line endings. Default: `true`
|
|
773
|
-
| `dryRun` | boolean | no | Validate without writing. Default: `false`
|
|
767
|
+
| Parameter | Type | Required | Description |
|
|
768
|
+
| ------------------------ | ------- | -------- | ---------------------------------------------------------- |
|
|
769
|
+
| `path` | string | **yes** | Path to file (single) or base directory (multi-file patch) |
|
|
770
|
+
| `patch` | string | **yes** | Unified diff with `@@` hunk headers (single or multi-file) |
|
|
771
|
+
| `fuzzFactor` | integer | no | Max fuzzy mismatches per hunk (0-20) |
|
|
772
|
+
| `autoConvertLineEndings` | boolean | no | Auto-convert line endings. Default: `true` |
|
|
773
|
+
| `dryRun` | boolean | no | Validate without writing. Default: `false` |
|
|
774
774
|
|
|
775
775
|
---
|
|
776
776
|
|
package/dist/completions.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getAllowedDirectories, isPathWithinDirectories, normalizePath, toPosixP
|
|
|
5
5
|
import { isRecord } from './lib/utils.js';
|
|
6
6
|
const MAX_COMPLETION_ITEMS = 100;
|
|
7
7
|
const COMPLETION_RATE_LIMIT_MS = 100;
|
|
8
|
+
const MAX_COMPLETION_CACHE_KEYS = 128;
|
|
8
9
|
// WeakMap keyed by McpServer instance so that each HTTP session gets isolated
|
|
9
10
|
// rate-limit state. In stdio mode there is a single server; in HTTP mode every
|
|
10
11
|
// session creates its own McpServer, so cross-session cache pollution is avoided.
|
|
@@ -39,6 +40,9 @@ const PATH_ARGUMENTS = new Set([
|
|
|
39
40
|
'root',
|
|
40
41
|
'cwd',
|
|
41
42
|
]);
|
|
43
|
+
const DESTINATION_CONTEXT_KEYS = ['source', 'path', 'cwd', 'root'];
|
|
44
|
+
const PRIMARY_PATH_CONTEXT_KEYS = ['path', 'cwd', 'root'];
|
|
45
|
+
const DEFAULT_CONTEXT_KEYS = ['path', 'source', 'cwd', 'root'];
|
|
42
46
|
function isPathLikeArgumentName(argName) {
|
|
43
47
|
return (PATH_ARGUMENTS.has(argName) ||
|
|
44
48
|
argName.endsWith('paths') ||
|
|
@@ -48,6 +52,22 @@ function isPathLikeArgumentName(argName) {
|
|
|
48
52
|
argName.endsWith('dirs') ||
|
|
49
53
|
argName.endsWith('dir'));
|
|
50
54
|
}
|
|
55
|
+
function isTemplateVariableChar(char) {
|
|
56
|
+
const code = char.charCodeAt(0);
|
|
57
|
+
const isDigit = code >= 48 && code <= 57;
|
|
58
|
+
const isUpper = code >= 65 && code <= 90;
|
|
59
|
+
const isLower = code >= 97 && code <= 122;
|
|
60
|
+
return isDigit || isUpper || isLower || code === 95;
|
|
61
|
+
}
|
|
62
|
+
function normalizeTemplateVariable(raw) {
|
|
63
|
+
let normalized = '';
|
|
64
|
+
for (const char of raw) {
|
|
65
|
+
if (isTemplateVariableChar(char)) {
|
|
66
|
+
normalized += char.toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return normalized;
|
|
70
|
+
}
|
|
51
71
|
function parseResourceReference(value) {
|
|
52
72
|
if (!isRecord(value))
|
|
53
73
|
return undefined;
|
|
@@ -60,13 +80,6 @@ function parseResourceReference(value) {
|
|
|
60
80
|
}
|
|
61
81
|
function extractTemplateVariables(uri) {
|
|
62
82
|
const vars = [];
|
|
63
|
-
const isVariableChar = (char) => {
|
|
64
|
-
const code = char.charCodeAt(0);
|
|
65
|
-
const isDigit = code >= 48 && code <= 57;
|
|
66
|
-
const isUpper = code >= 65 && code <= 90;
|
|
67
|
-
const isLower = code >= 97 && code <= 122;
|
|
68
|
-
return isDigit || isUpper || isLower || code === 95;
|
|
69
|
-
};
|
|
70
83
|
let index = 0;
|
|
71
84
|
while (index < uri.length) {
|
|
72
85
|
const start = uri.indexOf('{', index);
|
|
@@ -75,13 +88,7 @@ function extractTemplateVariables(uri) {
|
|
|
75
88
|
const end = uri.indexOf('}', start + 1);
|
|
76
89
|
if (end === -1)
|
|
77
90
|
break;
|
|
78
|
-
const
|
|
79
|
-
let normalized = '';
|
|
80
|
-
for (const char of raw) {
|
|
81
|
-
if (isVariableChar(char)) {
|
|
82
|
-
normalized += char.toLowerCase();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
91
|
+
const normalized = normalizeTemplateVariable(uri.slice(start + 1, end));
|
|
85
92
|
if (normalized.length > 0)
|
|
86
93
|
vars.push(normalized);
|
|
87
94
|
index = end + 1;
|
|
@@ -129,6 +136,43 @@ function extractContextArguments(value) {
|
|
|
129
136
|
return undefined;
|
|
130
137
|
return normalized;
|
|
131
138
|
}
|
|
139
|
+
function serializeCompletionRef(ref) {
|
|
140
|
+
if (!isRecord(ref) || typeof ref['type'] !== 'string') {
|
|
141
|
+
return { type: 'unknown' };
|
|
142
|
+
}
|
|
143
|
+
if (ref['type'] === 'ref/prompt' && typeof ref['name'] === 'string') {
|
|
144
|
+
return { type: ref['type'], name: ref['name'] };
|
|
145
|
+
}
|
|
146
|
+
if (ref['type'] === 'ref/resource' && typeof ref['uri'] === 'string') {
|
|
147
|
+
return { type: ref['type'], uri: ref['uri'] };
|
|
148
|
+
}
|
|
149
|
+
return { type: ref['type'] };
|
|
150
|
+
}
|
|
151
|
+
function serializeContextArguments(contextArguments) {
|
|
152
|
+
if (!contextArguments)
|
|
153
|
+
return [];
|
|
154
|
+
return Object.entries(contextArguments).sort(([left], [right]) => left.localeCompare(right));
|
|
155
|
+
}
|
|
156
|
+
function buildCompletionCacheKey(params) {
|
|
157
|
+
return JSON.stringify({
|
|
158
|
+
argumentName: params.argumentName.toLowerCase(),
|
|
159
|
+
value: params.value,
|
|
160
|
+
ref: serializeCompletionRef(params.ref),
|
|
161
|
+
contextArguments: serializeContextArguments(params.contextArguments),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function rememberCompletionCacheValue(cache, key, value) {
|
|
165
|
+
if (cache.has(key)) {
|
|
166
|
+
cache.delete(key);
|
|
167
|
+
}
|
|
168
|
+
cache.set(key, value);
|
|
169
|
+
while (cache.size > MAX_COMPLETION_CACHE_KEYS) {
|
|
170
|
+
const oldest = cache.keys().next();
|
|
171
|
+
if (oldest.done)
|
|
172
|
+
break;
|
|
173
|
+
cache.delete(oldest.value);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
132
176
|
function hasTrailingSeparator(value) {
|
|
133
177
|
return (value.endsWith(path.sep) || value.endsWith('/') || value.endsWith('\\'));
|
|
134
178
|
}
|
|
@@ -180,16 +224,19 @@ function findAllowedRootByName(rootName, allowed) {
|
|
|
180
224
|
function chooseContextKeys(argumentName) {
|
|
181
225
|
const normalized = argumentName.toLowerCase();
|
|
182
226
|
if (normalized === 'destination') {
|
|
183
|
-
return [
|
|
227
|
+
return [...DESTINATION_CONTEXT_KEYS];
|
|
184
228
|
}
|
|
185
229
|
if (normalized === 'path' ||
|
|
186
230
|
normalized === 'source' ||
|
|
187
231
|
normalized === 'original' ||
|
|
188
232
|
normalized === 'modified' ||
|
|
189
233
|
normalized === 'file') {
|
|
190
|
-
return [
|
|
234
|
+
return [...PRIMARY_PATH_CONTEXT_KEYS];
|
|
191
235
|
}
|
|
192
|
-
return [
|
|
236
|
+
return [...DEFAULT_CONTEXT_KEYS];
|
|
237
|
+
}
|
|
238
|
+
function hasContextArguments(contextArguments) {
|
|
239
|
+
return (contextArguments !== undefined && Object.keys(contextArguments).length > 0);
|
|
193
240
|
}
|
|
194
241
|
function resolveContextCandidatePath(candidate, allowed) {
|
|
195
242
|
if (isAbsolutePathInput(candidate)) {
|
|
@@ -218,17 +265,7 @@ async function toAllowedContextDirectory(resolved, allowed) {
|
|
|
218
265
|
return isPathWithinDirectories(parent, allowed) ? parent : undefined;
|
|
219
266
|
}
|
|
220
267
|
async function resolveContextBaseDirectory(argumentName, contextArguments, allowed) {
|
|
221
|
-
if (!contextArguments) {
|
|
222
|
-
return undefined;
|
|
223
|
-
}
|
|
224
|
-
let hasContextArgument = false;
|
|
225
|
-
for (const key in contextArguments) {
|
|
226
|
-
if (!Object.prototype.hasOwnProperty.call(contextArguments, key))
|
|
227
|
-
continue;
|
|
228
|
-
hasContextArgument = true;
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
if (!hasContextArgument) {
|
|
268
|
+
if (!hasContextArguments(contextArguments)) {
|
|
232
269
|
return undefined;
|
|
233
270
|
}
|
|
234
271
|
const keys = chooseContextKeys(argumentName);
|
|
@@ -245,6 +282,54 @@ async function resolveContextBaseDirectory(argumentName, contextArguments, allow
|
|
|
245
282
|
}
|
|
246
283
|
return undefined;
|
|
247
284
|
}
|
|
285
|
+
function withDirectorySeparator(value) {
|
|
286
|
+
return value.endsWith(path.sep) ? value : `${value}${path.sep}`;
|
|
287
|
+
}
|
|
288
|
+
function buildCompletionResult(values) {
|
|
289
|
+
return {
|
|
290
|
+
values: values.slice(0, MAX_COMPLETION_ITEMS),
|
|
291
|
+
total: values.length,
|
|
292
|
+
hasMore: values.length > MAX_COMPLETION_ITEMS,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function buildCompletionResponse(result) {
|
|
296
|
+
return { completion: result };
|
|
297
|
+
}
|
|
298
|
+
function sortCompletionMatches(matches) {
|
|
299
|
+
matches.sort((left, right) => {
|
|
300
|
+
const leftIsDir = left.endsWith(path.sep);
|
|
301
|
+
const rightIsDir = right.endsWith(path.sep);
|
|
302
|
+
if (leftIsDir && !rightIsDir)
|
|
303
|
+
return -1;
|
|
304
|
+
if (!leftIsDir && rightIsDir)
|
|
305
|
+
return 1;
|
|
306
|
+
return left.localeCompare(right);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function mergeCompletionMatches(...matchGroups) {
|
|
310
|
+
const uniqueMatches = new Set();
|
|
311
|
+
for (const group of matchGroups) {
|
|
312
|
+
for (const match of group) {
|
|
313
|
+
uniqueMatches.add(match);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const merged = [...uniqueMatches];
|
|
317
|
+
sortCompletionMatches(merged);
|
|
318
|
+
return merged;
|
|
319
|
+
}
|
|
320
|
+
function getRootPrefix(currentValue) {
|
|
321
|
+
const normalizedInput = toPosixPath(currentValue);
|
|
322
|
+
return (normalizedInput.split('/')[0] ?? '').toLowerCase();
|
|
323
|
+
}
|
|
324
|
+
function collectAllowedRoots(allowed, predicate) {
|
|
325
|
+
const matches = [];
|
|
326
|
+
for (const root of allowed) {
|
|
327
|
+
if (predicate(root)) {
|
|
328
|
+
matches.push(withDirectorySeparator(root));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return matches;
|
|
332
|
+
}
|
|
248
333
|
function getSearchContext(currentValue, allowed, contextBase) {
|
|
249
334
|
const trailingSeparator = hasTrailingSeparator(currentValue);
|
|
250
335
|
if (isAbsolutePathInput(currentValue)) {
|
|
@@ -290,38 +375,22 @@ async function findMatchesInDirectory(searchDir, prefix, allowed) {
|
|
|
290
375
|
return matches;
|
|
291
376
|
}
|
|
292
377
|
function findRootPrefixMatches(currentValue, allowed) {
|
|
293
|
-
const
|
|
294
|
-
const rootPrefix = (normalizedInput.split('/')[0] ?? '').toLowerCase();
|
|
378
|
+
const rootPrefix = getRootPrefix(currentValue);
|
|
295
379
|
if (!rootPrefix) {
|
|
296
|
-
|
|
297
|
-
for (const root of allowed) {
|
|
298
|
-
matches.push(`${root}${path.sep}`);
|
|
299
|
-
}
|
|
300
|
-
return matches;
|
|
380
|
+
return collectAllowedRoots(allowed, () => true);
|
|
301
381
|
}
|
|
302
|
-
|
|
303
|
-
for (const root of allowed) {
|
|
304
|
-
if (!path.basename(root).toLowerCase().startsWith(rootPrefix))
|
|
305
|
-
continue;
|
|
306
|
-
matches.push(`${root}${path.sep}`);
|
|
307
|
-
}
|
|
308
|
-
return matches;
|
|
382
|
+
return collectAllowedRoots(allowed, (root) => path.basename(root).toLowerCase().startsWith(rootPrefix));
|
|
309
383
|
}
|
|
310
384
|
function findMatchingRoots(searchDir, prefix, allowed) {
|
|
311
|
-
const matches = [];
|
|
312
385
|
const lowerPrefix = prefix.toLowerCase();
|
|
313
386
|
const normalizedSearchDir = normalizePath(searchDir);
|
|
314
|
-
|
|
387
|
+
return collectAllowedRoots(allowed, (root) => {
|
|
315
388
|
const rootDir = path.dirname(root);
|
|
316
389
|
// Check if root is a direct child of searchDir
|
|
317
|
-
if (normalizePath(rootDir)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return matches;
|
|
390
|
+
if (normalizePath(rootDir) !== normalizedSearchDir)
|
|
391
|
+
return false;
|
|
392
|
+
return path.basename(root).toLowerCase().startsWith(lowerPrefix);
|
|
393
|
+
});
|
|
325
394
|
}
|
|
326
395
|
export async function getPathCompletions(currentValue, options = {}) {
|
|
327
396
|
const allowed = getAllowedDirectories();
|
|
@@ -329,47 +398,16 @@ export async function getPathCompletions(currentValue, options = {}) {
|
|
|
329
398
|
const contextBase = await resolveContextBaseDirectory(options.argumentName ?? '', options.contextArguments, allowed);
|
|
330
399
|
// If no value and no context base, suggest roots.
|
|
331
400
|
if (!currentValue && !contextBase) {
|
|
332
|
-
return
|
|
333
|
-
values: allowed,
|
|
334
|
-
total: allowed.length,
|
|
335
|
-
hasMore: false,
|
|
336
|
-
};
|
|
401
|
+
return buildCompletionResult(allowed);
|
|
337
402
|
}
|
|
338
403
|
const context = getSearchContext(currentValue, allowed, contextBase);
|
|
339
404
|
if (!context) {
|
|
340
|
-
|
|
341
|
-
const sliced = rootMatches.slice(0, MAX_COMPLETION_ITEMS);
|
|
342
|
-
return {
|
|
343
|
-
values: sliced,
|
|
344
|
-
total: rootMatches.length,
|
|
345
|
-
hasMore: rootMatches.length > MAX_COMPLETION_ITEMS,
|
|
346
|
-
};
|
|
405
|
+
return buildCompletionResult(findRootPrefixMatches(currentValue, allowed));
|
|
347
406
|
}
|
|
348
407
|
const { searchDir, prefix } = context;
|
|
349
408
|
const dirMatches = await findMatchesInDirectory(searchDir, prefix, allowed);
|
|
350
409
|
const rootMatches = findMatchingRoots(searchDir, prefix, allowed);
|
|
351
|
-
|
|
352
|
-
const unique = new Set();
|
|
353
|
-
for (const match of dirMatches)
|
|
354
|
-
unique.add(match);
|
|
355
|
-
for (const match of rootMatches)
|
|
356
|
-
unique.add(match);
|
|
357
|
-
const uniqueMatches = Array.from(unique);
|
|
358
|
-
uniqueMatches.sort((a, b) => {
|
|
359
|
-
const aIsDir = a.endsWith(path.sep);
|
|
360
|
-
const bIsDir = b.endsWith(path.sep);
|
|
361
|
-
if (aIsDir && !bIsDir)
|
|
362
|
-
return -1;
|
|
363
|
-
if (!aIsDir && bIsDir)
|
|
364
|
-
return 1;
|
|
365
|
-
return a.localeCompare(b);
|
|
366
|
-
});
|
|
367
|
-
const sliced = uniqueMatches.slice(0, MAX_COMPLETION_ITEMS);
|
|
368
|
-
return {
|
|
369
|
-
values: sliced,
|
|
370
|
-
total: uniqueMatches.length,
|
|
371
|
-
hasMore: uniqueMatches.length > MAX_COMPLETION_ITEMS,
|
|
372
|
-
};
|
|
410
|
+
return buildCompletionResult(mergeCompletionMatches(dirMatches, rootMatches));
|
|
373
411
|
}
|
|
374
412
|
catch {
|
|
375
413
|
return { values: [] };
|
|
@@ -387,50 +425,37 @@ export function registerCompletions(server, instructions = '') {
|
|
|
387
425
|
const filtered = currentValue
|
|
388
426
|
? topicValues.filter((v) => v.startsWith(currentValue))
|
|
389
427
|
: topicValues;
|
|
390
|
-
|
|
391
|
-
return {
|
|
392
|
-
completion: {
|
|
393
|
-
values: sliced,
|
|
394
|
-
total: filtered.length,
|
|
395
|
-
hasMore: filtered.length > MAX_COMPLETION_ITEMS,
|
|
396
|
-
},
|
|
397
|
-
};
|
|
428
|
+
return buildCompletionResponse(buildCompletionResult(filtered));
|
|
398
429
|
}
|
|
399
430
|
const isPathArg = isPathLikeArgumentName(argName) ||
|
|
400
431
|
isPathArgumentFromReference(argName, ref);
|
|
401
432
|
if (!isPathArg) {
|
|
402
|
-
return
|
|
433
|
+
return buildCompletionResponse(buildCompletionResult([]));
|
|
403
434
|
}
|
|
435
|
+
const contextArguments = extractContextArguments(params.context);
|
|
436
|
+
const cacheKey = buildCompletionCacheKey({
|
|
437
|
+
argumentName: argName,
|
|
438
|
+
value: argument.value,
|
|
439
|
+
ref,
|
|
440
|
+
...(contextArguments ? { contextArguments } : {}),
|
|
441
|
+
});
|
|
404
442
|
const now = Date.now();
|
|
405
443
|
const sessionState = getCompletionState(server);
|
|
406
|
-
const lastCallMs = sessionState.lastCallMs.get(
|
|
444
|
+
const lastCallMs = sessionState.lastCallMs.get(cacheKey) ?? 0;
|
|
407
445
|
if (now - lastCallMs < COMPLETION_RATE_LIMIT_MS) {
|
|
408
|
-
const lastResult = sessionState.lastResult.get(
|
|
446
|
+
const lastResult = sessionState.lastResult.get(cacheKey);
|
|
409
447
|
if (lastResult) {
|
|
410
|
-
return
|
|
411
|
-
completion: {
|
|
412
|
-
values: lastResult.values,
|
|
413
|
-
total: lastResult.total,
|
|
414
|
-
hasMore: lastResult.hasMore,
|
|
415
|
-
},
|
|
416
|
-
};
|
|
448
|
+
return buildCompletionResponse(lastResult);
|
|
417
449
|
}
|
|
418
|
-
return
|
|
450
|
+
return buildCompletionResponse(buildCompletionResult([]));
|
|
419
451
|
}
|
|
420
|
-
sessionState.lastCallMs
|
|
421
|
-
const contextArguments = extractContextArguments(params.context);
|
|
452
|
+
rememberCompletionCacheValue(sessionState.lastCallMs, cacheKey, now);
|
|
422
453
|
const { value } = argument;
|
|
423
454
|
const completions = await getPathCompletions(value, {
|
|
424
455
|
argumentName: argName,
|
|
425
456
|
...(contextArguments ? { contextArguments } : {}),
|
|
426
457
|
});
|
|
427
|
-
sessionState.lastResult
|
|
428
|
-
return
|
|
429
|
-
completion: {
|
|
430
|
-
values: completions.values,
|
|
431
|
-
total: completions.total,
|
|
432
|
-
hasMore: completions.hasMore,
|
|
433
|
-
},
|
|
434
|
-
};
|
|
458
|
+
rememberCompletionCacheValue(sessionState.lastResult, cacheKey, completions);
|
|
459
|
+
return buildCompletionResponse(completions);
|
|
435
460
|
});
|
|
436
461
|
}
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export declare function parseTrueEnvFlag(value: string | undefined): boolean;
|
|
|
2
2
|
export declare function parseEnvInt(envVar: string, defaultValue: number, min: number, max: number): number;
|
|
3
3
|
export declare const DEFAULT_LOG_LEVEL: "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency";
|
|
4
4
|
export declare const REQUIRED_MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
5
|
+
export declare const DEFAULT_TASK_TTL_MS: number;
|
|
5
6
|
export declare const PARALLEL_CONCURRENCY: number;
|
|
6
7
|
export declare const MAX_SEARCHABLE_FILE_SIZE: number;
|
|
7
8
|
export declare const MAX_TEXT_FILE_SIZE: number;
|
package/dist/lib/constants.js
CHANGED
|
@@ -71,6 +71,8 @@ function parseEnvLogLevel(envVar, defaultValue) {
|
|
|
71
71
|
}
|
|
72
72
|
export const DEFAULT_LOG_LEVEL = parseEnvLogLevel('FILESYSTEM_MCP_LOG_LEVEL', 'info');
|
|
73
73
|
export const REQUIRED_MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
74
|
+
// Default TTL for MCP tasks when the client does not specify one (5 minutes).
|
|
75
|
+
export const DEFAULT_TASK_TTL_MS = 5 * 60 * 1000;
|
|
74
76
|
// Auto-tuned parallelism based on CPU cores (no env override)
|
|
75
77
|
const BYTES_PER_PARALLEL_TASK = 64 * MIB;
|
|
76
78
|
const BYTES_PER_SEARCH_WORKER = 128 * MIB;
|
|
@@ -24,6 +24,7 @@ interface TreeEntry {
|
|
|
24
24
|
name: string;
|
|
25
25
|
type: EntryType;
|
|
26
26
|
relativePath: string;
|
|
27
|
+
size?: number;
|
|
27
28
|
children?: TreeEntry[];
|
|
28
29
|
}
|
|
29
30
|
interface TreeOptions {
|
|
@@ -31,6 +32,7 @@ interface TreeOptions {
|
|
|
31
32
|
maxEntries?: number;
|
|
32
33
|
includeHidden?: boolean;
|
|
33
34
|
includeIgnored?: boolean;
|
|
35
|
+
includeSizes?: boolean;
|
|
34
36
|
timeoutMs?: number;
|
|
35
37
|
signal?: AbortSignal;
|
|
36
38
|
onProgress?: (progress: {
|
|
@@ -51,8 +53,9 @@ interface ReadMultipleResult {
|
|
|
51
53
|
content?: string;
|
|
52
54
|
truncated?: boolean;
|
|
53
55
|
totalLines?: number;
|
|
54
|
-
readMode?: 'full' | 'head' | 'range';
|
|
56
|
+
readMode?: 'full' | 'head' | 'tail' | 'range';
|
|
55
57
|
head?: number;
|
|
58
|
+
tail?: number;
|
|
56
59
|
startLine?: number;
|
|
57
60
|
endLine?: number;
|
|
58
61
|
linesRead?: number;
|
|
@@ -64,6 +67,7 @@ interface ReadMultipleOptions {
|
|
|
64
67
|
maxSize?: number;
|
|
65
68
|
maxTotalSize?: number;
|
|
66
69
|
head?: number;
|
|
70
|
+
tail?: number;
|
|
67
71
|
startLine?: number;
|
|
68
72
|
endLine?: number;
|
|
69
73
|
signal?: AbortSignal;
|
|
@@ -408,6 +408,7 @@ function normalizeTreeOptions(options) {
|
|
|
408
408
|
maxEntries: toSafeNonNegativeInt(options.maxEntries, 1000),
|
|
409
409
|
includeHidden: toSafeBoolean(options.includeHidden, false),
|
|
410
410
|
includeIgnored: toSafeBoolean(options.includeIgnored, false),
|
|
411
|
+
includeSizes: toSafeBoolean(options.includeSizes, false),
|
|
411
412
|
timeoutMs: toSafePositiveInt(options.timeoutMs, DEFAULT_SEARCH_TIMEOUT_MS),
|
|
412
413
|
};
|
|
413
414
|
}
|
|
@@ -608,7 +609,7 @@ export async function treeDirectory(dirPath, options = {}) {
|
|
|
608
609
|
maxDepth: normalized.maxDepth,
|
|
609
610
|
followSymbolicLinks: false,
|
|
610
611
|
onlyFiles: false,
|
|
611
|
-
stats:
|
|
612
|
+
stats: normalized.includeSizes,
|
|
612
613
|
suppressErrors: true,
|
|
613
614
|
});
|
|
614
615
|
for await (const entry of stream) {
|
|
@@ -629,6 +630,14 @@ export async function treeDirectory(dirPath, options = {}) {
|
|
|
629
630
|
}
|
|
630
631
|
const parent = ensureParentNodes(rootNode, nodeByPath, resolved.relativePosix);
|
|
631
632
|
upsertChildNode(parent, nodeByPath, resolved, childPathIndexByParent);
|
|
633
|
+
if (normalized.includeSizes &&
|
|
634
|
+
resolved.type === 'file' &&
|
|
635
|
+
entry.stats) {
|
|
636
|
+
const node = nodeByPath.get(resolved.relativePosix);
|
|
637
|
+
if (node) {
|
|
638
|
+
node.size = entry.stats.size;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
632
641
|
totalEntries += 1;
|
|
633
642
|
options.onProgress?.({ current: totalEntries });
|
|
634
643
|
}
|
|
@@ -665,6 +674,8 @@ function buildReadMultipleResult(filePath, result) {
|
|
|
665
674
|
output.totalLines = result.totalLines;
|
|
666
675
|
if (result.head !== undefined)
|
|
667
676
|
output.head = result.head;
|
|
677
|
+
if (result.tail !== undefined)
|
|
678
|
+
output.tail = result.tail;
|
|
668
679
|
if (result.startLine !== undefined)
|
|
669
680
|
output.startLine = result.startLine;
|
|
670
681
|
if (result.endLine !== undefined)
|
|
@@ -709,6 +720,8 @@ function normalizeReadMultipleOptions(options) {
|
|
|
709
720
|
function applyLineSelection(target, source) {
|
|
710
721
|
if (source.head !== undefined)
|
|
711
722
|
target.head = source.head;
|
|
723
|
+
if (source.tail !== undefined)
|
|
724
|
+
target.tail = source.tail;
|
|
712
725
|
if (source.startLine !== undefined)
|
|
713
726
|
target.startLine = source.startLine;
|
|
714
727
|
if (source.endLine !== undefined)
|
|
@@ -5,6 +5,7 @@ export declare const MatcherOptionsSchema: z.ZodObject<{
|
|
|
5
5
|
caseSensitive: z.ZodBoolean;
|
|
6
6
|
wholeWord: z.ZodBoolean;
|
|
7
7
|
isLiteral: z.ZodBoolean;
|
|
8
|
+
multiline: z.ZodBoolean;
|
|
8
9
|
}, z.core.$strict>;
|
|
9
10
|
export type MatcherOptions = z.infer<typeof MatcherOptionsSchema>;
|
|
10
11
|
export type Matcher = (line: string) => number;
|
|
@@ -27,6 +28,7 @@ declare const SearchOptionsSchema: z.ZodObject<{
|
|
|
27
28
|
contextLines: z.ZodInt;
|
|
28
29
|
wholeWord: z.ZodBoolean;
|
|
29
30
|
isLiteral: z.ZodBoolean;
|
|
31
|
+
multiline: z.ZodBoolean;
|
|
30
32
|
includeHidden: z.ZodBoolean;
|
|
31
33
|
baseNameMatch: z.ZodBoolean;
|
|
32
34
|
caseSensitiveFileMatch: z.ZodBoolean;
|