@j0hanz/filesystem-mcp 1.7.1 → 1.7.3

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.
@@ -291,6 +291,25 @@ async function scanFileResolved(resolvedPath, requestedPath, matcher, options, s
291
291
  await handle.close();
292
292
  }
293
293
  }
294
+ function buildScanFileOptions(opts) {
295
+ return {
296
+ maxFileSize: opts.maxFileSize,
297
+ skipBinary: opts.skipBinary,
298
+ contextLines: opts.contextLines,
299
+ };
300
+ }
301
+ function applyScanOutcome(summary, outcome) {
302
+ if (outcome.matched)
303
+ summary.filesMatched++;
304
+ if (outcome.skippedBinary)
305
+ summary.skippedBinary++;
306
+ if (outcome.skippedTooLarge)
307
+ summary.skippedTooLarge++;
308
+ }
309
+ function markTruncated(summary, reason) {
310
+ summary.truncated = true;
311
+ summary.stoppedReason = reason;
312
+ }
294
313
  function createScanSummary() {
295
314
  return {
296
315
  filesScanned: 0,
@@ -474,32 +493,21 @@ function getPool() {
474
493
  async function executeSequential(files, pattern, opts, signal, summary) {
475
494
  const matches = [];
476
495
  const matcher = buildMatcher(pattern, opts);
477
- const scanOpts = {
478
- maxFileSize: opts.maxFileSize,
479
- skipBinary: opts.skipBinary,
480
- contextLines: opts.contextLines,
481
- };
496
+ const scanOpts = buildScanFileOptions(opts);
482
497
  for await (const file of files) {
483
498
  if (signal.aborted) {
484
- summary.truncated = true;
485
- summary.stoppedReason = 'timeout';
499
+ markTruncated(summary, 'timeout');
486
500
  break;
487
501
  }
488
502
  if (matches.length >= opts.maxResults) {
489
- summary.truncated = true;
490
- summary.stoppedReason = 'maxResults';
503
+ markTruncated(summary, 'maxResults');
491
504
  break;
492
505
  }
493
506
  try {
494
507
  assertAllowedFileAccess(file.requestedPath, file.resolvedPath);
495
508
  const remaining = opts.maxResults - matches.length;
496
509
  const result = await scanFileResolved(file.resolvedPath, file.requestedPath, matcher, scanOpts, signal, remaining);
497
- if (result.matched)
498
- summary.filesMatched++;
499
- if (result.skippedBinary)
500
- summary.skippedBinary++;
501
- if (result.skippedTooLarge)
502
- summary.skippedTooLarge++;
510
+ applyScanOutcome(summary, result);
503
511
  matches.push(...result.matches);
504
512
  }
505
513
  catch {
@@ -542,12 +550,7 @@ function processScanResult(winner, summary, matches, maxResults) {
542
550
  }
543
551
  if (winner.result) {
544
552
  const res = winner.result;
545
- if (res.matched)
546
- summary.filesMatched++;
547
- if (res.skippedBinary)
548
- summary.skippedBinary++;
549
- if (res.skippedTooLarge)
550
- summary.skippedTooLarge++;
553
+ applyScanOutcome(summary, res);
551
554
  const remaining = maxResults - matches.length;
552
555
  if (remaining > 0 && res.matches.length > 0) {
553
556
  const take = Math.min(remaining, res.matches.length);
@@ -582,11 +585,7 @@ async function waitForWinner(pending) {
582
585
  async function executeParallel(files, pattern, opts, signal, summary) {
583
586
  const pool = getPool();
584
587
  const matches = [];
585
- const scanOpts = {
586
- maxFileSize: opts.maxFileSize,
587
- skipBinary: opts.skipBinary,
588
- contextLines: opts.contextLines,
589
- };
588
+ const scanOpts = buildScanFileOptions(opts);
590
589
  const matcherOpts = {
591
590
  caseSensitive: opts.caseSensitive,
592
591
  wholeWord: opts.wholeWord,
@@ -596,8 +595,7 @@ async function executeParallel(files, pattern, opts, signal, summary) {
596
595
  const iterator = files[Symbol.asyncIterator]();
597
596
  let exhausted = false;
598
597
  const onAbort = () => {
599
- summary.truncated = true;
600
- summary.stoppedReason = 'timeout';
598
+ markTruncated(summary, 'timeout');
601
599
  for (const t of pending)
602
600
  t.cancel();
603
601
  };
@@ -629,12 +627,10 @@ async function executeParallel(files, pattern, opts, signal, summary) {
629
627
  }
630
628
  // Update summary truncation
631
629
  if (signal.aborted) {
632
- summary.truncated = true;
633
- summary.stoppedReason = 'timeout';
630
+ markTruncated(summary, 'timeout');
634
631
  }
635
632
  else if (matches.length >= opts.maxResults) {
636
- summary.truncated = true;
637
- summary.stoppedReason = 'maxResults';
633
+ markTruncated(summary, 'maxResults');
638
634
  }
639
635
  return matches;
640
636
  }
@@ -649,6 +645,80 @@ export async function scanFileInWorker(resolvedPath, requestedPath, matcher, opt
649
645
  skippedTooLarge: res.skippedTooLarge,
650
646
  };
651
647
  }
648
+ async function searchSingleFile(details, opts, pattern, signal) {
649
+ const summary = createScanSummary();
650
+ summary.filesScanned = 1;
651
+ const matcher = buildMatcher(pattern, opts);
652
+ const result = await scanFileResolved(details.resolvedPath, details.requestedPath, matcher, {
653
+ ...buildScanFileOptions(opts),
654
+ }, signal, opts.maxResults);
655
+ if (result.matched)
656
+ summary.filesMatched = 1;
657
+ return buildSearchResult(path.dirname(details.resolvedPath), pattern, opts.filePattern, result.matches, summary);
658
+ }
659
+ async function searchDirectory(details, opts, pattern, signal, onProgress) {
660
+ const root = await validateExistingDirectory(details.resolvedPath, signal);
661
+ const rootDirectories = [root];
662
+ const stream = globEntries({
663
+ cwd: root,
664
+ pattern: opts.filePattern,
665
+ excludePatterns: opts.excludePatterns,
666
+ includeHidden: opts.includeHidden,
667
+ baseNameMatch: opts.baseNameMatch,
668
+ caseSensitiveMatch: opts.caseSensitiveFileMatch,
669
+ followSymbolicLinks: false,
670
+ onlyFiles: true,
671
+ stats: false,
672
+ suppressErrors: true,
673
+ });
674
+ async function* fileGenerator() {
675
+ let scanned = 0;
676
+ for await (const entry of stream) {
677
+ if (signal.aborted)
678
+ break;
679
+ if (scanned >= opts.maxFilesScanned)
680
+ break;
681
+ if (!entry.dirent.isFile())
682
+ continue;
683
+ const normalized = normalizePath(entry.path);
684
+ if (!isPathWithinDirectories(normalized, rootDirectories))
685
+ continue;
686
+ if (isSensitivePath(entry.path, normalized))
687
+ continue;
688
+ scanned++;
689
+ reportSearchProgress(onProgress, scanned, opts.maxFilesScanned);
690
+ yield { resolvedPath: normalized, requestedPath: entry.path };
691
+ }
692
+ reportSearchProgress(onProgress, scanned, opts.maxFilesScanned, true);
693
+ }
694
+ const summary = createScanSummary();
695
+ const resolvedStream = fileGenerator();
696
+ async function* countingStream() {
697
+ for await (const f of resolvedStream) {
698
+ summary.filesScanned++;
699
+ yield f;
700
+ }
701
+ if (summary.filesScanned >= opts.maxFilesScanned) {
702
+ summary.truncated = true;
703
+ summary.stoppedReason = 'maxFiles';
704
+ }
705
+ }
706
+ const matcherOpts = {
707
+ caseSensitive: opts.caseSensitive,
708
+ wholeWord: opts.wholeWord,
709
+ isLiteral: opts.isLiteral,
710
+ };
711
+ validatePattern(pattern, matcherOpts);
712
+ const matches = shouldUseWorkers()
713
+ ? await executeParallel(countingStream(), pattern, opts, signal, summary)
714
+ : await executeSequential(countingStream(), pattern, opts, signal, summary);
715
+ return buildSearchResult(root, pattern, opts.filePattern, matches, summary);
716
+ }
717
+ function buildTimeoutSearchResult(basePath, pattern, filePattern) {
718
+ const timeoutSummary = createScanSummary();
719
+ markTruncated(timeoutSummary, 'timeout');
720
+ return buildSearchResult(basePath, pattern, filePattern, [], timeoutSummary);
721
+ }
652
722
  export async function searchContent(basePath, pattern, options = {}) {
653
723
  if (!basePath.trim())
654
724
  throw new McpError(ErrorCode.E_INVALID_INPUT, 'basePath required');
@@ -659,94 +729,17 @@ export async function searchContent(basePath, pattern, options = {}) {
659
729
  try {
660
730
  const details = await validateExistingPathDetailed(basePath, signal);
661
731
  const stats = await withAbort(fsp.stat(details.resolvedPath), signal);
662
- // Check if simple file scan
663
732
  if (stats.isFile()) {
664
- const summary = createScanSummary();
665
- summary.filesScanned = 1;
666
- // Single file execution
667
- const matcher = buildMatcher(pattern, opts);
668
- const result = await scanFileResolved(details.resolvedPath, details.requestedPath, matcher, {
669
- maxFileSize: opts.maxFileSize,
670
- skipBinary: opts.skipBinary,
671
- contextLines: opts.contextLines,
672
- }, signal, opts.maxResults);
673
- if (result.matched)
674
- summary.filesMatched = 1;
675
- return buildSearchResult(path.dirname(details.resolvedPath), pattern, opts.filePattern, result.matches, summary);
733
+ return await searchSingleFile(details, opts, pattern, signal);
676
734
  }
677
735
  if (!stats.isDirectory()) {
678
- throw new McpError(ErrorCode.E_INVALID_INPUT, `Path must be file or directory`, basePath);
736
+ throw new McpError(ErrorCode.E_INVALID_INPUT, 'Path must be file or directory', basePath);
679
737
  }
680
- const root = await validateExistingDirectory(details.resolvedPath, signal);
681
- const rootDirectories = [root];
682
- // Glob
683
- const stream = globEntries({
684
- cwd: root,
685
- pattern: opts.filePattern,
686
- excludePatterns: opts.excludePatterns,
687
- includeHidden: opts.includeHidden,
688
- baseNameMatch: opts.baseNameMatch,
689
- caseSensitiveMatch: opts.caseSensitiveFileMatch,
690
- followSymbolicLinks: false,
691
- onlyFiles: true,
692
- stats: false,
693
- suppressErrors: true,
694
- });
695
- // Generator adapter to resolve paths
696
- async function* fileGenerator() {
697
- let scanned = 0;
698
- for await (const entry of stream) {
699
- if (signal.aborted)
700
- break;
701
- if (scanned >= opts.maxFilesScanned)
702
- break;
703
- if (!entry.dirent.isFile())
704
- continue;
705
- // Helper to resolve
706
- // We duplicate simple resolution logic to keep it fast
707
- const normalized = normalizePath(entry.path);
708
- if (!isPathWithinDirectories(normalized, rootDirectories))
709
- continue;
710
- if (isSensitivePath(entry.path, normalized))
711
- continue;
712
- scanned++;
713
- reportSearchProgress(options.onProgress, scanned, opts.maxFilesScanned);
714
- yield { resolvedPath: normalized, requestedPath: entry.path };
715
- }
716
- reportSearchProgress(options.onProgress, scanned, opts.maxFilesScanned, true);
717
- }
718
- // Choose Strategy
719
- // We recreate summary to track actual scans
720
- const summary = createScanSummary();
721
- const resolvedStream = fileGenerator();
722
- // Wrap generator to count scanned files in summary
723
- async function* countingStream() {
724
- for await (const f of resolvedStream) {
725
- summary.filesScanned++;
726
- yield f;
727
- }
728
- if (summary.filesScanned >= opts.maxFilesScanned) {
729
- summary.truncated = true;
730
- summary.stoppedReason = 'maxFiles';
731
- }
732
- }
733
- const matcherOpts = {
734
- caseSensitive: opts.caseSensitive,
735
- wholeWord: opts.wholeWord,
736
- isLiteral: opts.isLiteral,
737
- };
738
- validatePattern(pattern, matcherOpts);
739
- const matches = shouldUseWorkers()
740
- ? await executeParallel(countingStream(), pattern, opts, signal, summary)
741
- : await executeSequential(countingStream(), pattern, opts, signal, summary);
742
- return buildSearchResult(root, pattern, opts.filePattern, matches, summary);
738
+ return await searchDirectory(details, opts, pattern, signal, options.onProgress);
743
739
  }
744
740
  catch (error) {
745
741
  if (isTimeoutLikeError(error)) {
746
- const timeoutSummary = createScanSummary();
747
- timeoutSummary.truncated = true;
748
- timeoutSummary.stoppedReason = 'timeout';
749
- return buildSearchResult(basePath, pattern, opts.filePattern, [], timeoutSummary);
742
+ return buildTimeoutSearchResult(basePath, pattern, opts.filePattern);
750
743
  }
751
744
  throw error;
752
745
  }
@@ -3,9 +3,9 @@ import { DEFAULT_SEARCH_MAX_FILES, DEFAULT_SEARCH_TIMEOUT_MS, } from '../constan
3
3
  import { createTimedAbortSignal } from '../fs-helpers.js';
4
4
  import { isSensitivePath } from '../path-policy.js';
5
5
  import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
6
- import { needsStatsForSort, withOptionalStoppedReason } from './common.js';
6
+ import { compareOptionalNumberDesc, compareStringValues, isEntryAccessibleByType, needsStatsForSort, resolveEntryType, resolveStopReason, stableSortByDerivedString, withOptionalStoppedReason, } from './common.js';
7
7
  import { isIgnoredByGitignore, loadRootGitignore } from './gitignore.js';
8
- import { globEntries, resolveEntryType } from './glob-engine.js';
8
+ import { globEntries } from './glob-engine.js';
9
9
  // Internal default for find tool - not exposed to MCP users
10
10
  const INTERNAL_MAX_RESULTS = 1000;
11
11
  function normalizeOptions(options) {
@@ -41,17 +41,17 @@ function buildSearchResult(entry, entryType, needsStats) {
41
41
  ...(modified !== undefined ? { modified } : {}),
42
42
  };
43
43
  }
44
- function markStopped(state, reason) {
45
- state.truncated = true;
46
- state.stoppedReason = reason;
47
- }
48
44
  function shouldStopCollecting(state, normalized, signal) {
49
- if (signal.aborted) {
50
- markStopped(state, 'timeout');
51
- return true;
52
- }
53
- if (state.filesScanned >= normalized.maxFilesScanned) {
54
- markStopped(state, 'maxFiles');
45
+ const stopReason = resolveStopReason({
46
+ signal,
47
+ current: state.filesScanned,
48
+ max: normalized.maxFilesScanned,
49
+ abortedReason: 'timeout',
50
+ maxReason: 'maxFiles',
51
+ });
52
+ if (stopReason !== undefined) {
53
+ state.truncated = true;
54
+ state.stoppedReason = stopReason;
55
55
  return true;
56
56
  }
57
57
  return false;
@@ -99,7 +99,8 @@ function buildCollectResult(state) {
99
99
  function handleEntry(entry, entryType, needsStats, normalized, state) {
100
100
  state.results.push(buildSearchResult(entry, entryType, needsStats));
101
101
  if (state.results.length >= normalized.maxResults) {
102
- markStopped(state, 'maxResults');
102
+ state.truncated = true;
103
+ state.stoppedReason = 'maxResults';
103
104
  }
104
105
  }
105
106
  function reportSearchFilesProgress(onProgress, current, total, force = false) {
@@ -109,7 +110,7 @@ function reportSearchFilesProgress(onProgress, current, total, force = false) {
109
110
  return;
110
111
  onProgress({ current, total });
111
112
  }
112
- async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, onProgress) {
113
+ async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress) {
113
114
  for await (const entry of stream) {
114
115
  if (shouldStopCollecting(state, normalized, signal))
115
116
  break;
@@ -122,7 +123,7 @@ async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher
122
123
  if (!shouldIncludeEntry(entryType, normalized)) {
123
124
  continue;
124
125
  }
125
- const isAccessible = await isEntryAccessible(entry, entryType, rootDirectories, signal);
126
+ const isAccessible = await isEntryAccessibleByType(entry.path, entryType, rootDirectories, signal, accessDeps);
126
127
  if (!isAccessible) {
127
128
  state.skippedInaccessible++;
128
129
  continue;
@@ -138,31 +139,21 @@ function isEntryIgnoredByGitignore(matcher, root, entryPath, relativePath) {
138
139
  return false;
139
140
  return isIgnoredByGitignore(matcher, root, entryPath, relativePath ? { relativePath } : {});
140
141
  }
141
- async function isEntryAccessible(entry, entryType, rootDirectories, signal) {
142
- if (entryType === 'symlink') {
143
- try {
144
- const validated = await validateExistingPathDetailed(entry.path, signal);
145
- return !isSensitivePath(validated.requestedPath, validated.resolvedPath);
146
- }
147
- catch {
148
- return false;
149
- }
150
- }
151
- const resolvedPath = normalizePath(entry.path);
152
- if (!isPathWithinDirectories(resolvedPath, rootDirectories)) {
153
- return false;
154
- }
155
- return !isSensitivePath(entry.path, resolvedPath);
156
- }
157
142
  async function collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress) {
158
143
  const needsStats = needsStatsForSort(normalized.sortBy);
159
144
  const stream = buildSearchStream(root, pattern, excludePatterns, normalized, needsStats);
160
145
  const state = createCollectState();
161
146
  const rootDirectories = [root];
147
+ const accessDeps = {
148
+ normalizePath,
149
+ isPathWithinDirectories,
150
+ isSensitivePath,
151
+ validateSymlinkPath: validateExistingPathDetailed,
152
+ };
162
153
  const gitignoreMatcher = normalized.respectGitignore
163
154
  ? await loadRootGitignore(root, signal)
164
155
  : null;
165
- await collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, onProgress);
156
+ await collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress);
166
157
  return buildCollectResult(state);
167
158
  }
168
159
  function buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible) {
@@ -174,27 +165,17 @@ function buildSearchSummary(results, filesScanned, truncated, stoppedReason, ski
174
165
  };
175
166
  return withOptionalStoppedReason(summary, stoppedReason);
176
167
  }
177
- const collator = new Intl.Collator(undefined, { numeric: true });
178
- function compareString(a, b) {
179
- return collator.compare(a ?? '', b ?? '');
180
- }
181
168
  function compareNameThenPath(a, b) {
182
- const nameCompare = compareString(a.name, b.name);
169
+ const nameCompare = compareStringValues(a.name, b.name);
183
170
  if (nameCompare !== 0)
184
171
  return nameCompare;
185
- return compareString(a.path, b.path);
172
+ return compareStringValues(a.path, b.path);
186
173
  }
187
174
  function comparePathThenName(a, b) {
188
- const pathCompare = compareString(a.path, b.path);
175
+ const pathCompare = compareStringValues(a.path, b.path);
189
176
  if (pathCompare !== 0)
190
177
  return pathCompare;
191
- return compareString(a.name, b.name);
192
- }
193
- function compareOptionalNumberDesc(left, right, tieBreak) {
194
- const diff = (right ?? 0) - (left ?? 0);
195
- if (diff !== 0)
196
- return diff;
197
- return tieBreak();
178
+ return compareStringValues(a.name, b.name);
198
179
  }
199
180
  const SORT_COMPARATORS = {
200
181
  size: (a, b) => compareOptionalNumberDesc(a.size, b.size, () => compareNameThenPath(a, b)),
@@ -204,32 +185,7 @@ const SORT_COMPARATORS = {
204
185
  };
205
186
  export function sortSearchResults(results, sortBy) {
206
187
  if (sortBy === 'name') {
207
- const decorated = [];
208
- for (let index = 0; index < results.length; index += 1) {
209
- const item = results[index];
210
- if (!item)
211
- continue;
212
- decorated.push({
213
- item,
214
- baseName: path.basename(item.path ?? ''),
215
- index,
216
- });
217
- }
218
- decorated.sort((a, b) => {
219
- const baseCompare = compareString(a.baseName, b.baseName);
220
- if (baseCompare !== 0)
221
- return baseCompare;
222
- const pathCompare = compareString(a.item.path, b.item.path);
223
- if (pathCompare !== 0)
224
- return pathCompare;
225
- return a.index - b.index;
226
- });
227
- for (let index = 0; index < decorated.length; index += 1) {
228
- const entry = decorated[index];
229
- if (entry) {
230
- results[index] = entry.item;
231
- }
232
- }
188
+ stableSortByDerivedString(results, (item) => path.basename(item.path ?? ''), (left, right) => comparePathThenName(left, right));
233
189
  return;
234
190
  }
235
191
  const comparator = SORT_COMPARATORS[sortBy];
@@ -1,7 +1,7 @@
1
- type TreeEntryType = 'file' | 'directory' | 'symlink' | 'other';
1
+ import type { EntryType } from './common.js';
2
2
  interface TreeEntry {
3
3
  name: string;
4
- type: TreeEntryType;
4
+ type: EntryType;
5
5
  relativePath: string;
6
6
  children?: TreeEntry[];
7
7
  }
@@ -4,8 +4,9 @@ import { createTimedAbortSignal } from '../fs-helpers.js';
4
4
  import { toPosixPath } from '../path-format.js';
5
5
  import { isSensitivePath } from '../path-policy.js';
6
6
  import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
7
+ import { isEntryAccessibleByType, resolveEntryType, resolveStopReason, } from './common.js';
7
8
  import { isIgnoredByGitignore, loadRootGitignore } from './gitignore.js';
8
- import { globEntries, resolveEntryType } from './glob-engine.js';
9
+ import { globEntries } from './glob-engine.js';
9
10
  function toSafeNonNegativeInt(value, fallback) {
10
11
  if (typeof value !== 'number' || !Number.isFinite(value))
11
12
  return fallback;
@@ -90,36 +91,11 @@ function getTreeTypeRank(type) {
90
91
  return 1;
91
92
  return 2;
92
93
  }
93
- function getStopReason(signal, totalEntries, maxEntries) {
94
- if (signal.aborted) {
95
- return 'aborted';
96
- }
97
- if (totalEntries >= maxEntries) {
98
- return 'maxEntries';
99
- }
100
- return undefined;
101
- }
102
- async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal) {
94
+ async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal, accessDeps) {
103
95
  const type = resolveEntryType(entry.dirent);
104
- if (type !== 'symlink') {
105
- const normalized = normalizePath(entry.path);
106
- if (!isPathWithinDirectories(normalized, rootDirectories)) {
107
- return null;
108
- }
109
- if (isSensitivePath(entry.path, normalized)) {
110
- return null;
111
- }
112
- }
113
- else {
114
- try {
115
- const validated = await validateExistingPathDetailed(entry.path, signal);
116
- if (isSensitivePath(validated.requestedPath, validated.resolvedPath)) {
117
- return null;
118
- }
119
- }
120
- catch {
121
- return null;
122
- }
96
+ const isAccessible = await isEntryAccessibleByType(entry.path, type, rootDirectories, signal, accessDeps);
97
+ if (!isAccessible) {
98
+ return null;
123
99
  }
124
100
  if (gitignoreMatcher &&
125
101
  isIgnoredByGitignore(gitignoreMatcher, root, entry.path, {
@@ -222,6 +198,12 @@ export async function treeDirectory(dirPath, options = {}) {
222
198
  const root = await validateExistingDirectory(dirPath, signal);
223
199
  const rootNormalized = normalizePath(root);
224
200
  const rootDirectories = [rootNormalized];
201
+ const accessDeps = {
202
+ normalizePath,
203
+ isPathWithinDirectories,
204
+ isSensitivePath,
205
+ validateSymlinkPath: validateExistingPathDetailed,
206
+ };
225
207
  try {
226
208
  const excludePatterns = normalized.includeIgnored
227
209
  ? []
@@ -253,12 +235,18 @@ export async function treeDirectory(dirPath, options = {}) {
253
235
  suppressErrors: true,
254
236
  });
255
237
  for await (const entry of stream) {
256
- const stopReason = getStopReason(signal, totalEntries, normalized.maxEntries);
238
+ const stopReason = resolveStopReason({
239
+ signal,
240
+ current: totalEntries,
241
+ max: normalized.maxEntries,
242
+ abortedReason: 'aborted',
243
+ maxReason: 'maxEntries',
244
+ });
257
245
  if (stopReason) {
258
246
  truncated = true;
259
247
  break;
260
248
  }
261
- const resolved = await resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal);
249
+ const resolved = await resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal, accessDeps);
262
250
  if (!resolved) {
263
251
  continue;
264
252
  }
package/dist/prompts.js CHANGED
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
  import { withDefaultIcons } from './tools/shared.js';
3
3
  const HELP_PROMPT_NAME = 'get-help';
4
4
  const HELP_PROMPT_TITLE = 'Get Help';
5
- const HELP_PROMPT_DESCRIPTION = 'Retrieve the full filesystem-mcp XML usage guide.';
5
+ const HELP_PROMPT_DESCRIPTION = 'Return filesystem-mcp usage instructions.';
6
6
  function filterInstructionsByTopic(instructions, topic) {
7
7
  const normalized = topic.trim().toLowerCase();
8
8
  if (!normalized)
@@ -16,7 +16,7 @@ function filterInstructionsByTopic(instructions, topic) {
16
16
  .map((sec) => sec.split('\n')[0]?.replace(/^##\s*/u, '') ?? '')
17
17
  .filter(Boolean)
18
18
  .join(', ');
19
- return `Section '${topic}' not found. Available sections: ${available}\n\n${instructions}`;
19
+ return `Section '${topic}' not found. Available: ${available}\n\n${instructions}`;
20
20
  }
21
21
  export function registerGetHelpPrompt(server, instructions, iconInfo) {
22
22
  const baseConfig = withDefaultIcons({ title: HELP_PROMPT_TITLE, description: HELP_PROMPT_DESCRIPTION }, iconInfo);
@@ -26,7 +26,7 @@ export function registerGetHelpPrompt(server, instructions, iconInfo) {
26
26
  topic: z
27
27
  .string()
28
28
  .optional()
29
- .describe('Section heading prefix to filter (e.g. "error handling strategy"). Omit for full instructions.'),
29
+ .describe('Optional section heading prefix (example: "error handling"). Omit to return full instructions.'),
30
30
  },
31
31
  }, ({ topic }) => {
32
32
  const text = topic
@@ -2,7 +2,7 @@ import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
2
2
  import { buildCoreContextPack, getSharedConstraints, getToolContracts, } from './tool-info.js';
3
3
  import { buildWorkflowGuide } from './workflows.js';
4
4
  const INSTRUCTIONS_HEADER = `<role>
5
- Expert filesystem agent. Operate ONLY within allowed roots. Always discover before acting never guess paths.
5
+ Filesystem agent for local paths only. Operate inside allowed roots. Discover before action. Never guess paths.
6
6
  </role>
7
7
 
8
8
  <tools_overview>
@@ -15,16 +15,16 @@ Expert filesystem agent. Operate ONLY within allowed roots. Always discover befo
15
15
  </tools_overview>
16
16
 
17
17
  <resources>
18
- - \`internal://instructions\`: Full server usage guide.
19
- - \`internal://tool-catalog\`: Tool routing and cross-tool data-flow guide.
20
- - \`internal://workflows\`: Standard operating sequences (explore/search/edit/patch).
21
- - \`internal://tool-info/{name}\`: Per-tool details (nuances/gotchas), e.g. \`internal://tool-info/read\`.
22
- - \`filesystem-mcp://result/{id}\`: Large output cache. Call \`resources/read\` immediately if \`resourceUri\` is returned.
23
- - \`filesystem-mcp://metrics\`: Live per-tool stats.
18
+ - \`internal://instructions\`: Full usage reference.
19
+ - \`internal://tool-catalog\`: Tool routing and data-flow rules.
20
+ - \`internal://workflows\`: Standard execution sequences.
21
+ - \`internal://tool-info/{name}\`: Per-tool nuances and gotchas (example: \`internal://tool-info/read\`).
22
+ - \`filesystem-mcp://result/{id}\`: Cached large output. If \`resourceUri\` is returned, call \`resources/read\` immediately.
23
+ - \`filesystem-mcp://metrics\`: Per-tool runtime metrics.
24
24
  </resources>
25
25
 
26
26
  <task_protocol>
27
- Async execution: provide \`_meta.progressToken\` in \`tools/call\`, poll \`tasks/get\`, call \`tasks/result\`.
27
+ Async execution: pass \`_meta.progressToken\` in \`tools/call\`, poll \`tasks/get\`, then call \`tasks/result\`.
28
28
  Task-capable: \`find\`, \`tree\`, \`read\`, \`read_many\`, \`stat_many\`, \`grep\`, \`mkdir\`, \`write\`, \`mv\`, \`rm\`, \`calculate_hash\`, \`apply_patch\`, \`search_and_replace\`.
29
29
  </task_protocol>
30
30
  `;
@@ -35,19 +35,19 @@ ${getSharedConstraints()
35
35
  </constraints>
36
36
 
37
37
  <error_handling>
38
- - \`E_ACCESS_DENIED\` Call \`roots\`; use allowed path.
39
- - \`E_NOT_FOUND\` Call \`ls\`/\`find\`; verify spelling.
40
- - \`E_TOO_LARGE\` Use range/head or \`read_many\`.
41
- - \`E_TIMEOUT\` Reduce scope or result limits.
38
+ - \`E_ACCESS_DENIED\` => call \`roots\`, then use an allowed path.
39
+ - \`E_NOT_FOUND\` => call \`ls\` or \`find\`, then verify spelling.
40
+ - \`E_TOO_LARGE\` => use \`head\`, line ranges, or \`read_many\`.
41
+ - \`E_TIMEOUT\` => reduce scope or result limits.
42
42
  </error_handling>
43
43
  `;
44
44
  function formatToolSection(tool) {
45
45
  const parts = [`### ${tool.name}\n${tool.description}`];
46
46
  if (tool.nuances && tool.nuances.length > 0) {
47
- parts.push(...tool.nuances.map((n) => ${n}`));
47
+ parts.push(...tool.nuances.map((n) => `- Nuance: ${n}`));
48
48
  }
49
49
  if (tool.gotchas && tool.gotchas.length > 0) {
50
- parts.push(...tool.gotchas.map((g) => `⚠ ${g}`));
50
+ parts.push(...tool.gotchas.map((g) => `- Gotcha: ${g}`));
51
51
  }
52
52
  return parts.join('\n');
53
53
  }