@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.
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.js +1 -1
- package/dist/lib/file-operations/common.d.ts +42 -0
- package/dist/lib/file-operations/common.js +87 -0
- package/dist/lib/file-operations/file-info.js +13 -19
- package/dist/lib/file-operations/glob-engine.d.ts +1 -6
- package/dist/lib/file-operations/glob-engine.js +0 -9
- package/dist/lib/file-operations/list-directory.js +19 -39
- package/dist/lib/file-operations/read-multiple-files.js +11 -19
- package/dist/lib/file-operations/search-content.js +106 -113
- package/dist/lib/file-operations/search-files.js +28 -72
- package/dist/lib/file-operations/tree.d.ts +2 -2
- package/dist/lib/file-operations/tree.js +20 -32
- package/dist/prompts.js +3 -3
- package/dist/resources/generated-instructions.js +14 -14
- package/dist/resources/tool-catalog.js +9 -9
- package/dist/resources/tool-info.js +5 -5
- package/dist/resources/workflows.js +17 -17
- package/dist/schemas.js +21 -90
- package/dist/server/bootstrap.js +6 -8
- package/dist/tools/list-directory.js +3 -21
- package/dist/tools/read-multiple.js +21 -19
- package/dist/tools/search-files.js +3 -21
- package/dist/tools/shared.d.ts +3 -0
- package/dist/tools/shared.js +27 -0
- package/dist/tools/stat-many.js +20 -20
- package/dist/tools/task-support.js +7 -5
- package/package.json +1 -1
|
@@ -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
|
|
485
|
-
summary.stoppedReason = 'timeout';
|
|
499
|
+
markTruncated(summary, 'timeout');
|
|
486
500
|
break;
|
|
487
501
|
}
|
|
488
502
|
if (matches.length >= opts.maxResults) {
|
|
489
|
-
summary
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
633
|
-
summary.stoppedReason = 'timeout';
|
|
630
|
+
markTruncated(summary, 'timeout');
|
|
634
631
|
}
|
|
635
632
|
else if (matches.length >= opts.maxResults) {
|
|
636
|
-
summary
|
|
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
|
-
|
|
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,
|
|
736
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Path must be file or directory', basePath);
|
|
679
737
|
}
|
|
680
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
169
|
+
const nameCompare = compareStringValues(a.name, b.name);
|
|
183
170
|
if (nameCompare !== 0)
|
|
184
171
|
return nameCompare;
|
|
185
|
-
return
|
|
172
|
+
return compareStringValues(a.path, b.path);
|
|
186
173
|
}
|
|
187
174
|
function comparePathThenName(a, b) {
|
|
188
|
-
const pathCompare =
|
|
175
|
+
const pathCompare = compareStringValues(a.path, b.path);
|
|
189
176
|
if (pathCompare !== 0)
|
|
190
177
|
return pathCompare;
|
|
191
|
-
return
|
|
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
|
-
|
|
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];
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 =
|
|
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 = '
|
|
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
|
|
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('
|
|
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
|
-
|
|
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
|
|
19
|
-
- \`internal://tool-catalog\`: Tool routing and
|
|
20
|
-
- \`internal://workflows\`: Standard
|
|
21
|
-
- \`internal://tool-info/{name}\`: Per-tool
|
|
22
|
-
- \`filesystem-mcp://result/{id}\`:
|
|
23
|
-
- \`filesystem-mcp://metrics\`:
|
|
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:
|
|
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\`
|
|
39
|
-
- \`E_NOT_FOUND\`
|
|
40
|
-
- \`E_TOO_LARGE\`
|
|
41
|
-
- \`E_TIMEOUT\`
|
|
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) =>
|
|
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) =>
|
|
50
|
+
parts.push(...tool.gotchas.map((g) => `- Gotcha: ${g}`));
|
|
51
51
|
}
|
|
52
52
|
return parts.join('\n');
|
|
53
53
|
}
|