@j0hanz/filesystem-mcp 1.13.2 → 1.14.1
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 +162 -145
- package/dist/cli.js +2 -2
- package/dist/completions.js +54 -51
- package/dist/config.d.ts +13 -14
- package/dist/config.js +12 -12
- package/dist/index.js +1 -1
- package/dist/lib/abort.d.ts +7 -0
- package/dist/lib/abort.js +81 -0
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.js +8 -2
- package/dist/lib/errors.d.ts +7 -3
- package/dist/lib/errors.js +64 -41
- package/dist/lib/file-operations/core.d.ts +3 -3
- package/dist/lib/file-operations/core.js +23 -20
- package/dist/lib/file-operations/metadata.d.ts +2 -2
- package/dist/lib/file-operations/metadata.js +69 -22
- package/dist/lib/file-operations/search.d.ts +0 -1
- package/dist/lib/file-operations/search.js +87 -95
- package/dist/lib/file-operations/traversal.js +13 -15
- package/dist/lib/fs-helpers.d.ts +3 -10
- package/dist/lib/fs-helpers.js +29 -108
- package/dist/lib/globs.d.ts +2 -0
- package/dist/lib/globs.js +19 -0
- package/dist/lib/logger.d.ts +28 -0
- package/dist/lib/logger.js +91 -0
- package/dist/lib/observability.d.ts +7 -0
- package/dist/lib/observability.js +19 -9
- package/dist/lib/paths.js +55 -55
- package/dist/lib/resource-store.js +4 -4
- package/dist/lib/utils.d.ts +0 -12
- package/dist/lib/utils.js +0 -13
- 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 +41 -41
- package/dist/resources/tool-catalog.js +33 -58
- package/dist/resources/tool-info.d.ts +0 -1
- package/dist/resources/tool-info.js +44 -67
- package/dist/resources/workflows.js +47 -19
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +4 -4
- package/dist/schemas.d.ts +185 -465
- package/dist/schemas.js +174 -206
- package/dist/server/bootstrap.d.ts +12 -11
- package/dist/server/bootstrap.js +95 -86
- package/dist/server/roots-manager.d.ts +5 -2
- package/dist/server/roots-manager.js +9 -7
- package/dist/server/task-store.d.ts +10 -0
- package/dist/server/task-store.js +73 -0
- package/dist/tools/apply-patch.js +39 -20
- package/dist/tools/calculate-hash.js +14 -27
- package/dist/tools/create-directory.js +11 -9
- package/dist/tools/delete-file.js +19 -19
- package/dist/tools/diff-files.js +16 -18
- package/dist/tools/edit-file.js +11 -5
- package/dist/tools/list-directory.js +16 -21
- package/dist/tools/move-file.js +105 -100
- package/dist/tools/read-multiple.js +15 -10
- package/dist/tools/read.js +6 -7
- package/dist/tools/replace-in-files.js +76 -115
- package/dist/tools/roots.js +3 -7
- package/dist/tools/search-content.js +158 -203
- package/dist/tools/search-files.js +59 -50
- package/dist/tools/shared.d.ts +10 -0
- package/dist/tools/shared.js +105 -36
- package/dist/tools/stat-many.js +15 -9
- package/dist/tools/stat.js +6 -6
- package/dist/tools/task-support.d.ts +10 -9
- package/dist/tools/task-support.js +94 -23
- package/dist/tools/tree.js +4 -4
- package/dist/tools/write-file.js +11 -12
- package/package.json +10 -9
package/dist/completions.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
1
|
import { CompleteRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, parse, resolve, sep, } from 'node:path';
|
|
4
4
|
import { getAllowedDirectories, isPathWithinDirectories, normalizePath, toPosixPath, } from './lib/paths.js';
|
|
5
5
|
import { isRecord } from './lib/utils.js';
|
|
6
6
|
import { getSortedToolContracts } from './resources/tool-info.js';
|
|
@@ -191,21 +191,21 @@ function rememberCompletionCacheValue(cache, key, value) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
function hasTrailingSeparator(value) {
|
|
194
|
-
return
|
|
194
|
+
return value.endsWith(sep) || value.endsWith('/') || value.endsWith('\\');
|
|
195
195
|
}
|
|
196
196
|
function isAbsolutePathInput(value) {
|
|
197
|
-
return (
|
|
197
|
+
return (isAbsolute(value) ||
|
|
198
198
|
/^[A-Za-z]:[\\/]/u.test(value) ||
|
|
199
199
|
value.startsWith('\\\\'));
|
|
200
200
|
}
|
|
201
201
|
function resolveFromBase(base, rawValue, trailingSeparator) {
|
|
202
|
-
const normalizedValue = normalizePath(
|
|
202
|
+
const normalizedValue = normalizePath(resolve(base, rawValue));
|
|
203
203
|
if (trailingSeparator) {
|
|
204
204
|
return { searchDir: normalizedValue, prefix: '' };
|
|
205
205
|
}
|
|
206
206
|
return {
|
|
207
|
-
searchDir:
|
|
208
|
-
prefix:
|
|
207
|
+
searchDir: dirname(normalizedValue),
|
|
208
|
+
prefix: basename(normalizedValue),
|
|
209
209
|
};
|
|
210
210
|
}
|
|
211
211
|
function resolveNamedRootContext(currentValue, allowed) {
|
|
@@ -225,18 +225,18 @@ function resolveNamedRootPath(value, allowed) {
|
|
|
225
225
|
const root = findAllowedRootByName(parsed.rootName, allowed);
|
|
226
226
|
if (!root)
|
|
227
227
|
return undefined;
|
|
228
|
-
return normalizePath(
|
|
228
|
+
return normalizePath(resolve(root, parsed.remainder));
|
|
229
229
|
}
|
|
230
230
|
function parseNamedRootInput(value) {
|
|
231
231
|
const normalizedInput = toPosixPath(value);
|
|
232
232
|
const [rootName, ...rest] = normalizedInput.split('/');
|
|
233
233
|
if (!rootName)
|
|
234
234
|
return undefined;
|
|
235
|
-
return { rootName, remainder: rest.join(
|
|
235
|
+
return { rootName, remainder: rest.join(sep) };
|
|
236
236
|
}
|
|
237
237
|
function findAllowedRootByName(rootName, allowed) {
|
|
238
238
|
const normalizedRootName = rootName.toLowerCase();
|
|
239
|
-
return allowed.find((candidate) =>
|
|
239
|
+
return allowed.find((candidate) => basename(candidate).toLowerCase() === normalizedRootName);
|
|
240
240
|
}
|
|
241
241
|
function chooseContextKeys(argumentName) {
|
|
242
242
|
const normalized = argumentName.toLowerCase();
|
|
@@ -263,7 +263,7 @@ function resolveContextCandidatePath(candidate, allowed) {
|
|
|
263
263
|
const base = allowed[0];
|
|
264
264
|
if (!base)
|
|
265
265
|
return undefined;
|
|
266
|
-
return normalizePath(
|
|
266
|
+
return normalizePath(resolve(base, candidate));
|
|
267
267
|
}
|
|
268
268
|
return resolveNamedRootPath(candidate, allowed);
|
|
269
269
|
}
|
|
@@ -271,14 +271,14 @@ async function toAllowedContextDirectory(resolved, allowed) {
|
|
|
271
271
|
if (!isPathWithinDirectories(resolved, allowed))
|
|
272
272
|
return undefined;
|
|
273
273
|
try {
|
|
274
|
-
const stats = await
|
|
274
|
+
const stats = await stat(resolved);
|
|
275
275
|
if (stats.isDirectory())
|
|
276
276
|
return resolved;
|
|
277
277
|
}
|
|
278
278
|
catch {
|
|
279
279
|
// Fall back to parent path best-effort resolution.
|
|
280
280
|
}
|
|
281
|
-
const parent =
|
|
281
|
+
const parent = dirname(resolved);
|
|
282
282
|
return isPathWithinDirectories(parent, allowed) ? parent : undefined;
|
|
283
283
|
}
|
|
284
284
|
async function resolveContextBaseDirectory(argumentName, contextArguments, allowed) {
|
|
@@ -300,7 +300,7 @@ async function resolveContextBaseDirectory(argumentName, contextArguments, allow
|
|
|
300
300
|
return undefined;
|
|
301
301
|
}
|
|
302
302
|
function withDirectorySeparator(value) {
|
|
303
|
-
return value.endsWith(
|
|
303
|
+
return value.endsWith(sep) ? value : `${value}${sep}`;
|
|
304
304
|
}
|
|
305
305
|
function buildCompletionResult(values) {
|
|
306
306
|
return {
|
|
@@ -314,8 +314,8 @@ function buildCompletionResponse(result) {
|
|
|
314
314
|
}
|
|
315
315
|
function sortCompletionMatches(matches) {
|
|
316
316
|
matches.sort((left, right) => {
|
|
317
|
-
const leftIsDir = left.endsWith(
|
|
318
|
-
const rightIsDir = right.endsWith(
|
|
317
|
+
const leftIsDir = left.endsWith(sep);
|
|
318
|
+
const rightIsDir = right.endsWith(sep);
|
|
319
319
|
if (leftIsDir && !rightIsDir)
|
|
320
320
|
return -1;
|
|
321
321
|
if (!leftIsDir && rightIsDir)
|
|
@@ -350,7 +350,7 @@ function collectAllowedRoots(allowed, predicate) {
|
|
|
350
350
|
function getSearchContext(currentValue, allowed, contextBase) {
|
|
351
351
|
const trailingSeparator = hasTrailingSeparator(currentValue);
|
|
352
352
|
if (isAbsolutePathInput(currentValue)) {
|
|
353
|
-
return resolveFromBase(
|
|
353
|
+
return resolveFromBase(parse(currentValue).root || sep, currentValue, trailingSeparator);
|
|
354
354
|
}
|
|
355
355
|
const namedRootContext = resolveNamedRootContext(currentValue, allowed);
|
|
356
356
|
if (namedRootContext) {
|
|
@@ -376,13 +376,13 @@ async function findMatchesInDirectory(searchDir, prefix, allowed) {
|
|
|
376
376
|
return matches;
|
|
377
377
|
}
|
|
378
378
|
try {
|
|
379
|
-
const entries = await
|
|
379
|
+
const entries = await readdir(searchDir, { withFileTypes: true });
|
|
380
380
|
const lowerPrefix = prefix.toLowerCase();
|
|
381
381
|
for (const entry of entries) {
|
|
382
382
|
if (entry.name.toLowerCase().startsWith(lowerPrefix)) {
|
|
383
|
-
const fullPath =
|
|
383
|
+
const fullPath = join(searchDir, entry.name);
|
|
384
384
|
const isDir = entry.isDirectory();
|
|
385
|
-
matches.push(isDir ? `${fullPath}${
|
|
385
|
+
matches.push(isDir ? `${fullPath}${sep}` : fullPath);
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
388
|
}
|
|
@@ -396,17 +396,17 @@ function findRootPrefixMatches(currentValue, allowed) {
|
|
|
396
396
|
if (!rootPrefix) {
|
|
397
397
|
return collectAllowedRoots(allowed, () => true);
|
|
398
398
|
}
|
|
399
|
-
return collectAllowedRoots(allowed, (root) =>
|
|
399
|
+
return collectAllowedRoots(allowed, (root) => basename(root).toLowerCase().startsWith(rootPrefix));
|
|
400
400
|
}
|
|
401
401
|
function findMatchingRoots(searchDir, prefix, allowed) {
|
|
402
402
|
const lowerPrefix = prefix.toLowerCase();
|
|
403
403
|
const normalizedSearchDir = normalizePath(searchDir);
|
|
404
404
|
return collectAllowedRoots(allowed, (root) => {
|
|
405
|
-
const rootDir =
|
|
405
|
+
const rootDir = dirname(root);
|
|
406
406
|
// Check if root is a direct child of searchDir
|
|
407
407
|
if (normalizePath(rootDir) !== normalizedSearchDir)
|
|
408
408
|
return false;
|
|
409
|
-
return
|
|
409
|
+
return basename(root).toLowerCase().startsWith(lowerPrefix);
|
|
410
410
|
});
|
|
411
411
|
}
|
|
412
412
|
async function getPathCompletions(currentValue, options = {}) {
|
|
@@ -430,6 +430,34 @@ async function getPathCompletions(currentValue, options = {}) {
|
|
|
430
430
|
return { values: [] };
|
|
431
431
|
}
|
|
432
432
|
}
|
|
433
|
+
function handleTopicAndToolCompletions(ref, argName, argumentValue, topicValues, toolNameValues) {
|
|
434
|
+
if (!isRecord(ref))
|
|
435
|
+
return undefined;
|
|
436
|
+
const currentValue = argumentValue.toLowerCase();
|
|
437
|
+
if (ref['type'] === 'ref/prompt' && argName === 'topic') {
|
|
438
|
+
const filtered = currentValue
|
|
439
|
+
? topicValues.filter((v) => v.startsWith(currentValue))
|
|
440
|
+
: topicValues;
|
|
441
|
+
return buildCompletionResponse(buildCompletionResult(filtered));
|
|
442
|
+
}
|
|
443
|
+
if (ref['type'] === 'ref/prompt' &&
|
|
444
|
+
ref['name'] === 'get-tool-help' &&
|
|
445
|
+
argName === 'name') {
|
|
446
|
+
const filtered = currentValue
|
|
447
|
+
? toolNameValues.filter((value) => value.startsWith(currentValue))
|
|
448
|
+
: toolNameValues;
|
|
449
|
+
return buildCompletionResponse(buildCompletionResult(filtered));
|
|
450
|
+
}
|
|
451
|
+
if (ref['type'] === 'ref/resource' &&
|
|
452
|
+
ref['uri'] === 'internal://tool-info/{name}' &&
|
|
453
|
+
argName === 'name') {
|
|
454
|
+
const filtered = currentValue
|
|
455
|
+
? toolNameValues.filter((value) => value.startsWith(currentValue))
|
|
456
|
+
: toolNameValues;
|
|
457
|
+
return buildCompletionResponse(buildCompletionResult(filtered));
|
|
458
|
+
}
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
433
461
|
export function registerCompletions(server, instructions = '') {
|
|
434
462
|
const topicValues = extractTopicCompletions(instructions);
|
|
435
463
|
const toolNameValues = extractToolNameCompletions();
|
|
@@ -437,34 +465,9 @@ export function registerCompletions(server, instructions = '') {
|
|
|
437
465
|
const { params } = request;
|
|
438
466
|
const { argument, ref } = params;
|
|
439
467
|
const argName = argument.name.toLowerCase();
|
|
440
|
-
|
|
441
|
-
if (
|
|
442
|
-
|
|
443
|
-
const filtered = currentValue
|
|
444
|
-
? topicValues.filter((v) => v.startsWith(currentValue))
|
|
445
|
-
: topicValues;
|
|
446
|
-
return buildCompletionResponse(buildCompletionResult(filtered));
|
|
447
|
-
}
|
|
448
|
-
if (isRecord(ref) &&
|
|
449
|
-
ref['type'] === 'ref/prompt' &&
|
|
450
|
-
ref['name'] === 'get-tool-help' &&
|
|
451
|
-
argName === 'name') {
|
|
452
|
-
const currentValue = argument.value.toLowerCase();
|
|
453
|
-
const filtered = currentValue
|
|
454
|
-
? toolNameValues.filter((value) => value.startsWith(currentValue))
|
|
455
|
-
: toolNameValues;
|
|
456
|
-
return buildCompletionResponse(buildCompletionResult(filtered));
|
|
457
|
-
}
|
|
458
|
-
if (isRecord(ref) &&
|
|
459
|
-
ref['type'] === 'ref/resource' &&
|
|
460
|
-
ref['uri'] === 'internal://tool-info/{name}' &&
|
|
461
|
-
argName === 'name') {
|
|
462
|
-
const currentValue = argument.value.toLowerCase();
|
|
463
|
-
const filtered = currentValue
|
|
464
|
-
? toolNameValues.filter((value) => value.startsWith(currentValue))
|
|
465
|
-
: toolNameValues;
|
|
466
|
-
return buildCompletionResponse(buildCompletionResult(filtered));
|
|
467
|
-
}
|
|
468
|
+
const predef = handleTopicAndToolCompletions(ref, argName, argument.value, topicValues, toolNameValues);
|
|
469
|
+
if (predef)
|
|
470
|
+
return predef;
|
|
468
471
|
const enumResult = getEnumCompletions(argName, argument.value);
|
|
469
472
|
if (enumResult) {
|
|
470
473
|
return buildCompletionResponse(enumResult);
|
package/dist/config.d.ts
CHANGED
|
@@ -77,14 +77,13 @@ export interface SearchContentResult {
|
|
|
77
77
|
readonly skippedTooLarge: number;
|
|
78
78
|
readonly skippedBinary: number;
|
|
79
79
|
readonly skippedInaccessible: number;
|
|
80
|
-
readonly linesSkippedDueToRegexTimeout: number;
|
|
81
80
|
readonly stoppedReason?: 'maxResults' | 'maxFiles' | 'timeout';
|
|
82
81
|
};
|
|
83
82
|
}
|
|
84
83
|
export interface MultipleFileInfoResult {
|
|
85
84
|
readonly path: string;
|
|
86
85
|
readonly info?: FileInfo;
|
|
87
|
-
readonly error?:
|
|
86
|
+
readonly error?: Error;
|
|
88
87
|
}
|
|
89
88
|
export interface GetMultipleFileInfoResult {
|
|
90
89
|
readonly results: readonly MultipleFileInfoResult[];
|
|
@@ -96,18 +95,18 @@ export interface GetMultipleFileInfoResult {
|
|
|
96
95
|
};
|
|
97
96
|
}
|
|
98
97
|
export declare const ErrorCode: {
|
|
99
|
-
readonly
|
|
100
|
-
readonly
|
|
101
|
-
readonly
|
|
102
|
-
readonly
|
|
103
|
-
readonly
|
|
104
|
-
readonly
|
|
105
|
-
readonly
|
|
106
|
-
readonly
|
|
107
|
-
readonly
|
|
108
|
-
readonly
|
|
109
|
-
readonly
|
|
110
|
-
readonly
|
|
98
|
+
readonly ACCESS_DENIED: "ACCESS_DENIED";
|
|
99
|
+
readonly NOT_FOUND: "NOT_FOUND";
|
|
100
|
+
readonly NOT_FILE: "NOT_FILE";
|
|
101
|
+
readonly NOT_DIRECTORY: "NOT_DIRECTORY";
|
|
102
|
+
readonly TOO_LARGE: "TOO_LARGE";
|
|
103
|
+
readonly TIMEOUT: "TIMEOUT";
|
|
104
|
+
readonly CANCELLED: "CANCELLED";
|
|
105
|
+
readonly INVALID_PATTERN: "INVALID_PATTERN";
|
|
106
|
+
readonly INVALID_INPUT: "INVALID_INPUT";
|
|
107
|
+
readonly PERMISSION_DENIED: "PERMISSION_DENIED";
|
|
108
|
+
readonly SYMLINK_NOT_ALLOWED: "SYMLINK_NOT_ALLOWED";
|
|
109
|
+
readonly UNKNOWN: "UNKNOWN";
|
|
111
110
|
};
|
|
112
111
|
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
113
112
|
export declare function formatBytes(bytes: number): string;
|
package/dist/config.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
export const ErrorCode = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
ACCESS_DENIED: 'ACCESS_DENIED',
|
|
3
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
4
|
+
NOT_FILE: 'NOT_FILE',
|
|
5
|
+
NOT_DIRECTORY: 'NOT_DIRECTORY',
|
|
6
|
+
TOO_LARGE: 'TOO_LARGE',
|
|
7
|
+
TIMEOUT: 'TIMEOUT',
|
|
8
|
+
CANCELLED: 'CANCELLED',
|
|
9
|
+
INVALID_PATTERN: 'INVALID_PATTERN',
|
|
10
|
+
INVALID_INPUT: 'INVALID_INPUT',
|
|
11
|
+
PERMISSION_DENIED: 'PERMISSION_DENIED',
|
|
12
|
+
SYMLINK_NOT_ALLOWED: 'SYMLINK_NOT_ALLOWED',
|
|
13
|
+
UNKNOWN: 'UNKNOWN',
|
|
14
14
|
};
|
|
15
15
|
const BYTE_UNIT_LABELS = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
16
16
|
export function formatBytes(bytes) {
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from 'node:process';
|
|
3
|
+
import { createTimedAbortSignal } from './lib/abort.js';
|
|
3
4
|
import { DEFAULT_SEARCH_TIMEOUT_MS } from './lib/constants.js';
|
|
4
5
|
import { formatUnknownErrorMessage } from './lib/errors.js';
|
|
5
|
-
import { createTimedAbortSignal } from './lib/fs-helpers.js';
|
|
6
6
|
import { setAllowedDirectoriesResolved } from './lib/paths.js';
|
|
7
7
|
import { CliExitError, parseArgs } from './cli.js';
|
|
8
8
|
import { createServer, startHttpServer, startServer } from './server.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function assertNotAborted(signal?: AbortSignal, message?: string): void;
|
|
2
|
+
export declare function withAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
3
|
+
export declare function createTimedAbortSignal(baseSignal: AbortSignal | undefined, timeoutMs?: number): {
|
|
4
|
+
signal: AbortSignal;
|
|
5
|
+
cleanup: () => void;
|
|
6
|
+
};
|
|
7
|
+
export declare function withTimedAbortSignal<T>(baseSignal: AbortSignal | undefined, timeoutMs: number | undefined, run: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { normalizeUnknownError } from './errors.js';
|
|
2
|
+
function createAbortError(message = 'Operation aborted') {
|
|
3
|
+
return new DOMException(message, 'AbortError');
|
|
4
|
+
}
|
|
5
|
+
const SHARED_NOOP_SIGNAL = new AbortController().signal;
|
|
6
|
+
function normalizeAbortReason(reason, message) {
|
|
7
|
+
if (reason instanceof Error)
|
|
8
|
+
return reason;
|
|
9
|
+
return createAbortError(message);
|
|
10
|
+
}
|
|
11
|
+
function isFiniteNumber(value) {
|
|
12
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
13
|
+
}
|
|
14
|
+
export function assertNotAborted(signal, message) {
|
|
15
|
+
if (!signal)
|
|
16
|
+
return;
|
|
17
|
+
try {
|
|
18
|
+
signal.throwIfAborted();
|
|
19
|
+
}
|
|
20
|
+
catch (reason) {
|
|
21
|
+
throw normalizeAbortReason(reason, message);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function getAbortError(signal, message) {
|
|
25
|
+
try {
|
|
26
|
+
signal.throwIfAborted();
|
|
27
|
+
}
|
|
28
|
+
catch (reason) {
|
|
29
|
+
return normalizeAbortReason(reason, message);
|
|
30
|
+
}
|
|
31
|
+
return createAbortError(message);
|
|
32
|
+
}
|
|
33
|
+
export function withAbort(promise, signal) {
|
|
34
|
+
if (!signal)
|
|
35
|
+
return promise;
|
|
36
|
+
signal.throwIfAborted();
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const onAbort = () => {
|
|
39
|
+
reject(getAbortError(signal));
|
|
40
|
+
};
|
|
41
|
+
if (signal.aborted) {
|
|
42
|
+
onAbort();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
46
|
+
promise.then((value) => {
|
|
47
|
+
signal.removeEventListener('abort', onAbort);
|
|
48
|
+
resolve(value);
|
|
49
|
+
}, (error) => {
|
|
50
|
+
signal.removeEventListener('abort', onAbort);
|
|
51
|
+
reject(normalizeUnknownError(error));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export function createTimedAbortSignal(baseSignal, timeoutMs) {
|
|
56
|
+
const timeoutSignal = isFiniteNumber(timeoutMs)
|
|
57
|
+
? AbortSignal.timeout(timeoutMs)
|
|
58
|
+
: undefined;
|
|
59
|
+
if (baseSignal && timeoutSignal) {
|
|
60
|
+
return {
|
|
61
|
+
signal: AbortSignal.any([baseSignal, timeoutSignal]),
|
|
62
|
+
cleanup: () => { },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (baseSignal) {
|
|
66
|
+
return { signal: baseSignal, cleanup: () => { } };
|
|
67
|
+
}
|
|
68
|
+
if (timeoutSignal) {
|
|
69
|
+
return { signal: timeoutSignal, cleanup: () => { } };
|
|
70
|
+
}
|
|
71
|
+
return { signal: SHARED_NOOP_SIGNAL, cleanup: () => { } };
|
|
72
|
+
}
|
|
73
|
+
export async function withTimedAbortSignal(baseSignal, timeoutMs, run) {
|
|
74
|
+
const { signal, cleanup } = createTimedAbortSignal(baseSignal, timeoutMs);
|
|
75
|
+
try {
|
|
76
|
+
return await run(signal);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
cleanup();
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export declare function parseTrueEnvFlag(value: string | undefined): boolean;
|
|
2
2
|
export declare function parseEnvInt(envVar: string, defaultValue: number, min: number, max: number): number;
|
|
3
|
-
export declare const DEFAULT_LOG_LEVEL: "
|
|
3
|
+
export declare const DEFAULT_LOG_LEVEL: "error" | "debug" | "info" | "notice" | "warning" | "critical" | "alert" | "emergency";
|
|
4
4
|
export declare const DEFAULT_TASK_TTL_MS: number;
|
|
5
5
|
export declare const MAX_TASK_TTL_MS: number;
|
|
6
6
|
export declare const MAX_CONCURRENT_TASKS: number;
|
|
7
|
+
export declare const TASK_CANCEL_POLL_MS = 2000;
|
|
8
|
+
export declare const TASK_POLL_INTERVAL_MS = 100;
|
|
7
9
|
export declare const PARALLEL_CONCURRENCY: number;
|
|
8
10
|
export declare const MAX_SEARCHABLE_FILE_SIZE: number;
|
|
9
11
|
export declare const MAX_TEXT_FILE_SIZE: number;
|
package/dist/lib/constants.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { availableParallelism } from 'node:os';
|
|
2
|
+
import { Logger } from './logger.js';
|
|
2
3
|
const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes', 'y', 'on']);
|
|
3
4
|
const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'n', 'off']);
|
|
4
5
|
export function parseTrueEnvFlag(value) {
|
|
@@ -9,7 +10,7 @@ export function parseTrueEnvFlag(value) {
|
|
|
9
10
|
const KIB = 1024;
|
|
10
11
|
const MIB = 1024 * KIB;
|
|
11
12
|
function logInvalidEnvValue(envVar, value, expected, defaultValue) {
|
|
12
|
-
|
|
13
|
+
Logger.warn(`Invalid ${envVar} value: ${value} (must be ${expected}). Using default: ${String(defaultValue)}`);
|
|
13
14
|
}
|
|
14
15
|
// Helper for parsing environment variables (only used for configurable values)
|
|
15
16
|
export function parseEnvInt(envVar, defaultValue, min, max) {
|
|
@@ -66,7 +67,7 @@ function parseEnvLogLevel(envVar, defaultValue) {
|
|
|
66
67
|
if (VALID_LOG_LEVELS.includes(normalized)) {
|
|
67
68
|
return normalized;
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
+
Logger.warn(`Invalid ${envVar} value: ${value} (must be ${VALID_LOG_LEVELS.join('|')}). Using default: ${defaultValue}`);
|
|
70
71
|
return defaultValue;
|
|
71
72
|
}
|
|
72
73
|
export const DEFAULT_LOG_LEVEL = parseEnvLogLevel('FILESYSTEM_MCP_LOG_LEVEL', 'info');
|
|
@@ -74,6 +75,11 @@ export const DEFAULT_LOG_LEVEL = parseEnvLogLevel('FILESYSTEM_MCP_LOG_LEVEL', 'i
|
|
|
74
75
|
export const DEFAULT_TASK_TTL_MS = 5 * 60 * 1000;
|
|
75
76
|
export const MAX_TASK_TTL_MS = parseEnvInt('FILESYSTEM_MCP_MAX_TASK_TTL_MS', 60 * 60 * 1000, 1_000, 24 * 60 * 60 * 1000);
|
|
76
77
|
export const MAX_CONCURRENT_TASKS = parseEnvInt('FILESYSTEM_MCP_MAX_CONCURRENT_TASKS', 100, 1, 10_000);
|
|
78
|
+
// How often (ms) a background task checks the store for client cancellation.
|
|
79
|
+
export const TASK_CANCEL_POLL_MS = 2_000;
|
|
80
|
+
// Suggested poll interval (ms) returned to the SDK/client for automatic task polling.
|
|
81
|
+
// Filesystem operations complete quickly; a short interval avoids unnecessary latency.
|
|
82
|
+
export const TASK_POLL_INTERVAL_MS = 100;
|
|
77
83
|
// Auto-tuned parallelism based on CPU cores (no env override)
|
|
78
84
|
const BYTES_PER_PARALLEL_TASK = 64 * MIB;
|
|
79
85
|
const BYTES_PER_SEARCH_WORKER = 128 * MIB;
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -15,9 +15,13 @@ export declare function isTimeoutLikeError(error: unknown): boolean;
|
|
|
15
15
|
export declare class McpError extends Error {
|
|
16
16
|
code: ErrorCode;
|
|
17
17
|
path?: string | undefined;
|
|
18
|
-
details?: Record<string, unknown
|
|
19
|
-
constructor(code: ErrorCode, message: string, path?: string | undefined, details?: Record<string, unknown
|
|
18
|
+
details?: Record<string, unknown>;
|
|
19
|
+
constructor(code: ErrorCode, message: string, path?: string | undefined, details?: Record<string, unknown>, cause?: unknown);
|
|
20
|
+
static notFound(message: string, path?: string, details?: Record<string, unknown>, cause?: unknown): McpError;
|
|
21
|
+
static invalidInput(message: string, path?: string, details?: Record<string, unknown>, cause?: unknown): McpError;
|
|
22
|
+
static accessDenied(message: string, path?: string, details?: Record<string, unknown>, cause?: unknown): McpError;
|
|
23
|
+
static timeout(message: string, path?: string, details?: Record<string, unknown>, cause?: unknown): McpError;
|
|
20
24
|
}
|
|
21
25
|
export declare function createDetailedError(error: unknown, path?: string, additionalDetails?: Record<string, unknown>): DetailedError;
|
|
22
26
|
export declare function formatDetailedError(error: DetailedError): string;
|
|
23
|
-
export declare function getSuggestion(code: ErrorCode): string;
|
|
27
|
+
export declare function getSuggestion(code: ErrorCode): string | undefined;
|
package/dist/lib/errors.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { constants as osConstants } from 'node:os';
|
|
2
2
|
import { getSystemErrorMap, getSystemErrorName, inspect } from 'node:util';
|
|
3
3
|
import { ErrorCode, joinLines } from '../config.js';
|
|
4
|
+
import { getTraceContext } from './observability.js';
|
|
4
5
|
export { ErrorCode };
|
|
5
6
|
function isNativeError(error) {
|
|
6
7
|
const candidate = Error;
|
|
@@ -100,20 +101,20 @@ export function normalizeUnknownError(error) {
|
|
|
100
101
|
: new Error(formatUnknownErrorMessage(error));
|
|
101
102
|
}
|
|
102
103
|
const NODE_ERROR_CODE_MAP = {
|
|
103
|
-
ENOENT: ErrorCode.
|
|
104
|
-
EACCES: ErrorCode.
|
|
105
|
-
EPERM: ErrorCode.
|
|
106
|
-
ENOTDIR: ErrorCode.
|
|
107
|
-
EISDIR: ErrorCode.
|
|
108
|
-
ELOOP: ErrorCode.
|
|
109
|
-
ENAMETOOLONG: ErrorCode.
|
|
110
|
-
ETIMEDOUT: ErrorCode.
|
|
111
|
-
EMFILE: ErrorCode.
|
|
112
|
-
ENFILE: ErrorCode.
|
|
113
|
-
EBUSY: ErrorCode.
|
|
114
|
-
ENOTEMPTY: ErrorCode.
|
|
115
|
-
EEXIST: ErrorCode.
|
|
116
|
-
EINVAL: ErrorCode.
|
|
104
|
+
ENOENT: ErrorCode.NOT_FOUND,
|
|
105
|
+
EACCES: ErrorCode.PERMISSION_DENIED,
|
|
106
|
+
EPERM: ErrorCode.PERMISSION_DENIED,
|
|
107
|
+
ENOTDIR: ErrorCode.NOT_DIRECTORY,
|
|
108
|
+
EISDIR: ErrorCode.NOT_FILE,
|
|
109
|
+
ELOOP: ErrorCode.SYMLINK_NOT_ALLOWED,
|
|
110
|
+
ENAMETOOLONG: ErrorCode.INVALID_INPUT,
|
|
111
|
+
ETIMEDOUT: ErrorCode.TIMEOUT,
|
|
112
|
+
EMFILE: ErrorCode.TIMEOUT,
|
|
113
|
+
ENFILE: ErrorCode.TIMEOUT,
|
|
114
|
+
EBUSY: ErrorCode.PERMISSION_DENIED,
|
|
115
|
+
ENOTEMPTY: ErrorCode.NOT_DIRECTORY,
|
|
116
|
+
EEXIST: ErrorCode.INVALID_INPUT,
|
|
117
|
+
EINVAL: ErrorCode.INVALID_INPUT,
|
|
117
118
|
};
|
|
118
119
|
function isKnownNodeErrorCode(code) {
|
|
119
120
|
return code in NODE_ERROR_CODE_MAP;
|
|
@@ -169,26 +170,44 @@ export class McpError extends Error {
|
|
|
169
170
|
super(message, { cause });
|
|
170
171
|
this.code = code;
|
|
171
172
|
this.path = path;
|
|
172
|
-
this.details = details;
|
|
173
173
|
this.name = 'McpError';
|
|
174
174
|
Object.setPrototypeOf(this, McpError.prototype);
|
|
175
|
+
const trace = getTraceContext();
|
|
176
|
+
if (trace?.traceparent || details) {
|
|
177
|
+
this.details = { ...trace, ...details };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
static notFound(message, path, details, cause) {
|
|
181
|
+
return new McpError(ErrorCode.NOT_FOUND, message, path, details, cause);
|
|
182
|
+
}
|
|
183
|
+
static invalidInput(message, path, details, cause) {
|
|
184
|
+
return new McpError(ErrorCode.INVALID_INPUT, message, path, details, cause);
|
|
185
|
+
}
|
|
186
|
+
static accessDenied(message, path, details, cause) {
|
|
187
|
+
return new McpError(ErrorCode.ACCESS_DENIED, message, path, details, cause);
|
|
188
|
+
}
|
|
189
|
+
static timeout(message, path, details, cause) {
|
|
190
|
+
return new McpError(ErrorCode.TIMEOUT, message, path, details, cause);
|
|
175
191
|
}
|
|
176
192
|
}
|
|
177
193
|
const ERROR_SUGGESTIONS = {
|
|
178
|
-
[ErrorCode.
|
|
179
|
-
[ErrorCode.
|
|
180
|
-
[ErrorCode.
|
|
181
|
-
[ErrorCode.
|
|
182
|
-
[ErrorCode.
|
|
183
|
-
[ErrorCode.
|
|
184
|
-
[ErrorCode.
|
|
185
|
-
[ErrorCode.
|
|
186
|
-
[ErrorCode.
|
|
187
|
-
[ErrorCode.
|
|
188
|
-
[ErrorCode.
|
|
189
|
-
[ErrorCode.
|
|
194
|
+
[ErrorCode.ACCESS_DENIED]: 'Run roots to list allowed directories.',
|
|
195
|
+
[ErrorCode.NOT_FOUND]: 'Run ls or find to verify the path.',
|
|
196
|
+
[ErrorCode.NOT_FILE]: 'Target is a directory, not a file.',
|
|
197
|
+
[ErrorCode.NOT_DIRECTORY]: 'Target is a file, not a directory.',
|
|
198
|
+
[ErrorCode.TOO_LARGE]: 'Use head/tail or line ranges to read partially.',
|
|
199
|
+
[ErrorCode.TIMEOUT]: 'Reduce scope, depth, or maxResults.',
|
|
200
|
+
[ErrorCode.CANCELLED]: undefined,
|
|
201
|
+
[ErrorCode.INVALID_PATTERN]: 'Check syntax and escape special characters.',
|
|
202
|
+
[ErrorCode.INVALID_INPUT]: undefined,
|
|
203
|
+
[ErrorCode.PERMISSION_DENIED]: 'Check OS file permissions.',
|
|
204
|
+
[ErrorCode.SYMLINK_NOT_ALLOWED]: 'Symlink escapes allowed directories.',
|
|
205
|
+
[ErrorCode.UNKNOWN]: undefined,
|
|
190
206
|
};
|
|
191
|
-
const NOT_FOUND_PATTERNS = [
|
|
207
|
+
const NOT_FOUND_PATTERNS = [
|
|
208
|
+
'no such file or directory',
|
|
209
|
+
'does not exist',
|
|
210
|
+
];
|
|
192
211
|
const PERMISSION_DENIED_PATTERNS = [
|
|
193
212
|
'permission denied',
|
|
194
213
|
'not permitted',
|
|
@@ -204,16 +223,16 @@ function classifyMessageError(error) {
|
|
|
204
223
|
const message = isNativeError(error) ? error.message : String(error);
|
|
205
224
|
const lower = message.toLowerCase();
|
|
206
225
|
if (messageIncludesAny(lower, NOT_FOUND_PATTERNS)) {
|
|
207
|
-
return ErrorCode.
|
|
226
|
+
return ErrorCode.NOT_FOUND;
|
|
208
227
|
}
|
|
209
228
|
if (messageIncludesAny(lower, PERMISSION_DENIED_PATTERNS)) {
|
|
210
|
-
return ErrorCode.
|
|
229
|
+
return ErrorCode.PERMISSION_DENIED;
|
|
211
230
|
}
|
|
212
231
|
if (lower.includes('not a directory')) {
|
|
213
|
-
return ErrorCode.
|
|
232
|
+
return ErrorCode.NOT_DIRECTORY;
|
|
214
233
|
}
|
|
215
234
|
if (lower.includes('is a directory')) {
|
|
216
|
-
return ErrorCode.
|
|
235
|
+
return ErrorCode.NOT_FILE;
|
|
217
236
|
}
|
|
218
237
|
return undefined;
|
|
219
238
|
}
|
|
@@ -222,24 +241,28 @@ function classifyError(error) {
|
|
|
222
241
|
let fallbackCode;
|
|
223
242
|
const terminalCode = walkErrorChain(error, (candidate) => {
|
|
224
243
|
if (isAbortErrorSingle(candidate)) {
|
|
225
|
-
return ErrorCode.
|
|
244
|
+
return ErrorCode.CANCELLED;
|
|
226
245
|
}
|
|
227
246
|
if (timeoutCode === undefined && isTimeoutErrorSingle(candidate)) {
|
|
228
|
-
timeoutCode = ErrorCode.
|
|
247
|
+
timeoutCode = ErrorCode.TIMEOUT;
|
|
229
248
|
}
|
|
230
249
|
fallbackCode ??=
|
|
231
250
|
getDirectErrorCode(candidate) ?? classifyMessageError(candidate);
|
|
232
251
|
return undefined;
|
|
233
252
|
});
|
|
234
|
-
return terminalCode ?? timeoutCode ?? fallbackCode ?? ErrorCode.
|
|
253
|
+
return terminalCode ?? timeoutCode ?? fallbackCode ?? ErrorCode.UNKNOWN;
|
|
235
254
|
}
|
|
236
255
|
export function createDetailedError(error, path, additionalDetails) {
|
|
237
|
-
const message =
|
|
256
|
+
const message = formatUnknownErrorMessage(error);
|
|
238
257
|
const code = classifyError(error);
|
|
239
258
|
const suggestion = ERROR_SUGGESTIONS[code];
|
|
240
259
|
const resolvedPath = resolveErrorPath(error, path);
|
|
241
260
|
const details = mergeErrorDetails(error, additionalDetails);
|
|
242
|
-
const result = {
|
|
261
|
+
const result = {
|
|
262
|
+
code,
|
|
263
|
+
message,
|
|
264
|
+
...(suggestion ? { suggestion } : {}),
|
|
265
|
+
};
|
|
243
266
|
if (resolvedPath)
|
|
244
267
|
result.path = resolvedPath;
|
|
245
268
|
if (details)
|
|
@@ -264,12 +287,12 @@ function mergeErrorDetails(error, additionalDetails) {
|
|
|
264
287
|
return mergedDetails;
|
|
265
288
|
}
|
|
266
289
|
export function formatDetailedError(error) {
|
|
267
|
-
const lines = [
|
|
268
|
-
if (error.path) {
|
|
269
|
-
lines.push(
|
|
290
|
+
const lines = [`${error.code}: ${error.message}`];
|
|
291
|
+
if (error.path && !error.message.includes(error.path)) {
|
|
292
|
+
lines.push(error.path);
|
|
270
293
|
}
|
|
271
294
|
if (error.suggestion) {
|
|
272
|
-
lines.push(
|
|
295
|
+
lines.push(error.suggestion);
|
|
273
296
|
}
|
|
274
297
|
return joinLines(lines);
|
|
275
298
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type Ignore } from 'ignore';
|
|
2
2
|
export declare function needsStatsForSort(sortBy: string): boolean;
|
|
3
|
-
export declare function withOptionalStoppedReason<T extends object, R extends string>(summary: T, stoppedReason: R | undefined): T
|
|
4
|
-
stoppedReason
|
|
5
|
-
}
|
|
3
|
+
export declare function withOptionalStoppedReason<T extends object, R extends string>(summary: T, stoppedReason: R | undefined): T & {
|
|
4
|
+
stoppedReason?: R;
|
|
5
|
+
};
|
|
6
6
|
export interface DirentLike {
|
|
7
7
|
isDirectory(): boolean;
|
|
8
8
|
isFile(): boolean;
|