@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,216 +0,0 @@
1
- import * as path from 'node:path';
2
- import { DEFAULT_SEARCH_MAX_FILES, DEFAULT_SEARCH_TIMEOUT_MS, } from '../constants.js';
3
- import { withTimedAbortSignal } from '../fs-helpers.js';
4
- import { isSensitivePath } from '../path-policy.js';
5
- import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
6
- import { reportPeriodicProgress } from '../progress-reporting.js';
7
- import { compareOptionalNumberDesc, compareStringValues, isEntryAccessibleByType, needsStatsForSort, resolveEntryType, resolveStopReason, stableSortByDerivedString, withOptionalStoppedReason, } from './common.js';
8
- import { isIgnoredByGitignore, loadRootGitignore } from './gitignore.js';
9
- import { globEntries } from './glob-engine.js';
10
- import { buildGlobOptions } from './glob-helpers.js';
11
- // Internal default for find tool - not exposed to MCP users
12
- const INTERNAL_MAX_RESULTS = 1000;
13
- function normalizeOptions(options) {
14
- const normalized = {
15
- maxResults: options.maxResults ?? INTERNAL_MAX_RESULTS,
16
- sortBy: options.sortBy ?? 'path',
17
- maxFilesScanned: options.maxFilesScanned ?? DEFAULT_SEARCH_MAX_FILES,
18
- timeoutMs: options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
19
- baseNameMatch: options.baseNameMatch ?? false,
20
- skipSymlinks: options.skipSymlinks ?? true,
21
- includeHidden: options.includeHidden ?? false,
22
- respectGitignore: options.respectGitignore ?? false,
23
- };
24
- if (options.maxDepth !== undefined) {
25
- normalized.maxDepth = options.maxDepth;
26
- }
27
- return normalized;
28
- }
29
- function buildSearchResult(entry, entryType, needsStats) {
30
- let resolvedType = 'other';
31
- if (entryType === 'directory') {
32
- resolvedType = 'directory';
33
- }
34
- else if (entryType === 'file') {
35
- resolvedType = 'file';
36
- }
37
- const size = needsStats && entry.stats?.isFile() ? entry.stats.size : undefined;
38
- const modified = needsStats ? entry.stats?.mtime : undefined;
39
- return {
40
- path: entry.path,
41
- type: resolvedType,
42
- ...(size !== undefined ? { size } : {}),
43
- ...(modified !== undefined ? { modified } : {}),
44
- };
45
- }
46
- function shouldStopCollecting(state, normalized, signal) {
47
- const stopReason = resolveStopReason({
48
- signal,
49
- current: state.filesScanned,
50
- max: normalized.maxFilesScanned,
51
- abortedReason: 'timeout',
52
- maxReason: 'maxFiles',
53
- });
54
- if (stopReason !== undefined) {
55
- state.truncated = true;
56
- state.stoppedReason = stopReason;
57
- return true;
58
- }
59
- return false;
60
- }
61
- function shouldIncludeEntry(entryType, normalized) {
62
- return !normalized.skipSymlinks || entryType !== 'symlink';
63
- }
64
- function createCollectState() {
65
- return {
66
- results: [],
67
- filesScanned: 0,
68
- truncated: false,
69
- skippedInaccessible: 0,
70
- };
71
- }
72
- function buildSearchStream(root, pattern, excludePatterns, normalized, needsStats) {
73
- const options = buildGlobOptions({
74
- cwd: root,
75
- pattern,
76
- excludePatterns,
77
- includeHidden: normalized.includeHidden,
78
- baseNameMatch: normalized.baseNameMatch,
79
- caseSensitiveMatch: true,
80
- followSymbolicLinks: false,
81
- onlyFiles: true,
82
- stats: needsStats,
83
- ...(normalized.maxDepth !== undefined
84
- ? { maxDepth: normalized.maxDepth }
85
- : {}),
86
- });
87
- return globEntries(options);
88
- }
89
- function buildCollectResult(state) {
90
- const outcome = {
91
- results: state.results,
92
- filesScanned: state.filesScanned,
93
- truncated: state.truncated,
94
- skippedInaccessible: state.skippedInaccessible,
95
- };
96
- if (state.stoppedReason !== undefined) {
97
- outcome.stoppedReason = state.stoppedReason;
98
- }
99
- return outcome;
100
- }
101
- function handleEntry(entry, entryType, needsStats, normalized, state) {
102
- state.results.push(buildSearchResult(entry, entryType, needsStats));
103
- if (state.results.length >= normalized.maxResults) {
104
- state.truncated = true;
105
- state.stoppedReason = 'maxResults';
106
- }
107
- }
108
- async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress) {
109
- for await (const entry of stream) {
110
- if (shouldStopCollecting(state, normalized, signal))
111
- break;
112
- state.filesScanned++;
113
- reportPeriodicProgress(onProgress, state.filesScanned, {
114
- total: normalized.maxFilesScanned,
115
- throttleModulo: 25,
116
- });
117
- if (isEntryIgnoredByGitignore(gitignoreMatcher, root, entry.path, entry.relativePath)) {
118
- continue;
119
- }
120
- const entryType = resolveEntryType(entry.dirent);
121
- if (!shouldIncludeEntry(entryType, normalized)) {
122
- continue;
123
- }
124
- const isAccessible = await isEntryAccessibleByType(entry.path, entryType, rootDirectories, signal, accessDeps);
125
- if (!isAccessible) {
126
- state.skippedInaccessible++;
127
- continue;
128
- }
129
- handleEntry(entry, entryType, needsStats, normalized, state);
130
- if (state.truncated)
131
- break;
132
- }
133
- reportPeriodicProgress(onProgress, state.filesScanned, {
134
- total: normalized.maxFilesScanned,
135
- throttleModulo: 25,
136
- force: true,
137
- });
138
- }
139
- function isEntryIgnoredByGitignore(matcher, root, entryPath, relativePath) {
140
- if (!matcher)
141
- return false;
142
- return isIgnoredByGitignore(matcher, root, entryPath, relativePath ? { relativePath } : {});
143
- }
144
- async function collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress) {
145
- const needsStats = needsStatsForSort(normalized.sortBy);
146
- const stream = buildSearchStream(root, pattern, excludePatterns, normalized, needsStats);
147
- const state = createCollectState();
148
- const rootDirectories = [root];
149
- const accessDeps = {
150
- normalizePath,
151
- isPathWithinDirectories,
152
- isSensitivePath,
153
- validateSymlinkPath: validateExistingPathDetailed,
154
- };
155
- const gitignoreMatcher = normalized.respectGitignore
156
- ? await loadRootGitignore(root, signal)
157
- : null;
158
- await collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress);
159
- return buildCollectResult(state);
160
- }
161
- function buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible) {
162
- const summary = {
163
- matched: results.length,
164
- truncated,
165
- skippedInaccessible,
166
- filesScanned,
167
- };
168
- return withOptionalStoppedReason(summary, stoppedReason);
169
- }
170
- function compareNameThenPath(a, b) {
171
- const nameCompare = compareStringValues(a.name, b.name);
172
- if (nameCompare !== 0)
173
- return nameCompare;
174
- return compareStringValues(a.path, b.path);
175
- }
176
- function comparePathThenName(a, b) {
177
- const pathCompare = compareStringValues(a.path, b.path);
178
- if (pathCompare !== 0)
179
- return pathCompare;
180
- return compareStringValues(a.name, b.name);
181
- }
182
- const SORT_COMPARATORS = {
183
- size: (a, b) => compareOptionalNumberDesc(a.size, b.size, () => compareNameThenPath(a, b)),
184
- modified: (a, b) => compareOptionalNumberDesc(a.modified?.getTime(), b.modified?.getTime(), () => compareNameThenPath(a, b)),
185
- path: (a, b) => comparePathThenName(a, b),
186
- name: (a, b) => compareNameThenPath(a, b),
187
- };
188
- export function sortSearchResults(results, sortBy) {
189
- if (sortBy === 'name') {
190
- stableSortByDerivedString(results, (item) => path.basename(item.path ?? ''), (left, right) => comparePathThenName(left, right));
191
- return;
192
- }
193
- const comparator = SORT_COMPARATORS[sortBy];
194
- results.sort(comparator);
195
- }
196
- async function runSearchFiles(root, pattern, excludePatterns, normalized, signal, onProgress) {
197
- const { results, filesScanned, truncated, stoppedReason, skippedInaccessible, } = await collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress);
198
- sortSearchResults(results, normalized.sortBy);
199
- return {
200
- results,
201
- summary: buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible),
202
- };
203
- }
204
- export async function searchFiles(basePath, pattern, excludePatterns = [], options = {}) {
205
- const normalized = normalizeOptions(options);
206
- return withTimedAbortSignal(options.signal, normalized.timeoutMs, async (signal) => {
207
- const root = await validateExistingDirectory(basePath, signal);
208
- const { results, summary } = await runSearchFiles(root, pattern, excludePatterns, normalized, signal, options.onProgress);
209
- return {
210
- basePath: root,
211
- pattern,
212
- results,
213
- summary,
214
- };
215
- });
216
- }
@@ -1,10 +0,0 @@
1
- import { z } from 'zod';
2
- export declare const MatcherOptionsSchema: z.ZodObject<{
3
- caseSensitive: z.ZodBoolean;
4
- wholeWord: z.ZodBoolean;
5
- isLiteral: z.ZodBoolean;
6
- }, z.core.$strict>;
7
- export type MatcherOptions = z.infer<typeof MatcherOptionsSchema>;
8
- export type Matcher = (line: string) => number;
9
- export declare function validatePattern(pattern: string, options: MatcherOptions): void;
10
- export declare function buildMatcher(pattern: string, options: MatcherOptions): Matcher;
@@ -1,72 +0,0 @@
1
- import { z } from 'zod';
2
- import RE2 from 're2';
3
- import safeRegex from 'safe-regex2';
4
- export const MatcherOptionsSchema = z.strictObject({
5
- caseSensitive: z.boolean(),
6
- wholeWord: z.boolean(),
7
- isLiteral: z.boolean(),
8
- });
9
- function countRegexLineMatches(regex, line) {
10
- regex.lastIndex = 0;
11
- let count = 0;
12
- while (regex.exec(line) !== null) {
13
- count++;
14
- if (regex.lastIndex === 0)
15
- regex.lastIndex++;
16
- }
17
- return count;
18
- }
19
- function escapeLiteral(pattern) {
20
- return pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
21
- }
22
- function buildRegexPattern(pattern, options) {
23
- const escaped = options.isLiteral ? escapeLiteral(pattern) : pattern;
24
- return options.wholeWord ? `\\b${escaped}\\b` : escaped;
25
- }
26
- export function validatePattern(pattern, options) {
27
- if (options.isLiteral && pattern.length === 0)
28
- return;
29
- if (options.isLiteral && !options.wholeWord)
30
- return;
31
- const final = buildRegexPattern(pattern, options);
32
- if (!safeRegex(final)) {
33
- throw new Error(`Potentially unsafe regular expression (ReDoS risk): ${pattern}`);
34
- }
35
- }
36
- function buildLiteralMatcher(pattern, options) {
37
- if (!options.caseSensitive) {
38
- const final = escapeLiteral(pattern);
39
- const regex = new RegExp(final, 'gi');
40
- return (line) => countRegexLineMatches(regex, line);
41
- }
42
- // Fast path for case-sensitive literal
43
- const needle = pattern;
44
- if (needle.length === 0)
45
- return () => 0;
46
- return (line) => {
47
- if (line.length === 0)
48
- return 0;
49
- let count = 0;
50
- let pos = line.indexOf(needle);
51
- while (pos !== -1) {
52
- count++;
53
- pos = line.indexOf(needle, pos + needle.length);
54
- }
55
- return count;
56
- };
57
- }
58
- function buildRegexMatcher(final, caseSensitive) {
59
- const regex = new RE2(final, caseSensitive ? 'g' : 'gi');
60
- return (line) => countRegexLineMatches(regex, line);
61
- }
62
- export function buildMatcher(pattern, options) {
63
- if (options.isLiteral && pattern.length === 0)
64
- return () => 0;
65
- if (options.isLiteral && !options.wholeWord) {
66
- // fast path for simple literal search
67
- return buildLiteralMatcher(pattern, options);
68
- }
69
- const final = buildRegexPattern(pattern, options);
70
- validatePattern(pattern, options); // Re-validate to be safe
71
- return buildRegexMatcher(final, options.caseSensitive);
72
- }
@@ -1,2 +0,0 @@
1
- import type { ScanRequest, ScanResult, WorkerResponse } from './search-content.js';
2
- export type { WorkerResponse, ScanRequest, ScanResult };
@@ -1,131 +0,0 @@
1
- import { parentPort, threadId, workerData } from 'node:worker_threads';
2
- import { formatUnknownErrorMessage } from '../errors.js';
3
- import { isProbablyBinary } from '../fs-helpers.js';
4
- import { startPerfMeasure } from '../observability.js';
5
- import { scanFileInWorker } from './search-content.js';
6
- import { buildMatcher } from './search-matcher.js';
7
- import {} from './search-matcher.js';
8
- const matcherCache = new Map();
9
- const MAX_MATCHER_CACHE_SIZE = 100;
10
- function getMatcherCacheKey(pattern, options) {
11
- const cs = options.caseSensitive ? '1' : '0';
12
- const ww = options.wholeWord ? '1' : '0';
13
- const lit = options.isLiteral ? '1' : '0';
14
- return `${pattern}|${cs}|${ww}|${lit}`;
15
- }
16
- function getCachedMatcher(pattern, options) {
17
- const key = getMatcherCacheKey(pattern, options);
18
- const cached = matcherCache.get(key);
19
- if (cached) {
20
- refreshMatcherCacheEntry(key, cached);
21
- return cached;
22
- }
23
- const matcher = buildMatcher(pattern, options);
24
- refreshMatcherCacheEntry(key, matcher);
25
- evictOldestMatcherIfNeeded();
26
- return matcher;
27
- }
28
- function refreshMatcherCacheEntry(key, matcher) {
29
- matcherCache.delete(key);
30
- matcherCache.set(key, matcher);
31
- }
32
- function evictOldestMatcherIfNeeded() {
33
- if (matcherCache.size <= MAX_MATCHER_CACHE_SIZE)
34
- return;
35
- const firstKey = matcherCache.keys().next().value;
36
- if (firstKey !== undefined) {
37
- matcherCache.delete(firstKey);
38
- }
39
- }
40
- const cancelledRequests = new Set();
41
- const activeRequests = new Set();
42
- let shuttingDown = false;
43
- function maybeFinishShutdown() {
44
- if (!shuttingDown)
45
- return;
46
- if (activeRequests.size > 0)
47
- return;
48
- parentPort?.close();
49
- }
50
- function consumeCancelled(id) {
51
- if (!cancelledRequests.has(id)) {
52
- return false;
53
- }
54
- cancelledRequests.delete(id);
55
- return true;
56
- }
57
- function markCancelledIfActive(id) {
58
- if (activeRequests.has(id)) {
59
- cancelledRequests.add(id);
60
- }
61
- }
62
- function buildScanResponse(id, result) {
63
- return {
64
- type: 'result',
65
- id,
66
- result,
67
- };
68
- }
69
- function buildErrorResponse(id, error) {
70
- return {
71
- type: 'error',
72
- id,
73
- error: formatUnknownErrorMessage(error),
74
- };
75
- }
76
- async function handleScanRequest(request) {
77
- const { id, resolvedPath, requestedPath, pattern, matcherOptions, scanOptions, maxMatches, } = request;
78
- if (consumeCancelled(id))
79
- return;
80
- activeRequests.add(id);
81
- const endMeasure = startPerfMeasure('searchWorker.scan', {
82
- maxMatches,
83
- });
84
- let ok = false;
85
- try {
86
- const matcher = getCachedMatcher(pattern, matcherOptions);
87
- const isCancelled = () => cancelledRequests.has(id);
88
- const result = await scanFileInWorker(resolvedPath, requestedPath, matcher, scanOptions, maxMatches, isCancelled, isProbablyBinary);
89
- if (consumeCancelled(id))
90
- return;
91
- parentPort?.postMessage(buildScanResponse(id, result));
92
- ok = true;
93
- }
94
- catch (err) {
95
- if (consumeCancelled(id))
96
- return;
97
- parentPort?.postMessage(buildErrorResponse(id, err));
98
- }
99
- finally {
100
- activeRequests.delete(id);
101
- cancelledRequests.delete(id);
102
- endMeasure?.(ok);
103
- maybeFinishShutdown();
104
- }
105
- }
106
- function handleMessage(message) {
107
- switch (message.type) {
108
- case 'scan':
109
- if (shuttingDown)
110
- return;
111
- void handleScanRequest(message);
112
- break;
113
- case 'cancel':
114
- markCancelledIfActive(message.id);
115
- break;
116
- case 'shutdown':
117
- shuttingDown = true;
118
- for (const id of activeRequests) {
119
- markCancelledIfActive(id);
120
- }
121
- maybeFinishShutdown();
122
- break;
123
- }
124
- }
125
- if (parentPort) {
126
- parentPort.on('message', handleMessage);
127
- const data = workerData;
128
- if (data?.debug) {
129
- console.error(`[SearchWorker] Started with threadId=${String(threadId)}`);
130
- }
131
- }
@@ -1,28 +0,0 @@
1
- import type { EntryType } from './common.js';
2
- interface TreeEntry {
3
- name: string;
4
- type: EntryType;
5
- relativePath: string;
6
- children?: TreeEntry[];
7
- }
8
- interface TreeOptions {
9
- maxDepth?: number;
10
- maxEntries?: number;
11
- includeHidden?: boolean;
12
- includeIgnored?: boolean;
13
- timeoutMs?: number;
14
- signal?: AbortSignal;
15
- onProgress?: (progress: {
16
- total?: number;
17
- current: number;
18
- }) => void;
19
- }
20
- interface TreeResult {
21
- root: string;
22
- tree: TreeEntry;
23
- truncated: boolean;
24
- totalEntries: number;
25
- }
26
- export declare function formatTreeAscii(tree: TreeEntry): string;
27
- export declare function treeDirectory(dirPath: string, options?: TreeOptions): Promise<TreeResult>;
28
- export {};