@j0hanz/fs-context-mcp 2.2.0 → 2.3.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 +171 -417
- package/dist/assets/logo.svg +3766 -0
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/lib/constants.d.ts +0 -1
- package/dist/lib/constants.js +0 -1
- package/dist/lib/errors.d.ts +0 -1
- package/dist/lib/errors.js +0 -1
- package/dist/lib/file-operations/file-info.d.ts +0 -1
- package/dist/lib/file-operations/file-info.js +0 -1
- package/dist/lib/file-operations/gitignore.d.ts +0 -1
- package/dist/lib/file-operations/gitignore.js +0 -1
- package/dist/lib/file-operations/glob-engine.d.ts +0 -1
- package/dist/lib/file-operations/glob-engine.js +101 -84
- package/dist/lib/file-operations/list-directory.d.ts +0 -1
- package/dist/lib/file-operations/list-directory.js +91 -62
- package/dist/lib/file-operations/read-multiple-files.d.ts +0 -1
- package/dist/lib/file-operations/read-multiple-files.js +0 -1
- package/dist/lib/file-operations/search-content.d.ts +0 -1
- package/dist/lib/file-operations/search-content.js +68 -21
- package/dist/lib/file-operations/search-files.d.ts +0 -1
- package/dist/lib/file-operations/search-files.js +21 -11
- package/dist/lib/file-operations/search-worker.d.ts +0 -1
- package/dist/lib/file-operations/search-worker.js +0 -1
- package/dist/lib/file-operations/tree.d.ts +0 -1
- package/dist/lib/file-operations/tree.js +18 -8
- package/dist/lib/fs-helpers.d.ts +0 -1
- package/dist/lib/fs-helpers.js +19 -25
- package/dist/lib/observability.d.ts +0 -1
- package/dist/lib/observability.js +49 -43
- package/dist/lib/path-policy.d.ts +0 -1
- package/dist/lib/path-policy.js +0 -1
- package/dist/lib/path-validation.d.ts +0 -1
- package/dist/lib/path-validation.js +54 -38
- package/dist/lib/resource-store.d.ts +0 -1
- package/dist/lib/resource-store.js +0 -1
- package/dist/resources.d.ts +2 -3
- package/dist/resources.js +16 -3
- package/dist/schemas.d.ts +0 -1
- package/dist/schemas.js +35 -51
- package/dist/server.d.ts +0 -1
- package/dist/server.js +42 -11
- package/dist/tools/list-directory.d.ts +0 -1
- package/dist/tools/list-directory.js +14 -2
- package/dist/tools/read-multiple.d.ts +0 -1
- package/dist/tools/read-multiple.js +14 -2
- package/dist/tools/read.d.ts +0 -1
- package/dist/tools/read.js +14 -2
- package/dist/tools/roots.d.ts +0 -1
- package/dist/tools/roots.js +14 -2
- package/dist/tools/search-content.d.ts +0 -1
- package/dist/tools/search-content.js +29 -14
- package/dist/tools/search-files.d.ts +0 -1
- package/dist/tools/search-files.js +14 -2
- package/dist/tools/shared.d.ts +7 -1
- package/dist/tools/shared.js +7 -4
- package/dist/tools/stat-many.d.ts +0 -1
- package/dist/tools/stat-many.js +14 -2
- package/dist/tools/stat.d.ts +0 -1
- package/dist/tools/stat.js +14 -2
- package/dist/tools/tree.d.ts +0 -1
- package/dist/tools/tree.js +14 -2
- package/dist/tools.d.ts +0 -1
- package/dist/tools.js +0 -1
- package/package.json +22 -19
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/constants.d.ts.map +0 -1
- package/dist/lib/constants.js.map +0 -1
- package/dist/lib/errors.d.ts.map +0 -1
- package/dist/lib/errors.js.map +0 -1
- package/dist/lib/file-operations/file-info.d.ts.map +0 -1
- package/dist/lib/file-operations/file-info.js.map +0 -1
- package/dist/lib/file-operations/gitignore.d.ts.map +0 -1
- package/dist/lib/file-operations/gitignore.js.map +0 -1
- package/dist/lib/file-operations/glob-engine.d.ts.map +0 -1
- package/dist/lib/file-operations/glob-engine.js.map +0 -1
- package/dist/lib/file-operations/list-directory.d.ts.map +0 -1
- package/dist/lib/file-operations/list-directory.js.map +0 -1
- package/dist/lib/file-operations/read-multiple-files.d.ts.map +0 -1
- package/dist/lib/file-operations/read-multiple-files.js.map +0 -1
- package/dist/lib/file-operations/search-content.d.ts.map +0 -1
- package/dist/lib/file-operations/search-content.js.map +0 -1
- package/dist/lib/file-operations/search-files.d.ts.map +0 -1
- package/dist/lib/file-operations/search-files.js.map +0 -1
- package/dist/lib/file-operations/search-worker.d.ts.map +0 -1
- package/dist/lib/file-operations/search-worker.js.map +0 -1
- package/dist/lib/file-operations/tree.d.ts.map +0 -1
- package/dist/lib/file-operations/tree.js.map +0 -1
- package/dist/lib/fs-helpers.d.ts.map +0 -1
- package/dist/lib/fs-helpers.js.map +0 -1
- package/dist/lib/observability.d.ts.map +0 -1
- package/dist/lib/observability.js.map +0 -1
- package/dist/lib/path-policy.d.ts.map +0 -1
- package/dist/lib/path-policy.js.map +0 -1
- package/dist/lib/path-validation.d.ts.map +0 -1
- package/dist/lib/path-validation.js.map +0 -1
- package/dist/lib/resource-store.d.ts.map +0 -1
- package/dist/lib/resource-store.js.map +0 -1
- package/dist/resources.d.ts.map +0 -1
- package/dist/resources.js.map +0 -1
- package/dist/schemas.d.ts.map +0 -1
- package/dist/schemas.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/tools/list-directory.d.ts.map +0 -1
- package/dist/tools/list-directory.js.map +0 -1
- package/dist/tools/read-multiple.d.ts.map +0 -1
- package/dist/tools/read-multiple.js.map +0 -1
- package/dist/tools/read.d.ts.map +0 -1
- package/dist/tools/read.js.map +0 -1
- package/dist/tools/roots.d.ts.map +0 -1
- package/dist/tools/roots.js.map +0 -1
- package/dist/tools/search-content.d.ts.map +0 -1
- package/dist/tools/search-content.js.map +0 -1
- package/dist/tools/search-files.d.ts.map +0 -1
- package/dist/tools/search-files.js.map +0 -1
- package/dist/tools/shared.d.ts.map +0 -1
- package/dist/tools/shared.js.map +0 -1
- package/dist/tools/stat-many.d.ts.map +0 -1
- package/dist/tools/stat-many.js.map +0 -1
- package/dist/tools/stat.d.ts.map +0 -1
- package/dist/tools/stat.js.map +0 -1
- package/dist/tools/tree.d.ts.map +0 -1
- package/dist/tools/tree.js.map +0 -1
- package/dist/tools.d.ts.map +0 -1
- package/dist/tools.js.map +0 -1
|
@@ -10,7 +10,7 @@ import { ErrorCode, McpError } from '../errors.js';
|
|
|
10
10
|
import { assertNotAborted, createTimedAbortSignal, isProbablyBinary, withAbort, } from '../fs-helpers.js';
|
|
11
11
|
import { publishOpsTraceEnd, publishOpsTraceError, publishOpsTraceStart, shouldPublishOpsTrace, } from '../observability.js';
|
|
12
12
|
import { assertAllowedFileAccess, isSensitivePath } from '../path-policy.js';
|
|
13
|
-
import { validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
13
|
+
import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
14
14
|
import { globEntries } from './glob-engine.js';
|
|
15
15
|
const INTERNAL_MAX_RESULTS = 500;
|
|
16
16
|
const DEFAULTS = {
|
|
@@ -29,10 +29,11 @@ const DEFAULTS = {
|
|
|
29
29
|
baseNameMatch: false,
|
|
30
30
|
caseSensitiveFileMatch: true,
|
|
31
31
|
};
|
|
32
|
-
function mergeOptions(
|
|
33
|
-
const
|
|
34
|
-
delete
|
|
35
|
-
|
|
32
|
+
function mergeOptions(options) {
|
|
33
|
+
const optionsCopy = { ...options };
|
|
34
|
+
delete optionsCopy.signal;
|
|
35
|
+
delete optionsCopy.onProgress;
|
|
36
|
+
return { ...DEFAULTS, ...optionsCopy };
|
|
36
37
|
}
|
|
37
38
|
function validatePattern(pattern, options) {
|
|
38
39
|
if (options.isLiteral && !options.wholeWord) {
|
|
@@ -328,18 +329,44 @@ function createScanSummary() {
|
|
|
328
329
|
stoppedReason: undefined,
|
|
329
330
|
};
|
|
330
331
|
}
|
|
332
|
+
function isTimeoutAbort(error) {
|
|
333
|
+
return (error instanceof Error &&
|
|
334
|
+
error.name === 'AbortError' &&
|
|
335
|
+
error.message.toLowerCase().includes('timed out'));
|
|
336
|
+
}
|
|
337
|
+
function buildTimeoutSearchResult(root, pattern, filePattern) {
|
|
338
|
+
const summary = createScanSummary();
|
|
339
|
+
markTruncated(summary, 'timeout');
|
|
340
|
+
return buildSearchResult(root, pattern, filePattern, [], summary);
|
|
341
|
+
}
|
|
342
|
+
function stopIfAborted(signal, summary) {
|
|
343
|
+
if (!signal.aborted)
|
|
344
|
+
return false;
|
|
345
|
+
markTruncated(summary, 'timeout');
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
331
348
|
function shouldStopCollecting(summary, maxFilesScanned, signal) {
|
|
332
|
-
if (signal
|
|
333
|
-
markTruncated(summary, 'timeout');
|
|
349
|
+
if (stopIfAborted(signal, summary))
|
|
334
350
|
return true;
|
|
335
|
-
}
|
|
336
351
|
if (summary.filesScanned >= maxFilesScanned) {
|
|
337
352
|
markTruncated(summary, 'maxFiles');
|
|
338
353
|
return true;
|
|
339
354
|
}
|
|
340
355
|
return false;
|
|
341
356
|
}
|
|
342
|
-
|
|
357
|
+
function resolveNonSymlinkEntryPath(root, entryPath) {
|
|
358
|
+
const normalized = normalizePath(entryPath);
|
|
359
|
+
if (!isPathWithinDirectories(normalized, [root]))
|
|
360
|
+
return null;
|
|
361
|
+
return {
|
|
362
|
+
resolvedPath: normalized,
|
|
363
|
+
requestedPath: entryPath,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
async function resolveEntryPath(entry, root, signal) {
|
|
367
|
+
if (!entry.dirent.isSymbolicLink()) {
|
|
368
|
+
return resolveNonSymlinkEntryPath(root, entry.path);
|
|
369
|
+
}
|
|
343
370
|
try {
|
|
344
371
|
const details = await validateExistingPathDetailed(entry.path, signal);
|
|
345
372
|
return {
|
|
@@ -351,14 +378,14 @@ async function resolveEntryPath(entry, signal) {
|
|
|
351
378
|
return null;
|
|
352
379
|
}
|
|
353
380
|
}
|
|
354
|
-
async function* collectFromStream(stream, opts, signal, summary, onProgress) {
|
|
381
|
+
async function* collectFromStream(stream, root, opts, signal, summary, onProgress) {
|
|
355
382
|
for await (const entry of stream) {
|
|
356
383
|
if (!entry.dirent.isFile())
|
|
357
384
|
continue;
|
|
358
385
|
if (shouldStopCollecting(summary, opts.maxFilesScanned, signal)) {
|
|
359
386
|
break;
|
|
360
387
|
}
|
|
361
|
-
const resolved = await resolveEntryPath(entry, signal);
|
|
388
|
+
const resolved = await resolveEntryPath(entry, root, signal);
|
|
362
389
|
if (!resolved) {
|
|
363
390
|
summary.skippedInaccessible++;
|
|
364
391
|
continue;
|
|
@@ -389,15 +416,13 @@ function collectFilesStream(root, opts, signal, onProgress) {
|
|
|
389
416
|
suppressErrors: true,
|
|
390
417
|
});
|
|
391
418
|
return {
|
|
392
|
-
stream: collectFromStream(stream, opts, signal, summary, onProgress),
|
|
419
|
+
stream: collectFromStream(stream, root, opts, signal, summary, onProgress),
|
|
393
420
|
summary,
|
|
394
421
|
};
|
|
395
422
|
}
|
|
396
423
|
function shouldStopOnSignalOrLimit(signal, matchesCount, maxResults, summary) {
|
|
397
|
-
if (signal
|
|
398
|
-
markTruncated(summary, 'timeout');
|
|
424
|
+
if (stopIfAborted(signal, summary))
|
|
399
425
|
return true;
|
|
400
|
-
}
|
|
401
426
|
if (matchesCount >= maxResults) {
|
|
402
427
|
markTruncated(summary, 'maxResults');
|
|
403
428
|
return true;
|
|
@@ -642,11 +667,24 @@ class SearchWorkerPool {
|
|
|
642
667
|
const scanRequest = this.buildScanRequest(id, request);
|
|
643
668
|
const promise = this.createScanPromise(slot, worker, scanRequest);
|
|
644
669
|
const cancel = this.createCancel(slot, worker, id);
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
})
|
|
649
|
-
const task = {
|
|
670
|
+
let resolveOutcome = () => { };
|
|
671
|
+
const outcome = new Promise((resolve) => {
|
|
672
|
+
resolveOutcome = resolve;
|
|
673
|
+
});
|
|
674
|
+
const task = {
|
|
675
|
+
id,
|
|
676
|
+
promise,
|
|
677
|
+
cancel,
|
|
678
|
+
outcome,
|
|
679
|
+
};
|
|
680
|
+
void promise.then((result) => {
|
|
681
|
+
resolveOutcome({ task, result });
|
|
682
|
+
}, (error) => {
|
|
683
|
+
resolveOutcome({
|
|
684
|
+
task,
|
|
685
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
686
|
+
});
|
|
687
|
+
});
|
|
650
688
|
return task;
|
|
651
689
|
}
|
|
652
690
|
async close() {
|
|
@@ -929,15 +967,19 @@ async function executeSearch(root, pattern, opts, signal, onProgress) {
|
|
|
929
967
|
export async function searchContent(basePath, pattern, options = {}) {
|
|
930
968
|
const opts = mergeOptions(options);
|
|
931
969
|
const { signal, cleanup } = createTimedAbortSignal(options.signal, opts.timeoutMs);
|
|
970
|
+
let resolvedBasePath;
|
|
932
971
|
try {
|
|
933
972
|
const details = await validateExistingPathDetailed(basePath, signal);
|
|
973
|
+
resolvedBasePath = details.resolvedPath;
|
|
934
974
|
const stats = await withAbort(fsp.stat(details.resolvedPath), signal);
|
|
935
975
|
if (stats.isDirectory()) {
|
|
936
976
|
const root = await validateExistingDirectory(details.resolvedPath, signal);
|
|
977
|
+
resolvedBasePath = root;
|
|
937
978
|
return await executeSearch(root, pattern, opts, signal, options.onProgress);
|
|
938
979
|
}
|
|
939
980
|
if (stats.isFile()) {
|
|
940
981
|
const baseDir = path.dirname(details.requestedPath);
|
|
982
|
+
resolvedBasePath = baseDir;
|
|
941
983
|
return await executeSearchSingleFile({
|
|
942
984
|
resolvedPath: details.resolvedPath,
|
|
943
985
|
requestedPath: details.requestedPath,
|
|
@@ -945,8 +987,13 @@ export async function searchContent(basePath, pattern, options = {}) {
|
|
|
945
987
|
}
|
|
946
988
|
throw new McpError(ErrorCode.E_INVALID_INPUT, `Path must be a file or directory: ${basePath}`, basePath);
|
|
947
989
|
}
|
|
990
|
+
catch (error) {
|
|
991
|
+
if (isTimeoutAbort(error)) {
|
|
992
|
+
return buildTimeoutSearchResult(resolvedBasePath ?? basePath, pattern, opts.filePattern);
|
|
993
|
+
}
|
|
994
|
+
throw error;
|
|
995
|
+
}
|
|
948
996
|
finally {
|
|
949
997
|
cleanup();
|
|
950
998
|
}
|
|
951
999
|
}
|
|
952
|
-
//# sourceMappingURL=search-content.js.map
|
|
@@ -20,4 +20,3 @@ interface Sortable {
|
|
|
20
20
|
export declare function sortSearchResults(results: Sortable[], sortBy: 'name' | 'size' | 'modified' | 'path'): void;
|
|
21
21
|
export declare function searchFiles(basePath: string, pattern: string, excludePatterns?: readonly string[], options?: SearchFilesOptions): Promise<SearchFilesResult>;
|
|
22
22
|
export {};
|
|
23
|
-
//# sourceMappingURL=search-files.d.ts.map
|
|
@@ -2,7 +2,7 @@ import * as path from 'node:path';
|
|
|
2
2
|
import { DEFAULT_SEARCH_MAX_FILES, DEFAULT_SEARCH_TIMEOUT_MS, } from '../constants.js';
|
|
3
3
|
import { createTimedAbortSignal } from '../fs-helpers.js';
|
|
4
4
|
import { isSensitivePath } from '../path-policy.js';
|
|
5
|
-
import { validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
5
|
+
import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
6
6
|
import { isIgnoredByGitignore, loadRootGitignore } from './gitignore.js';
|
|
7
7
|
import { globEntries } from './glob-engine.js';
|
|
8
8
|
// Internal default for find tool - not exposed to MCP users
|
|
@@ -126,22 +126,33 @@ async function collectFromStream(stream, root, gitignoreMatcher, normalized, nee
|
|
|
126
126
|
isIgnoredByGitignore(gitignoreMatcher, root, entry.path)) {
|
|
127
127
|
continue;
|
|
128
128
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
state.skippedInaccessible++;
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
129
|
+
const entryType = resolveEntryType(entry.dirent);
|
|
130
|
+
const isAccessible = await isEntryAccessible(entry, entryType, root, signal);
|
|
131
|
+
if (!isAccessible) {
|
|
137
132
|
state.skippedInaccessible++;
|
|
138
133
|
continue;
|
|
139
134
|
}
|
|
140
|
-
handleEntry(entry,
|
|
135
|
+
handleEntry(entry, entryType, needsStats, normalized, state);
|
|
141
136
|
if (state.truncated)
|
|
142
137
|
break;
|
|
143
138
|
}
|
|
144
139
|
}
|
|
140
|
+
async function isEntryAccessible(entry, entryType, root, signal) {
|
|
141
|
+
if (entryType === 'symlink') {
|
|
142
|
+
try {
|
|
143
|
+
const validated = await validateExistingPathDetailed(entry.path, signal);
|
|
144
|
+
return !isSensitivePath(validated.requestedPath, validated.resolvedPath);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const normalized = normalizePath(entry.path);
|
|
151
|
+
if (!isPathWithinDirectories(normalized, [root])) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return !isSensitivePath(entry.path, normalized);
|
|
155
|
+
}
|
|
145
156
|
async function collectSearchResults(root, pattern, excludePatterns, normalized, signal) {
|
|
146
157
|
const needsStats = needsStatsForSort(normalized.sortBy);
|
|
147
158
|
const stream = buildSearchStream(root, pattern, excludePatterns, normalized, needsStats);
|
|
@@ -238,4 +249,3 @@ export async function searchFiles(basePath, pattern, excludePatterns = [], optio
|
|
|
238
249
|
cleanup();
|
|
239
250
|
}
|
|
240
251
|
}
|
|
241
|
-
//# sourceMappingURL=search-files.js.map
|
|
@@ -2,7 +2,7 @@ import * as path from 'node:path';
|
|
|
2
2
|
import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_TIMEOUT_MS, } from '../constants.js';
|
|
3
3
|
import { createTimedAbortSignal } from '../fs-helpers.js';
|
|
4
4
|
import { isSensitivePath } from '../path-policy.js';
|
|
5
|
-
import { validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
5
|
+
import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
6
6
|
import { isIgnoredByGitignore, loadRootGitignore } from './gitignore.js';
|
|
7
7
|
import { globEntries } from './glob-engine.js';
|
|
8
8
|
function normalizeOptions(options) {
|
|
@@ -79,16 +79,27 @@ function getStopReason(signal, totalEntries, maxEntries) {
|
|
|
79
79
|
return undefined;
|
|
80
80
|
}
|
|
81
81
|
async function resolveTreeEntry(entry, root, gitignoreMatcher, signal) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
const type = resolveEntryType(entry.dirent);
|
|
83
|
+
if (type !== 'symlink') {
|
|
84
|
+
const normalized = normalizePath(entry.path);
|
|
85
|
+
if (!isPathWithinDirectories(normalized, [root])) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (isSensitivePath(entry.path, normalized)) {
|
|
85
89
|
return null;
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
else {
|
|
93
|
+
try {
|
|
94
|
+
const validated = await validateExistingPathDetailed(entry.path, signal);
|
|
95
|
+
if (isSensitivePath(validated.requestedPath, validated.resolvedPath)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
90
102
|
}
|
|
91
|
-
const type = resolveEntryType(entry.dirent);
|
|
92
103
|
if (gitignoreMatcher &&
|
|
93
104
|
isIgnoredByGitignore(gitignoreMatcher, root, entry.path, {
|
|
94
105
|
isDirectory: type === 'directory',
|
|
@@ -194,4 +205,3 @@ export async function treeDirectory(dirPath, options = {}) {
|
|
|
194
205
|
cleanup();
|
|
195
206
|
}
|
|
196
207
|
}
|
|
197
|
-
//# sourceMappingURL=tree.js.map
|
package/dist/lib/fs-helpers.d.ts
CHANGED
|
@@ -44,4 +44,3 @@ declare function headFile(handle: fsp.FileHandle, numLines: number, encoding?: B
|
|
|
44
44
|
export declare function readFileWithStats(filePath: string, validPath: string, stats: Stats, options?: ReadFileOptions): Promise<ReadFileResult>;
|
|
45
45
|
export declare function readFile(filePath: string, options?: ReadFileOptions): Promise<ReadFileResult>;
|
|
46
46
|
export { headFile };
|
|
47
|
-
//# sourceMappingURL=fs-helpers.d.ts.map
|
package/dist/lib/fs-helpers.js
CHANGED
|
@@ -15,24 +15,21 @@ export function assertNotAborted(signal, message) {
|
|
|
15
15
|
if (!signal?.aborted)
|
|
16
16
|
return;
|
|
17
17
|
const { reason } = signal;
|
|
18
|
-
if (reason instanceof Error)
|
|
18
|
+
if (reason instanceof Error)
|
|
19
19
|
throw reason;
|
|
20
|
-
}
|
|
21
20
|
throw createAbortError(message);
|
|
22
21
|
}
|
|
23
22
|
function getAbortError(signal, message) {
|
|
24
23
|
const { reason } = signal;
|
|
25
|
-
if (reason instanceof Error)
|
|
24
|
+
if (reason instanceof Error)
|
|
26
25
|
return reason;
|
|
27
|
-
}
|
|
28
26
|
return createAbortError(message);
|
|
29
27
|
}
|
|
30
28
|
export function withAbort(promise, signal) {
|
|
31
29
|
if (!signal)
|
|
32
30
|
return promise;
|
|
33
|
-
if (signal.aborted)
|
|
31
|
+
if (signal.aborted)
|
|
34
32
|
throw getAbortError(signal);
|
|
35
|
-
}
|
|
36
33
|
return new Promise((resolve, reject) => {
|
|
37
34
|
const onAbort = () => {
|
|
38
35
|
reject(getAbortError(signal));
|
|
@@ -56,9 +53,6 @@ export function createTimedAbortSignal(baseSignal, timeoutMs) {
|
|
|
56
53
|
if (!timeoutMs && baseSignal) {
|
|
57
54
|
return createForwardedSignal(baseSignal);
|
|
58
55
|
}
|
|
59
|
-
if (typeof timeoutMs === 'number' && shouldUseAbortAny(baseSignal)) {
|
|
60
|
-
return createAnySignal(baseSignal, timeoutMs);
|
|
61
|
-
}
|
|
62
56
|
return createManualSignal(baseSignal, timeoutMs);
|
|
63
57
|
}
|
|
64
58
|
function createNoopSignal() {
|
|
@@ -68,19 +62,10 @@ function createNoopSignal() {
|
|
|
68
62
|
function createForwardedSignal(baseSignal) {
|
|
69
63
|
return { signal: baseSignal, cleanup: () => { } };
|
|
70
64
|
}
|
|
71
|
-
function shouldUseAbortAny(baseSignal) {
|
|
72
|
-
return typeof AbortSignal.any === 'function' && baseSignal !== undefined;
|
|
73
|
-
}
|
|
74
|
-
function createAnySignal(baseSignal, timeoutMs) {
|
|
75
|
-
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
76
|
-
const combined = AbortSignal.any([baseSignal, timeoutSignal]);
|
|
77
|
-
return { signal: combined, cleanup: () => { } };
|
|
78
|
-
}
|
|
79
65
|
function createManualSignal(baseSignal, timeoutMs) {
|
|
80
66
|
const controller = new AbortController();
|
|
81
67
|
const forwardAbort = () => {
|
|
82
|
-
|
|
83
|
-
controller.abort(reason);
|
|
68
|
+
controller.abort(baseSignal?.reason);
|
|
84
69
|
};
|
|
85
70
|
if (baseSignal) {
|
|
86
71
|
if (baseSignal.aborted) {
|
|
@@ -105,9 +90,11 @@ function createTimeout(controller, timeoutMs) {
|
|
|
105
90
|
if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs)) {
|
|
106
91
|
return undefined;
|
|
107
92
|
}
|
|
108
|
-
|
|
93
|
+
const timeoutId = setTimeout(() => {
|
|
109
94
|
controller.abort(createAbortError('Operation timed out'));
|
|
110
95
|
}, timeoutMs);
|
|
96
|
+
timeoutId.unref?.();
|
|
97
|
+
return timeoutId;
|
|
111
98
|
}
|
|
112
99
|
function createParallelAbortError() {
|
|
113
100
|
return createAbortError();
|
|
@@ -137,15 +124,20 @@ function attachAbortListener(state, signal) {
|
|
|
137
124
|
}
|
|
138
125
|
function createAbortPromise(signal) {
|
|
139
126
|
if (!signal)
|
|
140
|
-
return
|
|
127
|
+
return { cleanup: () => { } };
|
|
141
128
|
if (signal.aborted)
|
|
142
|
-
return Promise.resolve();
|
|
143
|
-
|
|
129
|
+
return { abortPromise: Promise.resolve(), cleanup: () => { } };
|
|
130
|
+
let cleanup = () => { };
|
|
131
|
+
const abortPromise = new Promise((resolve) => {
|
|
144
132
|
const onAbort = () => {
|
|
145
133
|
resolve();
|
|
146
134
|
};
|
|
147
135
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
136
|
+
cleanup = () => {
|
|
137
|
+
signal.removeEventListener('abort', onAbort);
|
|
138
|
+
};
|
|
148
139
|
});
|
|
140
|
+
return { abortPromise, cleanup };
|
|
149
141
|
}
|
|
150
142
|
function canStartNext(state) {
|
|
151
143
|
return (!state.aborted &&
|
|
@@ -195,8 +187,9 @@ async function drainTasks(state, abortPromise) {
|
|
|
195
187
|
}
|
|
196
188
|
export async function processInParallel(items, processor, concurrency = PARALLEL_CONCURRENCY, signal) {
|
|
197
189
|
const state = createState(items, processor, concurrency, signal);
|
|
198
|
-
const abortPromise = createAbortPromise(signal);
|
|
190
|
+
const { abortPromise, cleanup: cleanupAbortPromise } = createAbortPromise(signal);
|
|
199
191
|
if (items.length === 0) {
|
|
192
|
+
cleanupAbortPromise();
|
|
200
193
|
return { results: state.results, errors: state.errors };
|
|
201
194
|
}
|
|
202
195
|
const detachAbort = attachAbortListener(state, signal);
|
|
@@ -205,6 +198,7 @@ export async function processInParallel(items, processor, concurrency = PARALLEL
|
|
|
205
198
|
}
|
|
206
199
|
finally {
|
|
207
200
|
detachAbort();
|
|
201
|
+
cleanupAbortPromise();
|
|
208
202
|
}
|
|
209
203
|
if (state.aborted) {
|
|
210
204
|
throw createParallelAbortError();
|
|
@@ -441,6 +435,7 @@ async function readRangeContent(handle, startLine, endLine, options) {
|
|
|
441
435
|
continue;
|
|
442
436
|
}
|
|
443
437
|
if (lineNumber > stopAt) {
|
|
438
|
+
hasMoreLines = true;
|
|
444
439
|
stoppedEarly = true;
|
|
445
440
|
break;
|
|
446
441
|
}
|
|
@@ -598,4 +593,3 @@ export async function readFile(filePath, options = {}) {
|
|
|
598
593
|
return await readFileWithStatsInternal(filePath, validPath, stats, normalized);
|
|
599
594
|
}
|
|
600
595
|
export { headFile };
|
|
601
|
-
//# sourceMappingURL=fs-helpers.js.map
|
|
@@ -22,9 +22,8 @@ function resolveDiagnosticsOk(result) {
|
|
|
22
22
|
function resolvePrimitiveDiagnosticsMessage(error) {
|
|
23
23
|
if (typeof error === 'string')
|
|
24
24
|
return error;
|
|
25
|
-
if (typeof error === 'number' || typeof error === 'boolean')
|
|
25
|
+
if (typeof error === 'number' || typeof error === 'boolean')
|
|
26
26
|
return String(error);
|
|
27
|
-
}
|
|
28
27
|
if (typeof error === 'bigint')
|
|
29
28
|
return error.toString();
|
|
30
29
|
if (typeof error === 'symbol')
|
|
@@ -73,15 +72,19 @@ function captureEventLoopUtilization() {
|
|
|
73
72
|
function diffEventLoopUtilization(start) {
|
|
74
73
|
return performance.eventLoopUtilization(start);
|
|
75
74
|
}
|
|
75
|
+
function elapsedMs(startNs) {
|
|
76
|
+
return Number(process.hrtime.bigint() - startNs) / 1_000_000;
|
|
77
|
+
}
|
|
76
78
|
const TOOL_CHANNEL = channel('fs-context:tool');
|
|
77
79
|
const PERF_CHANNEL = channel('fs-context:perf');
|
|
78
80
|
const OPS_TRACE = tracingChannel('fs-context:ops');
|
|
79
|
-
function
|
|
80
|
-
const normalized = process.env[
|
|
81
|
-
?.trim()
|
|
82
|
-
.toLowerCase();
|
|
81
|
+
function parseEnvBoolean(name) {
|
|
82
|
+
const normalized = process.env[name]?.trim().toLowerCase();
|
|
83
83
|
return normalized === '1' || normalized === 'true' || normalized === 'yes';
|
|
84
84
|
}
|
|
85
|
+
function parseDiagnosticsEnabled() {
|
|
86
|
+
return parseEnvBoolean('FS_CONTEXT_DIAGNOSTICS');
|
|
87
|
+
}
|
|
85
88
|
function parseDiagnosticsDetail() {
|
|
86
89
|
const normalized = process.env['FS_CONTEXT_DIAGNOSTICS_DETAIL']?.trim();
|
|
87
90
|
if (normalized === '2')
|
|
@@ -91,16 +94,19 @@ function parseDiagnosticsDetail() {
|
|
|
91
94
|
return 0;
|
|
92
95
|
}
|
|
93
96
|
function parseToolErrorLogging() {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return
|
|
97
|
+
return parseEnvBoolean('FS_CONTEXT_TOOL_LOG_ERRORS');
|
|
98
|
+
}
|
|
99
|
+
function readDiagnosticsConfig() {
|
|
100
|
+
return {
|
|
101
|
+
enabled: parseDiagnosticsEnabled(),
|
|
102
|
+
detail: parseDiagnosticsDetail(),
|
|
103
|
+
logToolErrors: parseToolErrorLogging(),
|
|
104
|
+
};
|
|
98
105
|
}
|
|
99
106
|
function hashPath(value) {
|
|
100
107
|
return createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
101
108
|
}
|
|
102
|
-
function normalizePathForDiagnostics(pathValue) {
|
|
103
|
-
const detail = parseDiagnosticsDetail();
|
|
109
|
+
function normalizePathForDiagnostics(pathValue, detail) {
|
|
104
110
|
if (detail === 0)
|
|
105
111
|
return undefined;
|
|
106
112
|
if (detail === 2)
|
|
@@ -110,7 +116,8 @@ function normalizePathForDiagnostics(pathValue) {
|
|
|
110
116
|
function normalizeOpsTraceContext(context) {
|
|
111
117
|
if (!context.path)
|
|
112
118
|
return context;
|
|
113
|
-
const
|
|
119
|
+
const detail = parseDiagnosticsDetail();
|
|
120
|
+
const normalizedPath = normalizePathForDiagnostics(context.path, detail);
|
|
114
121
|
if (!normalizedPath) {
|
|
115
122
|
const sanitized = { ...context };
|
|
116
123
|
delete sanitized.path;
|
|
@@ -118,10 +125,10 @@ function normalizeOpsTraceContext(context) {
|
|
|
118
125
|
}
|
|
119
126
|
return { ...context, path: normalizedPath };
|
|
120
127
|
}
|
|
121
|
-
function publishStartEvent(tool, options) {
|
|
128
|
+
function publishStartEvent(tool, options, detail) {
|
|
122
129
|
const event = { phase: 'start', tool };
|
|
123
130
|
const normalizedPath = options?.path
|
|
124
|
-
? normalizePathForDiagnostics(options.path)
|
|
131
|
+
? normalizePathForDiagnostics(options.path, detail)
|
|
125
132
|
: undefined;
|
|
126
133
|
if (normalizedPath !== undefined) {
|
|
127
134
|
event.path = normalizedPath;
|
|
@@ -153,18 +160,18 @@ function publishPerfEndEvent(tool, durationMs, elu) {
|
|
|
153
160
|
},
|
|
154
161
|
});
|
|
155
162
|
}
|
|
156
|
-
function startToolDiagnostics(tool, options, shouldPublishTool, shouldPublishPerf) {
|
|
163
|
+
function startToolDiagnostics(tool, options, config, shouldPublishTool, shouldPublishPerf) {
|
|
157
164
|
const startNs = process.hrtime.bigint();
|
|
158
165
|
const eluStart = shouldPublishPerf
|
|
159
166
|
? captureEventLoopUtilization()
|
|
160
167
|
: undefined;
|
|
161
|
-
if (shouldPublishTool)
|
|
162
|
-
publishStartEvent(tool, options);
|
|
168
|
+
if (shouldPublishTool) {
|
|
169
|
+
publishStartEvent(tool, options, config.detail);
|
|
170
|
+
}
|
|
163
171
|
return { startNs, eluStart };
|
|
164
172
|
}
|
|
165
173
|
function finalizeToolDiagnostics(tool, startNs, options) {
|
|
166
|
-
const
|
|
167
|
-
const durationMs = Number(endNs - startNs) / 1_000_000;
|
|
174
|
+
const durationMs = elapsedMs(startNs);
|
|
168
175
|
if (options.shouldPublishPerf && options.eluStart) {
|
|
169
176
|
publishPerfEndEvent(tool, durationMs, diffEventLoopUtilization(options.eluStart));
|
|
170
177
|
}
|
|
@@ -172,8 +179,8 @@ function finalizeToolDiagnostics(tool, startNs, options) {
|
|
|
172
179
|
publishEndEvent(tool, options.ok, durationMs, options.error);
|
|
173
180
|
}
|
|
174
181
|
}
|
|
175
|
-
async function runWithDiagnostics(tool, run, options, shouldPublishTool, shouldPublishPerf) {
|
|
176
|
-
const { startNs, eluStart } = startToolDiagnostics(tool, options, shouldPublishTool, shouldPublishPerf);
|
|
182
|
+
async function runWithDiagnostics(tool, run, options, config, shouldPublishTool, shouldPublishPerf) {
|
|
183
|
+
const { startNs, eluStart } = startToolDiagnostics(tool, options, config, shouldPublishTool, shouldPublishPerf);
|
|
177
184
|
const finalizeOptions = { shouldPublishTool, shouldPublishPerf, eluStart };
|
|
178
185
|
try {
|
|
179
186
|
const result = await run();
|
|
@@ -192,6 +199,21 @@ async function runWithDiagnostics(tool, run, options, shouldPublishTool, shouldP
|
|
|
192
199
|
throw error;
|
|
193
200
|
}
|
|
194
201
|
}
|
|
202
|
+
async function runWithErrorLogging(tool, run) {
|
|
203
|
+
const startNs = process.hrtime.bigint();
|
|
204
|
+
try {
|
|
205
|
+
const result = await run();
|
|
206
|
+
const ok = resolveDiagnosticsOk(result);
|
|
207
|
+
if (ok === false) {
|
|
208
|
+
logToolError(tool, elapsedMs(startNs), resolveResultErrorMessage(result));
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
logToolError(tool, elapsedMs(startNs), resolveDiagnosticsErrorMessage(error));
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
195
217
|
export function shouldPublishOpsTrace() {
|
|
196
218
|
return parseDiagnosticsEnabled() && OPS_TRACE.hasSubscribers;
|
|
197
219
|
}
|
|
@@ -208,32 +230,16 @@ export function publishOpsTraceError(context, error) {
|
|
|
208
230
|
});
|
|
209
231
|
}
|
|
210
232
|
export async function withToolDiagnostics(tool, run, options) {
|
|
211
|
-
const
|
|
212
|
-
if (!
|
|
213
|
-
if (!
|
|
233
|
+
const config = readDiagnosticsConfig();
|
|
234
|
+
if (!config.enabled) {
|
|
235
|
+
if (!config.logToolErrors)
|
|
214
236
|
return await run();
|
|
215
|
-
|
|
216
|
-
const startNs = process.hrtime.bigint();
|
|
217
|
-
try {
|
|
218
|
-
const result = await run();
|
|
219
|
-
const ok = resolveDiagnosticsOk(result);
|
|
220
|
-
if (ok === false) {
|
|
221
|
-
const durationMs = Number(process.hrtime.bigint() - startNs) / 1_000_000;
|
|
222
|
-
logToolError(tool, durationMs, resolveResultErrorMessage(result));
|
|
223
|
-
}
|
|
224
|
-
return result;
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
const durationMs = Number(process.hrtime.bigint() - startNs) / 1_000_000;
|
|
228
|
-
logToolError(tool, durationMs, resolveDiagnosticsErrorMessage(error));
|
|
229
|
-
throw error;
|
|
230
|
-
}
|
|
237
|
+
return await runWithErrorLogging(tool, run);
|
|
231
238
|
}
|
|
232
239
|
const shouldPublishTool = TOOL_CHANNEL.hasSubscribers;
|
|
233
240
|
const shouldPublishPerf = PERF_CHANNEL.hasSubscribers;
|
|
234
241
|
if (!shouldPublishTool && !shouldPublishPerf) {
|
|
235
242
|
return await run();
|
|
236
243
|
}
|
|
237
|
-
return await runWithDiagnostics(tool, run, options, shouldPublishTool, shouldPublishPerf);
|
|
244
|
+
return await runWithDiagnostics(tool, run, options, config, shouldPublishTool, shouldPublishPerf);
|
|
238
245
|
}
|
|
239
|
-
//# sourceMappingURL=observability.js.map
|
package/dist/lib/path-policy.js
CHANGED
|
@@ -66,4 +66,3 @@ export function assertAllowedFileAccess(requestedPath, resolvedPath) {
|
|
|
66
66
|
throw new McpError(ErrorCode.E_ACCESS_DENIED, `Access denied: sensitive file blocked by policy (${requestedPath}). ` +
|
|
67
67
|
'Set FS_CONTEXT_ALLOW_SENSITIVE=1 or use FS_CONTEXT_ALLOWLIST to override.', requestedPath);
|
|
68
68
|
}
|
|
69
|
-
//# sourceMappingURL=path-policy.js.map
|
|
@@ -18,4 +18,3 @@ export declare function validateExistingPath(requestedPath: string, signal?: Abo
|
|
|
18
18
|
export declare function validateExistingDirectory(requestedPath: string, signal?: AbortSignal): Promise<string>;
|
|
19
19
|
export declare function getValidRootDirectories(roots: Root[], signal?: AbortSignal): Promise<string[]>;
|
|
20
20
|
export {};
|
|
21
|
-
//# sourceMappingURL=path-validation.d.ts.map
|