@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.
Files changed (131) hide show
  1. package/README.md +171 -417
  2. package/dist/assets/logo.svg +3766 -0
  3. package/dist/config.d.ts +0 -1
  4. package/dist/config.js +0 -1
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.js +0 -1
  7. package/dist/lib/constants.d.ts +0 -1
  8. package/dist/lib/constants.js +0 -1
  9. package/dist/lib/errors.d.ts +0 -1
  10. package/dist/lib/errors.js +0 -1
  11. package/dist/lib/file-operations/file-info.d.ts +0 -1
  12. package/dist/lib/file-operations/file-info.js +0 -1
  13. package/dist/lib/file-operations/gitignore.d.ts +0 -1
  14. package/dist/lib/file-operations/gitignore.js +0 -1
  15. package/dist/lib/file-operations/glob-engine.d.ts +0 -1
  16. package/dist/lib/file-operations/glob-engine.js +101 -84
  17. package/dist/lib/file-operations/list-directory.d.ts +0 -1
  18. package/dist/lib/file-operations/list-directory.js +91 -62
  19. package/dist/lib/file-operations/read-multiple-files.d.ts +0 -1
  20. package/dist/lib/file-operations/read-multiple-files.js +0 -1
  21. package/dist/lib/file-operations/search-content.d.ts +0 -1
  22. package/dist/lib/file-operations/search-content.js +68 -21
  23. package/dist/lib/file-operations/search-files.d.ts +0 -1
  24. package/dist/lib/file-operations/search-files.js +21 -11
  25. package/dist/lib/file-operations/search-worker.d.ts +0 -1
  26. package/dist/lib/file-operations/search-worker.js +0 -1
  27. package/dist/lib/file-operations/tree.d.ts +0 -1
  28. package/dist/lib/file-operations/tree.js +18 -8
  29. package/dist/lib/fs-helpers.d.ts +0 -1
  30. package/dist/lib/fs-helpers.js +19 -25
  31. package/dist/lib/observability.d.ts +0 -1
  32. package/dist/lib/observability.js +49 -43
  33. package/dist/lib/path-policy.d.ts +0 -1
  34. package/dist/lib/path-policy.js +0 -1
  35. package/dist/lib/path-validation.d.ts +0 -1
  36. package/dist/lib/path-validation.js +54 -38
  37. package/dist/lib/resource-store.d.ts +0 -1
  38. package/dist/lib/resource-store.js +0 -1
  39. package/dist/resources.d.ts +2 -3
  40. package/dist/resources.js +16 -3
  41. package/dist/schemas.d.ts +0 -1
  42. package/dist/schemas.js +35 -51
  43. package/dist/server.d.ts +0 -1
  44. package/dist/server.js +42 -11
  45. package/dist/tools/list-directory.d.ts +0 -1
  46. package/dist/tools/list-directory.js +14 -2
  47. package/dist/tools/read-multiple.d.ts +0 -1
  48. package/dist/tools/read-multiple.js +14 -2
  49. package/dist/tools/read.d.ts +0 -1
  50. package/dist/tools/read.js +14 -2
  51. package/dist/tools/roots.d.ts +0 -1
  52. package/dist/tools/roots.js +14 -2
  53. package/dist/tools/search-content.d.ts +0 -1
  54. package/dist/tools/search-content.js +29 -14
  55. package/dist/tools/search-files.d.ts +0 -1
  56. package/dist/tools/search-files.js +14 -2
  57. package/dist/tools/shared.d.ts +7 -1
  58. package/dist/tools/shared.js +7 -4
  59. package/dist/tools/stat-many.d.ts +0 -1
  60. package/dist/tools/stat-many.js +14 -2
  61. package/dist/tools/stat.d.ts +0 -1
  62. package/dist/tools/stat.js +14 -2
  63. package/dist/tools/tree.d.ts +0 -1
  64. package/dist/tools/tree.js +14 -2
  65. package/dist/tools.d.ts +0 -1
  66. package/dist/tools.js +0 -1
  67. package/package.json +22 -19
  68. package/dist/config.d.ts.map +0 -1
  69. package/dist/config.js.map +0 -1
  70. package/dist/index.d.ts.map +0 -1
  71. package/dist/index.js.map +0 -1
  72. package/dist/lib/constants.d.ts.map +0 -1
  73. package/dist/lib/constants.js.map +0 -1
  74. package/dist/lib/errors.d.ts.map +0 -1
  75. package/dist/lib/errors.js.map +0 -1
  76. package/dist/lib/file-operations/file-info.d.ts.map +0 -1
  77. package/dist/lib/file-operations/file-info.js.map +0 -1
  78. package/dist/lib/file-operations/gitignore.d.ts.map +0 -1
  79. package/dist/lib/file-operations/gitignore.js.map +0 -1
  80. package/dist/lib/file-operations/glob-engine.d.ts.map +0 -1
  81. package/dist/lib/file-operations/glob-engine.js.map +0 -1
  82. package/dist/lib/file-operations/list-directory.d.ts.map +0 -1
  83. package/dist/lib/file-operations/list-directory.js.map +0 -1
  84. package/dist/lib/file-operations/read-multiple-files.d.ts.map +0 -1
  85. package/dist/lib/file-operations/read-multiple-files.js.map +0 -1
  86. package/dist/lib/file-operations/search-content.d.ts.map +0 -1
  87. package/dist/lib/file-operations/search-content.js.map +0 -1
  88. package/dist/lib/file-operations/search-files.d.ts.map +0 -1
  89. package/dist/lib/file-operations/search-files.js.map +0 -1
  90. package/dist/lib/file-operations/search-worker.d.ts.map +0 -1
  91. package/dist/lib/file-operations/search-worker.js.map +0 -1
  92. package/dist/lib/file-operations/tree.d.ts.map +0 -1
  93. package/dist/lib/file-operations/tree.js.map +0 -1
  94. package/dist/lib/fs-helpers.d.ts.map +0 -1
  95. package/dist/lib/fs-helpers.js.map +0 -1
  96. package/dist/lib/observability.d.ts.map +0 -1
  97. package/dist/lib/observability.js.map +0 -1
  98. package/dist/lib/path-policy.d.ts.map +0 -1
  99. package/dist/lib/path-policy.js.map +0 -1
  100. package/dist/lib/path-validation.d.ts.map +0 -1
  101. package/dist/lib/path-validation.js.map +0 -1
  102. package/dist/lib/resource-store.d.ts.map +0 -1
  103. package/dist/lib/resource-store.js.map +0 -1
  104. package/dist/resources.d.ts.map +0 -1
  105. package/dist/resources.js.map +0 -1
  106. package/dist/schemas.d.ts.map +0 -1
  107. package/dist/schemas.js.map +0 -1
  108. package/dist/server.d.ts.map +0 -1
  109. package/dist/server.js.map +0 -1
  110. package/dist/tools/list-directory.d.ts.map +0 -1
  111. package/dist/tools/list-directory.js.map +0 -1
  112. package/dist/tools/read-multiple.d.ts.map +0 -1
  113. package/dist/tools/read-multiple.js.map +0 -1
  114. package/dist/tools/read.d.ts.map +0 -1
  115. package/dist/tools/read.js.map +0 -1
  116. package/dist/tools/roots.d.ts.map +0 -1
  117. package/dist/tools/roots.js.map +0 -1
  118. package/dist/tools/search-content.d.ts.map +0 -1
  119. package/dist/tools/search-content.js.map +0 -1
  120. package/dist/tools/search-files.d.ts.map +0 -1
  121. package/dist/tools/search-files.js.map +0 -1
  122. package/dist/tools/shared.d.ts.map +0 -1
  123. package/dist/tools/shared.js.map +0 -1
  124. package/dist/tools/stat-many.d.ts.map +0 -1
  125. package/dist/tools/stat-many.js.map +0 -1
  126. package/dist/tools/stat.d.ts.map +0 -1
  127. package/dist/tools/stat.js.map +0 -1
  128. package/dist/tools/tree.d.ts.map +0 -1
  129. package/dist/tools/tree.js.map +0 -1
  130. package/dist/tools.d.ts.map +0 -1
  131. 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(partial) {
33
- const rest = { ...partial };
34
- delete rest.signal;
35
- return { ...DEFAULTS, ...rest };
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.aborted) {
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
- async function resolveEntryPath(entry, signal) {
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.aborted) {
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
- const outcome = promise.then((result) => ({ task, result }), (error) => ({
646
- task,
647
- error: error instanceof Error ? error : new Error(String(error)),
648
- }));
649
- const task = { id, promise, cancel, outcome };
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
- try {
130
- const validated = await validateExistingPathDetailed(entry.path, signal);
131
- if (isSensitivePath(validated.requestedPath, validated.resolvedPath)) {
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, resolveEntryType(entry.dirent), needsStats, normalized, state);
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
@@ -1,3 +1,2 @@
1
1
  import type { ScanRequest, ScanResult, WorkerResponse } from './search-content.js';
2
2
  export type { WorkerResponse, ScanRequest, ScanResult };
3
- //# sourceMappingURL=search-worker.d.ts.map
@@ -94,4 +94,3 @@ if (parentPort) {
94
94
  console.error(`[SearchWorker] Started with threadId=${String(data.threadId)}`);
95
95
  }
96
96
  }
97
- //# sourceMappingURL=search-worker.js.map
@@ -22,4 +22,3 @@ export interface TreeResult {
22
22
  export declare function formatTreeAscii(tree: TreeEntry): string;
23
23
  export declare function treeDirectory(dirPath: string, options?: TreeOptions): Promise<TreeResult>;
24
24
  export {};
25
- //# sourceMappingURL=tree.d.ts.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
- try {
83
- const validated = await validateExistingPathDetailed(entry.path, signal);
84
- if (isSensitivePath(validated.requestedPath, validated.resolvedPath)) {
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
- catch {
89
- return null;
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
@@ -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
@@ -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
- const reason = baseSignal?.reason instanceof Error ? baseSignal.reason : undefined;
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
- return setTimeout(() => {
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 undefined;
127
+ return { cleanup: () => { } };
141
128
  if (signal.aborted)
142
- return Promise.resolve();
143
- return new Promise((resolve) => {
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
@@ -12,4 +12,3 @@ export declare function publishOpsTraceError(context: OpsTraceContext, error: un
12
12
  export declare function withToolDiagnostics<T>(tool: string, run: () => Promise<T>, options?: {
13
13
  path?: string;
14
14
  }): Promise<T>;
15
- //# sourceMappingURL=observability.d.ts.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 parseDiagnosticsEnabled() {
80
- const normalized = process.env['FS_CONTEXT_DIAGNOSTICS']
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
- const normalized = process.env['FS_CONTEXT_TOOL_LOG_ERRORS']
95
- ?.trim()
96
- .toLowerCase();
97
- return normalized === '1' || normalized === 'true' || normalized === 'yes';
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 normalizedPath = normalizePathForDiagnostics(context.path);
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 endNs = process.hrtime.bigint();
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 shouldLogErrors = parseToolErrorLogging();
212
- if (!parseDiagnosticsEnabled()) {
213
- if (!shouldLogErrors) {
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
@@ -1,3 +1,2 @@
1
1
  export declare function isSensitivePath(requestedPath: string, resolvedPath?: string): boolean;
2
2
  export declare function assertAllowedFileAccess(requestedPath: string, resolvedPath?: string): void;
3
- //# sourceMappingURL=path-policy.d.ts.map
@@ -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