@j0hanz/filesystem-mcp 1.8.0 → 1.9.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 (83) hide show
  1. package/README.md +635 -377
  2. package/dist/cli.js +2 -2
  3. package/dist/completions.js +2 -3
  4. package/dist/index.js +2 -2
  5. package/dist/lib/file-operations/{common.d.ts → core.d.ts} +6 -0
  6. package/dist/lib/file-operations/{common.js → core.js} +45 -0
  7. package/dist/lib/file-operations/metadata.d.ts +73 -0
  8. package/dist/lib/file-operations/metadata.js +889 -0
  9. package/dist/lib/file-operations/{search-content.d.ts → search.d.ts} +35 -2
  10. package/dist/lib/file-operations/{search-content.js → search.js} +413 -15
  11. package/dist/lib/file-operations/{glob-engine.d.ts → traversal.d.ts} +18 -1
  12. package/dist/lib/file-operations/{glob-engine.js → traversal.js} +25 -2
  13. package/dist/lib/fs-helpers.js +1 -2
  14. package/dist/lib/observability.js +1 -1
  15. package/dist/lib/{path-validation.d.ts → paths.d.ts} +3 -0
  16. package/dist/lib/{path-validation.js → paths.js} +105 -1
  17. package/dist/lib/utils.d.ts +15 -0
  18. package/dist/lib/utils.js +33 -0
  19. package/dist/resources/generated-instructions.js +6 -6
  20. package/dist/resources/tool-catalog.js +9 -9
  21. package/dist/resources/tool-info.js +3 -3
  22. package/dist/resources/workflows.js +10 -23
  23. package/dist/schemas.js +15 -15
  24. package/dist/server/bootstrap.d.ts +19 -1
  25. package/dist/server/bootstrap.js +103 -9
  26. package/dist/server/roots-manager.d.ts +2 -2
  27. package/dist/server/roots-manager.js +3 -3
  28. package/dist/tools/apply-patch.js +4 -3
  29. package/dist/tools/calculate-hash.js +4 -4
  30. package/dist/tools/create-directory.js +1 -1
  31. package/dist/tools/delete-file.js +3 -3
  32. package/dist/tools/diff-files.js +2 -2
  33. package/dist/tools/edit-file.js +14 -13
  34. package/dist/tools/list-directory.js +5 -7
  35. package/dist/tools/move-file.js +2 -1
  36. package/dist/tools/read-multiple.js +3 -4
  37. package/dist/tools/read.js +3 -3
  38. package/dist/tools/replace-in-files.js +8 -10
  39. package/dist/tools/roots.js +4 -6
  40. package/dist/tools/search-content.js +7 -9
  41. package/dist/tools/search-files.js +4 -6
  42. package/dist/tools/shared.d.ts +2 -1
  43. package/dist/tools/shared.js +2 -1
  44. package/dist/tools/stat-many.js +4 -3
  45. package/dist/tools/stat.js +4 -3
  46. package/dist/tools/task-support.js +1 -1
  47. package/dist/tools/tree.js +3 -4
  48. package/dist/tools/write-file.js +1 -1
  49. package/package.json +4 -4
  50. package/dist/lib/file-operations/file-info.d.ts +0 -10
  51. package/dist/lib/file-operations/file-info.js +0 -143
  52. package/dist/lib/file-operations/gitignore.d.ts +0 -6
  53. package/dist/lib/file-operations/gitignore.js +0 -45
  54. package/dist/lib/file-operations/glob-helpers.d.ts +0 -18
  55. package/dist/lib/file-operations/glob-helpers.js +0 -23
  56. package/dist/lib/file-operations/list-directory.d.ts +0 -14
  57. package/dist/lib/file-operations/list-directory.js +0 -252
  58. package/dist/lib/file-operations/read-multiple-files.d.ts +0 -25
  59. package/dist/lib/file-operations/read-multiple-files.js +0 -252
  60. package/dist/lib/file-operations/search-files.d.ts +0 -27
  61. package/dist/lib/file-operations/search-files.js +0 -216
  62. package/dist/lib/file-operations/search-matcher.d.ts +0 -10
  63. package/dist/lib/file-operations/search-matcher.js +0 -72
  64. package/dist/lib/file-operations/search-worker.d.ts +0 -2
  65. package/dist/lib/file-operations/search-worker.js +0 -131
  66. package/dist/lib/file-operations/tree.d.ts +0 -28
  67. package/dist/lib/file-operations/tree.js +0 -265
  68. package/dist/lib/option-utils.d.ts +0 -3
  69. package/dist/lib/option-utils.js +0 -15
  70. package/dist/lib/path-format.d.ts +0 -1
  71. package/dist/lib/path-format.js +0 -7
  72. package/dist/lib/path-policy.d.ts +0 -2
  73. package/dist/lib/path-policy.js +0 -100
  74. package/dist/lib/progress-reporting.d.ts +0 -11
  75. package/dist/lib/progress-reporting.js +0 -13
  76. package/dist/lib/type-guards.d.ts +0 -1
  77. package/dist/lib/type-guards.js +0 -3
  78. package/dist/server/capabilities.d.ts +0 -10
  79. package/dist/server/capabilities.js +0 -48
  80. package/dist/server/logging.d.ts +0 -7
  81. package/dist/server/logging.js +0 -41
  82. package/dist/server/types.d.ts +0 -4
  83. package/dist/server/types.js +0 -1
@@ -1,7 +1,15 @@
1
1
  import * as fsp from 'node:fs/promises';
2
2
  import { z } from 'zod';
3
- import type { ContentMatch, SearchContentResult } from '../../config.js';
4
- import type { Matcher, MatcherOptions } from './search-matcher.js';
3
+ import type { ContentMatch, SearchContentResult, SearchFilesResult } from '../../config.js';
4
+ export declare const MatcherOptionsSchema: z.ZodObject<{
5
+ caseSensitive: z.ZodBoolean;
6
+ wholeWord: z.ZodBoolean;
7
+ isLiteral: z.ZodBoolean;
8
+ }, z.core.$strict>;
9
+ export type MatcherOptions = z.infer<typeof MatcherOptionsSchema>;
10
+ export type Matcher = (line: string) => number;
11
+ export declare function validatePattern(pattern: string, options: MatcherOptions): void;
12
+ export declare function buildMatcher(pattern: string, options: MatcherOptions): Matcher;
5
13
  export interface ScanFileOptions {
6
14
  maxFileSize: number;
7
15
  skipBinary: boolean;
@@ -66,4 +74,29 @@ interface WorkerScanResult {
66
74
  }
67
75
  export declare function scanFileInWorker(resolvedPath: string, requestedPath: string, matcher: Matcher, options: ScanFileOptions, maxMatches: number, isCancelled: () => boolean, isBinaryDetector: BinaryDetector): Promise<WorkerScanResult>;
68
76
  export declare function searchContent(basePath: string, pattern: string, options?: SearchContentOptions): Promise<SearchContentResult>;
77
+ type SortBy = 'name' | 'size' | 'modified' | 'path';
78
+ interface SearchFilesOptions {
79
+ maxResults?: number;
80
+ sortBy?: SortBy;
81
+ maxDepth?: number;
82
+ maxFilesScanned?: number;
83
+ timeoutMs?: number;
84
+ baseNameMatch?: boolean;
85
+ skipSymlinks?: boolean;
86
+ includeHidden?: boolean;
87
+ respectGitignore?: boolean;
88
+ signal?: AbortSignal;
89
+ onProgress?: (progress: {
90
+ total?: number;
91
+ current: number;
92
+ }) => void;
93
+ }
94
+ interface Sortable {
95
+ name?: string;
96
+ size?: number;
97
+ modified?: Date;
98
+ path?: string;
99
+ }
100
+ export declare function sortSearchResults(results: Sortable[], sortBy: SortBy): void;
101
+ export declare function searchFiles(basePath: string, pattern: string, excludePatterns?: readonly string[], options?: SearchFilesOptions): Promise<SearchFilesResult>;
69
102
  export {};
@@ -1,21 +1,89 @@
1
1
  import * as fsp from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { fileURLToPath, pathToFileURL } from 'node:url';
4
- import { Worker } from 'node:worker_threads';
4
+ import { parentPort, threadId, Worker, workerData } from 'node:worker_threads';
5
+ import RE2 from 're2';
6
+ import safeRegex from 'safe-regex2';
5
7
  import { z } from 'zod';
6
8
  import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_MAX_FILES, DEFAULT_SEARCH_TIMEOUT_MS, MAX_LINE_CONTENT_LENGTH, MAX_SEARCHABLE_FILE_SIZE, SEARCH_WORKERS, } from '../constants.js';
7
9
  import { ErrorCode, formatUnknownErrorMessage, isTimeoutLikeError, McpError, } from '../errors.js';
8
10
  import { assertNotAborted, isProbablyBinary, withAbort, withTimedAbortSignal, } from '../fs-helpers.js';
9
- import { mergeOptions, omitOptionKeys } from '../option-utils.js';
10
- import { assertAllowedFileAccess, isSensitivePath } from '../path-policy.js';
11
- import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
12
- import { reportPeriodicProgress } from '../progress-reporting.js';
13
- import { withOptionalStoppedReason } from './common.js';
14
- import { globEntries } from './glob-engine.js';
15
- import { buildGlobOptions } from './glob-helpers.js';
16
- import { buildMatcher, validatePattern } from './search-matcher.js';
11
+ import { startPerfMeasure } from '../observability.js';
12
+ import { assertAllowedFileAccess, isPathWithinDirectories, isSensitivePath, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../paths.js';
13
+ import { mergeOptions, omitOptionKeys, reportPeriodicProgress, } from '../utils.js';
14
+ import { compareOptionalNumberDesc, compareStringValues, isEntryAccessibleByType, isIgnoredByGitignore, loadRootGitignore, needsStatsForSort, resolveEntryType, resolveStopReason, stableSortByDerivedString, withOptionalStoppedReason, } from './core.js';
15
+ import { buildGlobOptions, globEntries } from './traversal.js';
16
+ export const MatcherOptionsSchema = z.strictObject({
17
+ caseSensitive: z.boolean(),
18
+ wholeWord: z.boolean(),
19
+ isLiteral: z.boolean(),
20
+ });
21
+ function countRegexLineMatches(regex, line) {
22
+ regex.lastIndex = 0;
23
+ let count = 0;
24
+ while (regex.exec(line) !== null) {
25
+ count++;
26
+ if (regex.lastIndex === 0)
27
+ regex.lastIndex++;
28
+ }
29
+ return count;
30
+ }
31
+ function escapeLiteral(pattern) {
32
+ return pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
33
+ }
34
+ function buildRegexPattern(pattern, options) {
35
+ const escaped = options.isLiteral ? escapeLiteral(pattern) : pattern;
36
+ return options.wholeWord ? `\\b${escaped}\\b` : escaped;
37
+ }
38
+ export function validatePattern(pattern, options) {
39
+ if (options.isLiteral && pattern.length === 0)
40
+ return;
41
+ if (options.isLiteral && !options.wholeWord)
42
+ return;
43
+ const final = buildRegexPattern(pattern, options);
44
+ if (!safeRegex(final)) {
45
+ throw new Error(`Potentially unsafe regular expression (ReDoS risk): ${pattern}`);
46
+ }
47
+ }
48
+ function buildLiteralMatcher(pattern, options) {
49
+ if (!options.caseSensitive) {
50
+ const final = escapeLiteral(pattern);
51
+ const regex = new RegExp(final, 'gi');
52
+ return (line) => countRegexLineMatches(regex, line);
53
+ }
54
+ // Fast path for case-sensitive literal
55
+ const needle = pattern;
56
+ if (needle.length === 0)
57
+ return () => 0;
58
+ return (line) => {
59
+ if (line.length === 0)
60
+ return 0;
61
+ let count = 0;
62
+ let pos = line.indexOf(needle);
63
+ while (pos !== -1) {
64
+ count++;
65
+ pos = line.indexOf(needle, pos + needle.length);
66
+ }
67
+ return count;
68
+ };
69
+ }
70
+ function buildRegexMatcher(final, caseSensitive) {
71
+ const regex = new RE2(final, caseSensitive ? 'g' : 'gi');
72
+ return (line) => countRegexLineMatches(regex, line);
73
+ }
74
+ export function buildMatcher(pattern, options) {
75
+ if (options.isLiteral && pattern.length === 0)
76
+ return () => 0;
77
+ if (options.isLiteral && !options.wholeWord) {
78
+ // fast path for simple literal search
79
+ return buildLiteralMatcher(pattern, options);
80
+ }
81
+ const final = buildRegexPattern(pattern, options);
82
+ validatePattern(pattern, options); // Re-validate to be safe
83
+ return buildRegexMatcher(final, options.caseSensitive);
84
+ }
17
85
  // --- Configuration & Schemas ---
18
- const INTERNAL_MAX_RESULTS = 500;
86
+ const SEARCH_CONTENT_MAX_RESULTS = 500;
19
87
  const SearchOptionsSchema = z.strictObject({
20
88
  filePattern: z.string().min(1),
21
89
  excludePatterns: z.array(z.string()),
@@ -36,7 +104,7 @@ const DEFAULTS = {
36
104
  filePattern: '**/*',
37
105
  excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
38
106
  caseSensitive: false,
39
- maxResults: INTERNAL_MAX_RESULTS,
107
+ maxResults: SEARCH_CONTENT_MAX_RESULTS,
40
108
  maxFileSize: MAX_SEARCHABLE_FILE_SIZE,
41
109
  maxFilesScanned: DEFAULT_SEARCH_MAX_FILES,
42
110
  timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS,
@@ -252,7 +320,7 @@ function createScanSummary() {
252
320
  stoppedReason: undefined,
253
321
  };
254
322
  }
255
- function buildSearchResult(root, pattern, filePattern, matches, summary) {
323
+ function buildSearchContentResult(root, pattern, filePattern, matches, summary) {
256
324
  const baseSummary = {
257
325
  filesScanned: summary.filesScanned,
258
326
  filesMatched: summary.filesMatched,
@@ -578,7 +646,7 @@ async function searchSingleFile(details, opts, pattern, signal) {
578
646
  }, signal, opts.maxResults);
579
647
  if (result.matched)
580
648
  summary.filesMatched = 1;
581
- return buildSearchResult(path.dirname(details.resolvedPath), pattern, opts.filePattern, result.matches, summary);
649
+ return buildSearchContentResult(path.dirname(details.resolvedPath), pattern, opts.filePattern, result.matches, summary);
582
650
  }
583
651
  async function searchDirectory(details, opts, pattern, signal, onProgress) {
584
652
  const root = await validateExistingDirectory(details.resolvedPath, signal);
@@ -643,12 +711,12 @@ async function searchDirectory(details, opts, pattern, signal, onProgress) {
643
711
  const matches = shouldUseWorkers()
644
712
  ? await executeParallel(countingStream(), pattern, opts, signal, summary)
645
713
  : await executeSequential(countingStream(), pattern, opts, signal, summary);
646
- return buildSearchResult(root, pattern, opts.filePattern, matches, summary);
714
+ return buildSearchContentResult(root, pattern, opts.filePattern, matches, summary);
647
715
  }
648
716
  function buildTimeoutSearchResult(basePath, pattern, filePattern) {
649
717
  const timeoutSummary = createScanSummary();
650
718
  markTruncated(timeoutSummary, 'timeout');
651
- return buildSearchResult(basePath, pattern, filePattern, [], timeoutSummary);
719
+ return buildSearchContentResult(basePath, pattern, filePattern, [], timeoutSummary);
652
720
  }
653
721
  export async function searchContent(basePath, pattern, options = {}) {
654
722
  if (!basePath.trim())
@@ -676,3 +744,333 @@ export async function searchContent(basePath, pattern, options = {}) {
676
744
  throw error;
677
745
  }
678
746
  }
747
+ // Internal default for find tool - not exposed to MCP users
748
+ const SEARCH_FILES_MAX_RESULTS = 1000;
749
+ function normalizeSearchFilesOptions(options) {
750
+ const normalized = {
751
+ maxResults: options.maxResults ?? SEARCH_FILES_MAX_RESULTS,
752
+ sortBy: options.sortBy ?? 'path',
753
+ maxFilesScanned: options.maxFilesScanned ?? DEFAULT_SEARCH_MAX_FILES,
754
+ timeoutMs: options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
755
+ baseNameMatch: options.baseNameMatch ?? false,
756
+ skipSymlinks: options.skipSymlinks ?? true,
757
+ includeHidden: options.includeHidden ?? false,
758
+ respectGitignore: options.respectGitignore ?? false,
759
+ };
760
+ if (options.maxDepth !== undefined) {
761
+ normalized.maxDepth = options.maxDepth;
762
+ }
763
+ return normalized;
764
+ }
765
+ function buildSearchFilesResult(entry, entryType, needsStats) {
766
+ let resolvedType = 'other';
767
+ if (entryType === 'directory') {
768
+ resolvedType = 'directory';
769
+ }
770
+ else if (entryType === 'file') {
771
+ resolvedType = 'file';
772
+ }
773
+ const size = needsStats && entry.stats?.isFile() ? entry.stats.size : undefined;
774
+ const modified = needsStats ? entry.stats?.mtime : undefined;
775
+ return {
776
+ path: entry.path,
777
+ type: resolvedType,
778
+ ...(size !== undefined ? { size } : {}),
779
+ ...(modified !== undefined ? { modified } : {}),
780
+ };
781
+ }
782
+ function shouldStopCollecting(state, normalized, signal) {
783
+ const stopReason = resolveStopReason({
784
+ signal,
785
+ current: state.filesScanned,
786
+ max: normalized.maxFilesScanned,
787
+ abortedReason: 'timeout',
788
+ maxReason: 'maxFiles',
789
+ });
790
+ if (stopReason !== undefined) {
791
+ state.truncated = true;
792
+ state.stoppedReason = stopReason;
793
+ return true;
794
+ }
795
+ return false;
796
+ }
797
+ function shouldIncludeEntry(entryType, normalized) {
798
+ return !normalized.skipSymlinks || entryType !== 'symlink';
799
+ }
800
+ function createCollectState() {
801
+ return {
802
+ results: [],
803
+ filesScanned: 0,
804
+ truncated: false,
805
+ skippedInaccessible: 0,
806
+ };
807
+ }
808
+ function buildSearchStream(root, pattern, excludePatterns, normalized, needsStats) {
809
+ const options = buildGlobOptions({
810
+ cwd: root,
811
+ pattern,
812
+ excludePatterns,
813
+ includeHidden: normalized.includeHidden,
814
+ baseNameMatch: normalized.baseNameMatch,
815
+ caseSensitiveMatch: true,
816
+ followSymbolicLinks: false,
817
+ onlyFiles: true,
818
+ stats: needsStats,
819
+ ...(normalized.maxDepth !== undefined
820
+ ? { maxDepth: normalized.maxDepth }
821
+ : {}),
822
+ });
823
+ return globEntries(options);
824
+ }
825
+ function buildCollectResult(state) {
826
+ const outcome = {
827
+ results: state.results,
828
+ filesScanned: state.filesScanned,
829
+ truncated: state.truncated,
830
+ skippedInaccessible: state.skippedInaccessible,
831
+ };
832
+ if (state.stoppedReason !== undefined) {
833
+ outcome.stoppedReason = state.stoppedReason;
834
+ }
835
+ return outcome;
836
+ }
837
+ function handleEntry(entry, entryType, needsStats, normalized, state) {
838
+ state.results.push(buildSearchFilesResult(entry, entryType, needsStats));
839
+ if (state.results.length >= normalized.maxResults) {
840
+ state.truncated = true;
841
+ state.stoppedReason = 'maxResults';
842
+ }
843
+ }
844
+ async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress) {
845
+ for await (const entry of stream) {
846
+ if (shouldStopCollecting(state, normalized, signal))
847
+ break;
848
+ state.filesScanned++;
849
+ reportPeriodicProgress(onProgress, state.filesScanned, {
850
+ total: normalized.maxFilesScanned,
851
+ throttleModulo: 25,
852
+ });
853
+ if (isEntryIgnoredByGitignore(gitignoreMatcher, root, entry.path, entry.relativePath)) {
854
+ continue;
855
+ }
856
+ const entryType = resolveEntryType(entry.dirent);
857
+ if (!shouldIncludeEntry(entryType, normalized)) {
858
+ continue;
859
+ }
860
+ const isAccessible = await isEntryAccessibleByType(entry.path, entryType, rootDirectories, signal, accessDeps);
861
+ if (!isAccessible) {
862
+ state.skippedInaccessible++;
863
+ continue;
864
+ }
865
+ handleEntry(entry, entryType, needsStats, normalized, state);
866
+ if (state.truncated)
867
+ break;
868
+ }
869
+ reportPeriodicProgress(onProgress, state.filesScanned, {
870
+ total: normalized.maxFilesScanned,
871
+ throttleModulo: 25,
872
+ force: true,
873
+ });
874
+ }
875
+ function isEntryIgnoredByGitignore(matcher, root, entryPath, relativePath) {
876
+ if (!matcher)
877
+ return false;
878
+ return isIgnoredByGitignore(matcher, root, entryPath, relativePath ? { relativePath } : {});
879
+ }
880
+ async function collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress) {
881
+ const needsStats = needsStatsForSort(normalized.sortBy);
882
+ const stream = buildSearchStream(root, pattern, excludePatterns, normalized, needsStats);
883
+ const state = createCollectState();
884
+ const rootDirectories = [root];
885
+ const accessDeps = {
886
+ normalizePath,
887
+ isPathWithinDirectories,
888
+ isSensitivePath,
889
+ validateSymlinkPath: validateExistingPathDetailed,
890
+ };
891
+ const gitignoreMatcher = normalized.respectGitignore
892
+ ? await loadRootGitignore(root, signal)
893
+ : null;
894
+ await collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress);
895
+ return buildCollectResult(state);
896
+ }
897
+ function buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible) {
898
+ const summary = {
899
+ matched: results.length,
900
+ truncated,
901
+ skippedInaccessible,
902
+ filesScanned,
903
+ };
904
+ return withOptionalStoppedReason(summary, stoppedReason);
905
+ }
906
+ function compareNameThenPath(a, b) {
907
+ const nameCompare = compareStringValues(a.name, b.name);
908
+ if (nameCompare !== 0)
909
+ return nameCompare;
910
+ return compareStringValues(a.path, b.path);
911
+ }
912
+ function comparePathThenName(a, b) {
913
+ const pathCompare = compareStringValues(a.path, b.path);
914
+ if (pathCompare !== 0)
915
+ return pathCompare;
916
+ return compareStringValues(a.name, b.name);
917
+ }
918
+ const SORT_COMPARATORS = {
919
+ size: (a, b) => compareOptionalNumberDesc(a.size, b.size, () => compareNameThenPath(a, b)),
920
+ modified: (a, b) => compareOptionalNumberDesc(a.modified?.getTime(), b.modified?.getTime(), () => compareNameThenPath(a, b)),
921
+ path: (a, b) => comparePathThenName(a, b),
922
+ name: (a, b) => compareNameThenPath(a, b),
923
+ };
924
+ export function sortSearchResults(results, sortBy) {
925
+ if (sortBy === 'name') {
926
+ stableSortByDerivedString(results, (item) => path.basename(item.path ?? ''), (left, right) => comparePathThenName(left, right));
927
+ return;
928
+ }
929
+ const comparator = SORT_COMPARATORS[sortBy];
930
+ results.sort(comparator);
931
+ }
932
+ async function runSearchFiles(root, pattern, excludePatterns, normalized, signal, onProgress) {
933
+ const { results, filesScanned, truncated, stoppedReason, skippedInaccessible, } = await collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress);
934
+ sortSearchResults(results, normalized.sortBy);
935
+ return {
936
+ results,
937
+ summary: buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible),
938
+ };
939
+ }
940
+ export async function searchFiles(basePath, pattern, excludePatterns = [], options = {}) {
941
+ const normalized = normalizeSearchFilesOptions(options);
942
+ return withTimedAbortSignal(options.signal, normalized.timeoutMs, async (signal) => {
943
+ const root = await validateExistingDirectory(basePath, signal);
944
+ const { results, summary } = await runSearchFiles(root, pattern, excludePatterns, normalized, signal, options.onProgress);
945
+ return {
946
+ basePath: root,
947
+ pattern,
948
+ results,
949
+ summary,
950
+ };
951
+ });
952
+ }
953
+ const matcherCache = new Map();
954
+ const MAX_MATCHER_CACHE_SIZE = 100;
955
+ function getMatcherCacheKey(pattern, options) {
956
+ const cs = options.caseSensitive ? '1' : '0';
957
+ const ww = options.wholeWord ? '1' : '0';
958
+ const lit = options.isLiteral ? '1' : '0';
959
+ return `${pattern}|${cs}|${ww}|${lit}`;
960
+ }
961
+ function getCachedMatcher(pattern, options) {
962
+ const key = getMatcherCacheKey(pattern, options);
963
+ const cached = matcherCache.get(key);
964
+ if (cached) {
965
+ refreshMatcherCacheEntry(key, cached);
966
+ return cached;
967
+ }
968
+ const matcher = buildMatcher(pattern, options);
969
+ refreshMatcherCacheEntry(key, matcher);
970
+ evictOldestMatcherIfNeeded();
971
+ return matcher;
972
+ }
973
+ function refreshMatcherCacheEntry(key, matcher) {
974
+ matcherCache.delete(key);
975
+ matcherCache.set(key, matcher);
976
+ }
977
+ function evictOldestMatcherIfNeeded() {
978
+ if (matcherCache.size <= MAX_MATCHER_CACHE_SIZE)
979
+ return;
980
+ const firstKey = matcherCache.keys().next().value;
981
+ if (firstKey !== undefined) {
982
+ matcherCache.delete(firstKey);
983
+ }
984
+ }
985
+ const cancelledRequests = new Set();
986
+ const activeRequests = new Set();
987
+ let shuttingDown = false;
988
+ function maybeFinishShutdown() {
989
+ if (!shuttingDown)
990
+ return;
991
+ if (activeRequests.size > 0)
992
+ return;
993
+ parentPort?.close();
994
+ }
995
+ function consumeCancelled(id) {
996
+ if (!cancelledRequests.has(id)) {
997
+ return false;
998
+ }
999
+ cancelledRequests.delete(id);
1000
+ return true;
1001
+ }
1002
+ function markCancelledIfActive(id) {
1003
+ if (activeRequests.has(id)) {
1004
+ cancelledRequests.add(id);
1005
+ }
1006
+ }
1007
+ function buildScanResponse(id, result) {
1008
+ return {
1009
+ type: 'result',
1010
+ id,
1011
+ result,
1012
+ };
1013
+ }
1014
+ function buildErrorResponse(id, error) {
1015
+ return {
1016
+ type: 'error',
1017
+ id,
1018
+ error: formatUnknownErrorMessage(error),
1019
+ };
1020
+ }
1021
+ async function handleScanRequest(request) {
1022
+ const { id, resolvedPath, requestedPath, pattern, matcherOptions, scanOptions, maxMatches, } = request;
1023
+ if (consumeCancelled(id))
1024
+ return;
1025
+ activeRequests.add(id);
1026
+ const endMeasure = startPerfMeasure('searchWorker.scan', {
1027
+ maxMatches,
1028
+ });
1029
+ let ok = false;
1030
+ try {
1031
+ const matcher = getCachedMatcher(pattern, matcherOptions);
1032
+ const isCancelled = () => cancelledRequests.has(id);
1033
+ const result = await scanFileInWorker(resolvedPath, requestedPath, matcher, scanOptions, maxMatches, isCancelled, isProbablyBinary);
1034
+ if (consumeCancelled(id))
1035
+ return;
1036
+ parentPort?.postMessage(buildScanResponse(id, result));
1037
+ ok = true;
1038
+ }
1039
+ catch (err) {
1040
+ if (consumeCancelled(id))
1041
+ return;
1042
+ parentPort?.postMessage(buildErrorResponse(id, err));
1043
+ }
1044
+ finally {
1045
+ activeRequests.delete(id);
1046
+ cancelledRequests.delete(id);
1047
+ endMeasure?.(ok);
1048
+ maybeFinishShutdown();
1049
+ }
1050
+ }
1051
+ function handleMessage(message) {
1052
+ switch (message.type) {
1053
+ case 'scan':
1054
+ if (shuttingDown)
1055
+ return;
1056
+ void handleScanRequest(message);
1057
+ break;
1058
+ case 'cancel':
1059
+ markCancelledIfActive(message.id);
1060
+ break;
1061
+ case 'shutdown':
1062
+ shuttingDown = true;
1063
+ for (const id of activeRequests) {
1064
+ markCancelledIfActive(id);
1065
+ }
1066
+ maybeFinishShutdown();
1067
+ break;
1068
+ }
1069
+ }
1070
+ if (parentPort) {
1071
+ parentPort.on('message', handleMessage);
1072
+ const data = workerData;
1073
+ if (data?.debug) {
1074
+ console.error(`[SearchWorker] Started with threadId=${String(threadId)}`);
1075
+ }
1076
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Stats } from 'node:fs';
2
- import type { DirentLike } from './common.js';
2
+ import type { DirentLike } from './core.js';
3
3
  interface GlobEntry {
4
4
  path: string;
5
5
  relativePath?: string;
@@ -20,4 +20,21 @@ export interface GlobEntriesOptions {
20
20
  suppressErrors?: boolean;
21
21
  }
22
22
  export declare function globEntries(options: GlobEntriesOptions): AsyncGenerator<GlobEntry>;
23
+ export interface GlobConfig {
24
+ cwd: string;
25
+ pattern: string;
26
+ excludePatterns?: readonly string[];
27
+ includeHidden?: boolean;
28
+ baseNameMatch?: boolean;
29
+ caseSensitiveMatch?: boolean;
30
+ followSymbolicLinks?: boolean;
31
+ onlyFiles?: boolean;
32
+ stats?: boolean;
33
+ maxDepth?: number;
34
+ suppressErrors?: boolean;
35
+ }
36
+ /**
37
+ * Builds standard options for globEntries to ensure consistency across search tools.
38
+ */
39
+ export declare function buildGlobOptions(config: GlobConfig): Parameters<typeof globEntries>[0];
23
40
  export {};
@@ -2,8 +2,8 @@ import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { glob as fsGlob } from 'node:fs/promises';
4
4
  import { getToolContextSnapshot, publishOpsTraceEnd, publishOpsTraceError, publishOpsTraceStart, shouldPublishOpsTrace, startPerfMeasure, } from '../observability.js';
5
- import { toPosixPath } from '../path-format.js';
6
- import { isRecord } from '../type-guards.js';
5
+ import { toPosixPath } from '../paths.js';
6
+ import { isRecord } from '../utils.js';
7
7
  const GLOB_MAGIC_RE = /[*?[\]{}!]/u;
8
8
  const DEFAULT_MAX_HIDDEN_DEPTH = 10;
9
9
  const GLOB_BATCH_CONCURRENCY = 64;
@@ -344,3 +344,26 @@ export async function* globEntries(options) {
344
344
  endMeasure?.(ok);
345
345
  }
346
346
  }
347
+ /**
348
+ * Builds standard options for globEntries to ensure consistency across search tools.
349
+ */
350
+ export function buildGlobOptions(config) {
351
+ const options = {
352
+ cwd: config.cwd,
353
+ pattern: config.pattern,
354
+ excludePatterns: config.excludePatterns ?? [],
355
+ includeHidden: config.includeHidden ?? false,
356
+ baseNameMatch: config.baseNameMatch ?? false,
357
+ caseSensitiveMatch: config.caseSensitiveMatch ?? true,
358
+ followSymbolicLinks: config.followSymbolicLinks ?? false,
359
+ onlyFiles: config.onlyFiles ?? true,
360
+ stats: config.stats ?? false,
361
+ };
362
+ if (config.suppressErrors) {
363
+ options.suppressErrors = config.suppressErrors;
364
+ }
365
+ if (config.maxDepth !== undefined) {
366
+ options.maxDepth = config.maxDepth;
367
+ }
368
+ return options;
369
+ }
@@ -6,8 +6,7 @@ import { Writable } from 'node:stream';
6
6
  import { pipeline } from 'node:stream/promises';
7
7
  import { BINARY_CHECK_BUFFER_SIZE, KNOWN_BINARY_EXTENSIONS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from './constants.js';
8
8
  import { ErrorCode, McpError, normalizeUnknownError } from './errors.js';
9
- import { assertAllowedFileAccess } from './path-policy.js';
10
- import { validateExistingPath } from './path-validation.js';
9
+ import { assertAllowedFileAccess, validateExistingPath } from './paths.js';
11
10
  function createAbortError(message = 'Operation aborted') {
12
11
  return new DOMException(message, 'AbortError');
13
12
  }
@@ -3,7 +3,7 @@ import { hash } from 'node:crypto';
3
3
  import { channel, tracingChannel } from 'node:diagnostics_channel';
4
4
  import { monitorEventLoopDelay, performance, PerformanceObserver, } from 'node:perf_hooks';
5
5
  import { parseTrueEnvFlag } from './constants.js';
6
- import { isRecord } from './type-guards.js';
6
+ import { isRecord } from './utils.js';
7
7
  // --- Configuration ---
8
8
  const ENV = process.env;
9
9
  let _cachedConfig;
@@ -1,5 +1,8 @@
1
1
  import type { Root } from '@modelcontextprotocol/sdk/types.js';
2
2
  import { McpError } from './errors.js';
3
+ export declare function toPosixPath(value: string): string;
4
+ export declare function isSensitivePath(requestedPath: string, resolvedPath?: string): boolean;
5
+ export declare function assertAllowedFileAccess(requestedPath: string, resolvedPath?: string): void;
3
6
  /**
4
7
  * Normalizes any path-like input to an absolute path suitable for comparisons.
5
8
  * - Expands "~" home directory shorthand.