@j0hanz/filesystem-context-mcp 1.0.0 → 1.0.2
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/README.md +426 -224
- package/dist/__tests__/lib/errors.test.js +36 -1
- package/dist/__tests__/lib/errors.test.js.map +1 -1
- package/dist/__tests__/lib/file-operations.test.js +1 -1
- package/dist/__tests__/lib/file-operations.test.js.map +1 -1
- package/dist/__tests__/lib/formatters.test.d.ts +2 -0
- package/dist/__tests__/lib/formatters.test.d.ts.map +1 -0
- package/dist/__tests__/lib/formatters.test.js +251 -0
- package/dist/__tests__/lib/formatters.test.js.map +1 -0
- package/dist/__tests__/lib/image-parsing.test.d.ts +2 -0
- package/dist/__tests__/lib/image-parsing.test.d.ts.map +1 -0
- package/dist/__tests__/lib/image-parsing.test.js +265 -0
- package/dist/__tests__/lib/image-parsing.test.js.map +1 -0
- package/dist/__tests__/schemas/validators.test.d.ts +2 -0
- package/dist/__tests__/schemas/validators.test.d.ts.map +1 -0
- package/dist/__tests__/schemas/validators.test.js +142 -0
- package/dist/__tests__/schemas/validators.test.js.map +1 -0
- package/dist/config/types.d.ts +29 -3
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js.map +1 -1
- package/dist/index.js +5 -12
- package/dist/index.js.map +1 -1
- package/dist/lib/constants.d.ts +8 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +10 -0
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/errors.d.ts +2 -6
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +59 -58
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/file-operations.d.ts +0 -12
- package/dist/lib/file-operations.d.ts.map +1 -1
- package/dist/lib/file-operations.js +70 -207
- package/dist/lib/file-operations.js.map +1 -1
- package/dist/lib/fs-helpers.d.ts.map +1 -1
- package/dist/lib/fs-helpers.js +50 -11
- package/dist/lib/fs-helpers.js.map +1 -1
- package/dist/lib/image-parsing.d.ts +8 -0
- package/dist/lib/image-parsing.d.ts.map +1 -0
- package/dist/lib/image-parsing.js +119 -0
- package/dist/lib/image-parsing.js.map +1 -0
- package/dist/lib/path-validation.d.ts.map +1 -1
- package/dist/lib/path-validation.js +1 -4
- package/dist/lib/path-validation.js.map +1 -1
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +2 -0
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/inputs.d.ts.map +1 -1
- package/dist/schemas/inputs.js +9 -4
- package/dist/schemas/inputs.js.map +1 -1
- package/dist/schemas/outputs.d.ts +12 -9
- package/dist/schemas/outputs.d.ts.map +1 -1
- package/dist/schemas/outputs.js +10 -3
- package/dist/schemas/outputs.js.map +1 -1
- package/dist/schemas/validators.d.ts +12 -0
- package/dist/schemas/validators.d.ts.map +1 -0
- package/dist/schemas/validators.js +35 -0
- package/dist/schemas/validators.js.map +1 -0
- package/dist/server.d.ts +3 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +26 -15
- package/dist/server.js.map +1 -1
- package/dist/tools/analyze-directory.js +1 -1
- package/dist/tools/analyze-directory.js.map +1 -1
- package/dist/tools/directory-tree.js +1 -1
- package/dist/tools/directory-tree.js.map +1 -1
- package/dist/tools/list-directory.js +1 -1
- package/dist/tools/list-directory.js.map +1 -1
- package/dist/tools/read-file.d.ts.map +1 -1
- package/dist/tools/read-file.js +3 -0
- package/dist/tools/read-file.js.map +1 -1
- package/dist/tools/read-multiple-files.d.ts.map +1 -1
- package/dist/tools/read-multiple-files.js +3 -0
- package/dist/tools/read-multiple-files.js.map +1 -1
- package/dist/tools/search-content.d.ts.map +1 -1
- package/dist/tools/search-content.js +4 -3
- package/dist/tools/search-content.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,12 +5,18 @@ import { createReadStream } from 'node:fs';
|
|
|
5
5
|
import fg from 'fast-glob';
|
|
6
6
|
import { Minimatch } from 'minimatch';
|
|
7
7
|
import safeRegex from 'safe-regex2';
|
|
8
|
-
import { DEFAULT_MAX_DEPTH, DEFAULT_MAX_RESULTS, DEFAULT_TOP_N, DIR_TRAVERSAL_CONCURRENCY, MAX_LINE_CONTENT_LENGTH, MAX_MEDIA_FILE_SIZE, MAX_SEARCHABLE_FILE_SIZE, MAX_TEXT_FILE_SIZE,
|
|
8
|
+
import { DEFAULT_MAX_DEPTH, DEFAULT_MAX_RESULTS, DEFAULT_TOP_N, DIR_TRAVERSAL_CONCURRENCY, getMimeType, MAX_LINE_CONTENT_LENGTH, MAX_MEDIA_FILE_SIZE, MAX_SEARCHABLE_FILE_SIZE, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, REGEX_MATCH_TIMEOUT_MS, } from './constants.js';
|
|
9
9
|
import { ErrorCode, McpError } from './errors.js';
|
|
10
10
|
import { getFileType, isHidden, isProbablyBinary, readFile, runWorkQueue, } from './fs-helpers.js';
|
|
11
|
+
import { parseImageDimensions } from './image-parsing.js';
|
|
11
12
|
import { validateExistingPath, validateExistingPathDetailed, } from './path-validation.js';
|
|
12
|
-
function
|
|
13
|
-
|
|
13
|
+
// Create a matcher function from exclude patterns
|
|
14
|
+
function createExcludeMatcher(excludePatterns) {
|
|
15
|
+
if (excludePatterns.length === 0) {
|
|
16
|
+
return () => false;
|
|
17
|
+
}
|
|
18
|
+
const matchers = excludePatterns.map((pattern) => new Minimatch(pattern));
|
|
19
|
+
return (name, relativePath) => matchers.some((m) => m.match(name) || m.match(relativePath));
|
|
14
20
|
}
|
|
15
21
|
async function processInParallel(items, processor, concurrency = PARALLEL_CONCURRENCY) {
|
|
16
22
|
const results = [];
|
|
@@ -54,33 +60,22 @@ function countRegexMatches(line, regex, timeoutMs = REGEX_MATCH_TIMEOUT_MS) {
|
|
|
54
60
|
/**
|
|
55
61
|
* Check if a regex pattern is simple enough to be safe without full ReDoS analysis.
|
|
56
62
|
* This reduces false positives from safe-regex2 for common safe patterns.
|
|
57
|
-
*
|
|
58
|
-
* A pattern is considered "simple safe" if it:
|
|
59
|
-
* - Has no nested quantifiers (e.g., (a+)+ or (a*)*) which are the main ReDoS concern
|
|
60
|
-
* - Has no high repetition counts (e.g., {25} or higher) that safe-regex2 would flag
|
|
61
|
-
*
|
|
62
|
-
* This is a quick heuristic, not a full safety proof.
|
|
63
63
|
*/
|
|
64
64
|
function isSimpleSafePattern(pattern) {
|
|
65
65
|
// Patterns with nested quantifiers are the main ReDoS concern
|
|
66
|
-
// Look for quantifier followed by closing paren then another quantifier
|
|
67
|
-
// Matches patterns like: (a+)+, (a*)+, (a+)*, (a?)+, (a{2})+
|
|
68
66
|
const nestedQuantifierPattern = /[+*?}]\s*\)\s*[+*?{]/;
|
|
69
67
|
if (nestedQuantifierPattern.test(pattern)) {
|
|
70
|
-
return false;
|
|
68
|
+
return false;
|
|
71
69
|
}
|
|
72
70
|
// Check for high repetition counts that safe-regex2 would flag (default limit is 25)
|
|
73
|
-
// Matches {n} or {n,} or {n,m} where n >= 25
|
|
74
71
|
const highRepetitionPattern = /\{(\d+)(?:,\d*)?\}/g;
|
|
75
72
|
let match;
|
|
76
73
|
while ((match = highRepetitionPattern.exec(pattern)) !== null) {
|
|
77
74
|
const count = parseInt(match[1] ?? '0', 10);
|
|
78
75
|
if (count >= 25) {
|
|
79
|
-
return false;
|
|
76
|
+
return false;
|
|
80
77
|
}
|
|
81
78
|
}
|
|
82
|
-
// Simple patterns without nested quantifiers or high repetition are generally safe
|
|
83
|
-
// Examples: "throw new McpError\(", "\bword\b", "foo|bar"
|
|
84
79
|
return true;
|
|
85
80
|
}
|
|
86
81
|
function getPermissions(mode) {
|
|
@@ -94,8 +89,7 @@ export async function getFileInfo(filePath) {
|
|
|
94
89
|
const { requestedPath, resolvedPath, isSymlink } = await validateExistingPathDetailed(filePath);
|
|
95
90
|
const name = path.basename(requestedPath);
|
|
96
91
|
const ext = path.extname(name).toLowerCase();
|
|
97
|
-
const mimeType =
|
|
98
|
-
// If it is a symlink, try to read the link target without following.
|
|
92
|
+
const mimeType = ext ? getMimeType(ext) : undefined;
|
|
99
93
|
let symlinkTarget;
|
|
100
94
|
if (isSymlink) {
|
|
101
95
|
try {
|
|
@@ -105,7 +99,6 @@ export async function getFileInfo(filePath) {
|
|
|
105
99
|
// Symlink target unreadable
|
|
106
100
|
}
|
|
107
101
|
}
|
|
108
|
-
// Use stat for size/dates (follows symlinks), but keep type as symlink based on lstat.
|
|
109
102
|
const stats = await fs.stat(resolvedPath);
|
|
110
103
|
return {
|
|
111
104
|
name,
|
|
@@ -130,7 +123,7 @@ export async function listDirectory(dirPath, options = {}) {
|
|
|
130
123
|
let maxDepthReached = 0;
|
|
131
124
|
let truncated = false;
|
|
132
125
|
let skippedInaccessible = 0;
|
|
133
|
-
let
|
|
126
|
+
let symlinksNotFollowed = 0;
|
|
134
127
|
const stopIfNeeded = () => {
|
|
135
128
|
if (maxEntries !== undefined && entries.length >= maxEntries) {
|
|
136
129
|
truncated = true;
|
|
@@ -164,7 +157,7 @@ export async function listDirectory(dirPath, options = {}) {
|
|
|
164
157
|
const relativePath = path.relative(validPath, fullPath) || item.name;
|
|
165
158
|
try {
|
|
166
159
|
if (item.isSymbolicLink()) {
|
|
167
|
-
|
|
160
|
+
symlinksNotFollowed++;
|
|
168
161
|
const stats = await fs.lstat(fullPath);
|
|
169
162
|
let symlinkTarget;
|
|
170
163
|
if (includeSymlinkTargets) {
|
|
@@ -224,7 +217,6 @@ export async function listDirectory(dirPath, options = {}) {
|
|
|
224
217
|
return { entry };
|
|
225
218
|
}
|
|
226
219
|
});
|
|
227
|
-
// Count errors from parallel processing as inaccessible
|
|
228
220
|
skippedInaccessible += processingErrors.length;
|
|
229
221
|
for (const { entry, enqueueDir } of processedEntries) {
|
|
230
222
|
if (stopIfNeeded())
|
|
@@ -245,7 +237,6 @@ export async function listDirectory(dirPath, options = {}) {
|
|
|
245
237
|
case 'modified':
|
|
246
238
|
return (b.modified?.getTime() ?? 0) - (a.modified?.getTime() ?? 0);
|
|
247
239
|
case 'type':
|
|
248
|
-
// directories first, then by name
|
|
249
240
|
if (a.type !== b.type) {
|
|
250
241
|
return a.type === 'directory' ? -1 : 1;
|
|
251
242
|
}
|
|
@@ -265,7 +256,7 @@ export async function listDirectory(dirPath, options = {}) {
|
|
|
265
256
|
maxDepthReached,
|
|
266
257
|
truncated,
|
|
267
258
|
skippedInaccessible,
|
|
268
|
-
|
|
259
|
+
symlinksNotFollowed,
|
|
269
260
|
},
|
|
270
261
|
};
|
|
271
262
|
}
|
|
@@ -311,8 +302,8 @@ export async function searchFiles(basePath, pattern, excludePatterns = [], optio
|
|
|
311
302
|
dot: true,
|
|
312
303
|
ignore: excludePatterns,
|
|
313
304
|
suppressErrors: true,
|
|
314
|
-
followSymbolicLinks: false,
|
|
315
|
-
deep: maxDepth,
|
|
305
|
+
followSymbolicLinks: false,
|
|
306
|
+
deep: maxDepth,
|
|
316
307
|
});
|
|
317
308
|
for await (const entry of stream) {
|
|
318
309
|
const matchPath = typeof entry === 'string' ? entry : String(entry);
|
|
@@ -354,17 +345,11 @@ export async function searchFiles(basePath, pattern, excludePatterns = [], optio
|
|
|
354
345
|
},
|
|
355
346
|
};
|
|
356
347
|
}
|
|
357
|
-
// Re-export readFile from fs-helpers so it can be used by tools
|
|
358
348
|
export { readFile };
|
|
359
|
-
/**
|
|
360
|
-
* Read multiple files in parallel.
|
|
361
|
-
* Individual file errors don't fail the entire operation.
|
|
362
|
-
*/
|
|
363
349
|
export async function readMultipleFiles(filePaths, options = {}) {
|
|
364
350
|
const { encoding = 'utf-8', maxSize = MAX_TEXT_FILE_SIZE, head, tail, } = options;
|
|
365
351
|
if (filePaths.length === 0)
|
|
366
352
|
return [];
|
|
367
|
-
// Preserve input order while limiting concurrency to avoid spiky I/O / EMFILE.
|
|
368
353
|
const output = filePaths.map((filePath) => ({ path: filePath }));
|
|
369
354
|
const { results, errors } = await processInParallel(filePaths.map((filePath, index) => ({ filePath, index })), async ({ filePath, index }) => {
|
|
370
355
|
const result = await readFile(filePath, {
|
|
@@ -394,17 +379,13 @@ export async function searchContent(basePath, searchPattern, options = {}) {
|
|
|
394
379
|
const { filePattern = '**/*', excludePatterns = [], caseSensitive = false, maxResults = DEFAULT_MAX_RESULTS, maxFileSize = MAX_SEARCHABLE_FILE_SIZE, maxFilesScanned, timeoutMs, skipBinary = true, contextLines = 0, wholeWord = false, isLiteral = false, } = options;
|
|
395
380
|
const validPath = await validateExistingPath(basePath);
|
|
396
381
|
const deadlineMs = timeoutMs !== undefined ? Date.now() + timeoutMs : undefined;
|
|
397
|
-
// Build the final pattern
|
|
398
382
|
let finalPattern = searchPattern;
|
|
399
|
-
// Escape regex special characters if literal mode
|
|
400
383
|
if (isLiteral) {
|
|
401
384
|
finalPattern = finalPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
402
385
|
}
|
|
403
|
-
// Add word boundaries if whole word mode
|
|
404
386
|
if (wholeWord) {
|
|
405
387
|
finalPattern = `\\b${finalPattern}\\b`;
|
|
406
388
|
}
|
|
407
|
-
// ReDoS protection: skip check for literal or simple patterns
|
|
408
389
|
const needsReDoSCheck = !isLiteral && !isSimpleSafePattern(finalPattern);
|
|
409
390
|
if (needsReDoSCheck && !safeRegex(finalPattern)) {
|
|
410
391
|
throw new McpError(ErrorCode.E_INVALID_PATTERN, `Potentially unsafe regular expression (ReDoS risk): ${searchPattern}. ` +
|
|
@@ -422,12 +403,42 @@ export async function searchContent(basePath, searchPattern, options = {}) {
|
|
|
422
403
|
const message = error instanceof Error ? error.message : String(error);
|
|
423
404
|
throw new McpError(ErrorCode.E_INVALID_PATTERN, `Invalid regular expression: ${finalPattern} (${message})`, basePath, { searchPattern: finalPattern });
|
|
424
405
|
}
|
|
406
|
+
// Circular buffer to hold context lines before a match
|
|
407
|
+
class CircularLineBuffer {
|
|
408
|
+
capacity;
|
|
409
|
+
buffer;
|
|
410
|
+
writeIndex = 0;
|
|
411
|
+
count = 0;
|
|
412
|
+
constructor(capacity) {
|
|
413
|
+
this.capacity = capacity;
|
|
414
|
+
this.buffer = new Array(capacity);
|
|
415
|
+
}
|
|
416
|
+
push(line) {
|
|
417
|
+
this.buffer[this.writeIndex] = line;
|
|
418
|
+
this.writeIndex = (this.writeIndex + 1) % this.capacity;
|
|
419
|
+
if (this.count < this.capacity)
|
|
420
|
+
this.count++;
|
|
421
|
+
}
|
|
422
|
+
toArray() {
|
|
423
|
+
if (this.count === 0)
|
|
424
|
+
return [];
|
|
425
|
+
if (this.count < this.capacity) {
|
|
426
|
+
return this.buffer.slice(0, this.count);
|
|
427
|
+
}
|
|
428
|
+
// Buffer is full - return in correct order starting from writeIndex
|
|
429
|
+
return [
|
|
430
|
+
...this.buffer.slice(this.writeIndex),
|
|
431
|
+
...this.buffer.slice(0, this.writeIndex),
|
|
432
|
+
];
|
|
433
|
+
}
|
|
434
|
+
}
|
|
425
435
|
const matches = [];
|
|
426
436
|
let filesScanned = 0;
|
|
427
437
|
let filesMatched = 0;
|
|
428
438
|
let skippedTooLarge = 0;
|
|
429
439
|
let skippedBinary = 0;
|
|
430
440
|
let skippedInaccessible = 0;
|
|
441
|
+
let linesSkippedDueToRegexTimeout = 0;
|
|
431
442
|
let truncated = false;
|
|
432
443
|
let stoppedReason;
|
|
433
444
|
const stopNow = (reason) => {
|
|
@@ -442,11 +453,11 @@ export async function searchContent(basePath, searchPattern, options = {}) {
|
|
|
442
453
|
dot: false,
|
|
443
454
|
ignore: excludePatterns,
|
|
444
455
|
suppressErrors: true,
|
|
445
|
-
followSymbolicLinks: false,
|
|
456
|
+
followSymbolicLinks: false,
|
|
446
457
|
});
|
|
447
458
|
for await (const entry of stream) {
|
|
448
459
|
const file = typeof entry === 'string' ? entry : String(entry);
|
|
449
|
-
if (
|
|
460
|
+
if (deadlineMs !== undefined && Date.now() > deadlineMs) {
|
|
450
461
|
stopNow('timeout');
|
|
451
462
|
break;
|
|
452
463
|
}
|
|
@@ -491,12 +502,12 @@ export async function searchContent(basePath, searchPattern, options = {}) {
|
|
|
491
502
|
});
|
|
492
503
|
let fileHadMatches = false;
|
|
493
504
|
let lineNumber = 0;
|
|
494
|
-
const lineBuffer =
|
|
505
|
+
const lineBuffer = contextLines > 0 ? new CircularLineBuffer(contextLines) : null;
|
|
495
506
|
const pendingMatches = [];
|
|
496
507
|
try {
|
|
497
508
|
for await (const line of rl) {
|
|
498
509
|
lineNumber++;
|
|
499
|
-
if (
|
|
510
|
+
if (deadlineMs !== undefined && Date.now() > deadlineMs) {
|
|
500
511
|
stopNow('timeout');
|
|
501
512
|
break;
|
|
502
513
|
}
|
|
@@ -518,13 +529,10 @@ export async function searchContent(basePath, searchPattern, options = {}) {
|
|
|
518
529
|
}
|
|
519
530
|
const matchCount = countRegexMatches(line, regex);
|
|
520
531
|
if (matchCount < 0) {
|
|
532
|
+
linesSkippedDueToRegexTimeout++;
|
|
521
533
|
console.error(`[searchContent] Skipping line ${lineNumber} in ${validFile} due to regex timeout`);
|
|
522
|
-
|
|
523
|
-
if (contextLines > 0) {
|
|
534
|
+
if (lineBuffer) {
|
|
524
535
|
lineBuffer.push(trimmedLine);
|
|
525
|
-
if (lineBuffer.length > contextLines) {
|
|
526
|
-
lineBuffer.shift();
|
|
527
|
-
}
|
|
528
536
|
}
|
|
529
537
|
continue;
|
|
530
538
|
}
|
|
@@ -536,8 +544,9 @@ export async function searchContent(basePath, searchPattern, options = {}) {
|
|
|
536
544
|
content: trimmedLine,
|
|
537
545
|
matchCount,
|
|
538
546
|
};
|
|
539
|
-
|
|
540
|
-
|
|
547
|
+
const contextBefore = lineBuffer?.toArray();
|
|
548
|
+
if (contextBefore && contextBefore.length > 0) {
|
|
549
|
+
newMatch.contextBefore = contextBefore;
|
|
541
550
|
}
|
|
542
551
|
matches.push(newMatch);
|
|
543
552
|
if (contextLines > 0) {
|
|
@@ -547,11 +556,8 @@ export async function searchContent(basePath, searchPattern, options = {}) {
|
|
|
547
556
|
});
|
|
548
557
|
}
|
|
549
558
|
}
|
|
550
|
-
if (
|
|
559
|
+
if (lineBuffer) {
|
|
551
560
|
lineBuffer.push(trimmedLine);
|
|
552
|
-
if (lineBuffer.length > contextLines) {
|
|
553
|
-
lineBuffer.shift();
|
|
554
|
-
}
|
|
555
561
|
}
|
|
556
562
|
}
|
|
557
563
|
}
|
|
@@ -564,16 +570,11 @@ export async function searchContent(basePath, searchPattern, options = {}) {
|
|
|
564
570
|
if (stoppedReason !== undefined)
|
|
565
571
|
break;
|
|
566
572
|
}
|
|
567
|
-
catch
|
|
573
|
+
catch {
|
|
568
574
|
if (handle) {
|
|
569
575
|
await handle.close().catch(() => { });
|
|
570
576
|
}
|
|
571
577
|
skippedInaccessible++;
|
|
572
|
-
// Log unexpected errors for debugging
|
|
573
|
-
const { code } = error;
|
|
574
|
-
if (code !== 'ENOENT' && code !== 'EACCES' && code !== 'EPERM') {
|
|
575
|
-
console.error(`[searchContent] Error processing ${file}:`, error);
|
|
576
|
-
}
|
|
577
578
|
}
|
|
578
579
|
}
|
|
579
580
|
return {
|
|
@@ -589,6 +590,7 @@ export async function searchContent(basePath, searchPattern, options = {}) {
|
|
|
589
590
|
skippedTooLarge,
|
|
590
591
|
skippedBinary,
|
|
591
592
|
skippedInaccessible,
|
|
593
|
+
linesSkippedDueToRegexTimeout,
|
|
592
594
|
stoppedReason,
|
|
593
595
|
},
|
|
594
596
|
};
|
|
@@ -601,18 +603,11 @@ export async function analyzeDirectory(dirPath, options = {}) {
|
|
|
601
603
|
let totalSize = 0;
|
|
602
604
|
let currentMaxDepth = 0;
|
|
603
605
|
let skippedInaccessible = 0;
|
|
604
|
-
let
|
|
606
|
+
let symlinksNotFollowed = 0;
|
|
605
607
|
const fileTypes = {};
|
|
606
608
|
const largestFiles = [];
|
|
607
609
|
const recentlyModified = [];
|
|
608
|
-
const
|
|
609
|
-
? excludePatterns.map((pattern) => new Minimatch(pattern))
|
|
610
|
-
: [];
|
|
611
|
-
const shouldExclude = (name, relativePath) => {
|
|
612
|
-
if (excludeMatchers.length === 0)
|
|
613
|
-
return false;
|
|
614
|
-
return excludeMatchers.some((m) => m.match(name) || m.match(relativePath));
|
|
615
|
-
};
|
|
610
|
+
const shouldExclude = createExcludeMatcher(excludePatterns);
|
|
616
611
|
const insertSorted = (arr, item, compare, maxLen) => {
|
|
617
612
|
if (maxLen <= 0)
|
|
618
613
|
return;
|
|
@@ -646,18 +641,16 @@ export async function analyzeDirectory(dirPath, options = {}) {
|
|
|
646
641
|
for (const item of items) {
|
|
647
642
|
const fullPath = path.join(currentPath, item.name);
|
|
648
643
|
const relativePath = path.relative(validPath, fullPath);
|
|
649
|
-
// Skip hidden files/directories unless includeHidden is true
|
|
650
644
|
if (!includeHidden && isHidden(item.name)) {
|
|
651
645
|
continue;
|
|
652
646
|
}
|
|
653
|
-
// Skip items matching exclude patterns
|
|
654
647
|
if (shouldExclude(item.name, relativePath)) {
|
|
655
648
|
continue;
|
|
656
649
|
}
|
|
657
650
|
try {
|
|
658
651
|
const validated = await validateExistingPathDetailed(fullPath);
|
|
659
652
|
if (validated.isSymlink || item.isSymbolicLink()) {
|
|
660
|
-
|
|
653
|
+
symlinksNotFollowed++;
|
|
661
654
|
continue;
|
|
662
655
|
}
|
|
663
656
|
const stats = await fs.stat(validated.resolvedPath);
|
|
@@ -683,7 +676,7 @@ export async function analyzeDirectory(dirPath, options = {}) {
|
|
|
683
676
|
if (error instanceof McpError &&
|
|
684
677
|
(error.code === ErrorCode.E_ACCESS_DENIED ||
|
|
685
678
|
error.code === ErrorCode.E_SYMLINK_NOT_ALLOWED)) {
|
|
686
|
-
|
|
679
|
+
symlinksNotFollowed++;
|
|
687
680
|
}
|
|
688
681
|
else {
|
|
689
682
|
skippedInaccessible++;
|
|
@@ -706,18 +699,13 @@ export async function analyzeDirectory(dirPath, options = {}) {
|
|
|
706
699
|
summary: {
|
|
707
700
|
truncated: false,
|
|
708
701
|
skippedInaccessible,
|
|
709
|
-
|
|
702
|
+
symlinksNotFollowed,
|
|
710
703
|
},
|
|
711
704
|
};
|
|
712
705
|
}
|
|
713
|
-
/**
|
|
714
|
-
* Build a JSON tree structure of a directory.
|
|
715
|
-
* More efficient for AI parsing than flat file lists.
|
|
716
|
-
*/
|
|
717
706
|
export async function getDirectoryTree(dirPath, options = {}) {
|
|
718
707
|
const { maxDepth = DEFAULT_MAX_DEPTH, excludePatterns = [], includeHidden = false, includeSize = false, maxFiles, } = options;
|
|
719
708
|
const validPath = await validateExistingPath(dirPath);
|
|
720
|
-
// Ensure the requested path is a directory (not just an existing path).
|
|
721
709
|
const rootStats = await fs.stat(validPath);
|
|
722
710
|
if (!rootStats.isDirectory()) {
|
|
723
711
|
throw new McpError(ErrorCode.E_NOT_DIRECTORY, `Not a directory: ${dirPath}`, dirPath);
|
|
@@ -726,19 +714,12 @@ export async function getDirectoryTree(dirPath, options = {}) {
|
|
|
726
714
|
let totalDirectories = 0;
|
|
727
715
|
let maxDepthReached = 0;
|
|
728
716
|
let skippedInaccessible = 0;
|
|
729
|
-
let
|
|
717
|
+
let symlinksNotFollowed = 0;
|
|
730
718
|
let truncated = false;
|
|
731
|
-
const
|
|
732
|
-
? excludePatterns.map((pattern) => new Minimatch(pattern))
|
|
733
|
-
: [];
|
|
719
|
+
const shouldExclude = createExcludeMatcher(excludePatterns);
|
|
734
720
|
const hitMaxFiles = () => {
|
|
735
721
|
return maxFiles !== undefined && totalFiles >= maxFiles;
|
|
736
722
|
};
|
|
737
|
-
const shouldExclude = (name, relativePath) => {
|
|
738
|
-
if (excludeMatchers.length === 0)
|
|
739
|
-
return false;
|
|
740
|
-
return excludeMatchers.some((m) => m.match(name) || m.match(relativePath));
|
|
741
|
-
};
|
|
742
723
|
const buildTree = async (currentPath, depth, relativePath = '') => {
|
|
743
724
|
if (hitMaxFiles()) {
|
|
744
725
|
truncated = true;
|
|
@@ -754,7 +735,7 @@ export async function getDirectoryTree(dirPath, options = {}) {
|
|
|
754
735
|
if (error instanceof McpError &&
|
|
755
736
|
(error.code === ErrorCode.E_ACCESS_DENIED ||
|
|
756
737
|
error.code === ErrorCode.E_SYMLINK_NOT_ALLOWED)) {
|
|
757
|
-
|
|
738
|
+
symlinksNotFollowed++;
|
|
758
739
|
}
|
|
759
740
|
else {
|
|
760
741
|
skippedInaccessible++;
|
|
@@ -762,7 +743,6 @@ export async function getDirectoryTree(dirPath, options = {}) {
|
|
|
762
743
|
return null;
|
|
763
744
|
}
|
|
764
745
|
const name = path.basename(currentPath);
|
|
765
|
-
// Check exclusions
|
|
766
746
|
if (shouldExclude(name, relativePath)) {
|
|
767
747
|
return null;
|
|
768
748
|
}
|
|
@@ -771,7 +751,7 @@ export async function getDirectoryTree(dirPath, options = {}) {
|
|
|
771
751
|
}
|
|
772
752
|
maxDepthReached = Math.max(maxDepthReached, depth);
|
|
773
753
|
if (isSymlink) {
|
|
774
|
-
|
|
754
|
+
symlinksNotFollowed++;
|
|
775
755
|
return null;
|
|
776
756
|
}
|
|
777
757
|
let stats;
|
|
@@ -847,14 +827,10 @@ export async function getDirectoryTree(dirPath, options = {}) {
|
|
|
847
827
|
maxDepthReached,
|
|
848
828
|
truncated,
|
|
849
829
|
skippedInaccessible,
|
|
850
|
-
|
|
830
|
+
symlinksNotFollowed,
|
|
851
831
|
},
|
|
852
832
|
};
|
|
853
833
|
}
|
|
854
|
-
/**
|
|
855
|
-
* Read a media/binary file and return as base64.
|
|
856
|
-
* Useful for images, audio, and other binary content.
|
|
857
|
-
*/
|
|
858
834
|
export async function readMediaFile(filePath, options = {}) {
|
|
859
835
|
const { maxSize = MAX_MEDIA_FILE_SIZE } = options;
|
|
860
836
|
const validPath = await validateExistingPath(filePath);
|
|
@@ -867,7 +843,7 @@ export async function readMediaFile(filePath, options = {}) {
|
|
|
867
843
|
throw new McpError(ErrorCode.E_TOO_LARGE, `File too large: ${size} bytes (max: ${maxSize} bytes)`, filePath, { size, maxSize });
|
|
868
844
|
}
|
|
869
845
|
const ext = path.extname(validPath).toLowerCase();
|
|
870
|
-
const mimeType =
|
|
846
|
+
const mimeType = getMimeType(ext);
|
|
871
847
|
const buffer = await fs.readFile(validPath);
|
|
872
848
|
const data = buffer.toString('base64');
|
|
873
849
|
let width;
|
|
@@ -887,117 +863,4 @@ export async function readMediaFile(filePath, options = {}) {
|
|
|
887
863
|
height,
|
|
888
864
|
};
|
|
889
865
|
}
|
|
890
|
-
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47];
|
|
891
|
-
const JPEG_SIGNATURE = [0xff, 0xd8];
|
|
892
|
-
const GIF_SIGNATURE = [0x47, 0x49, 0x46];
|
|
893
|
-
const BMP_SIGNATURE = [0x42, 0x4d];
|
|
894
|
-
const WEBP_RIFF = [0x52, 0x49, 0x46, 0x46];
|
|
895
|
-
const WEBP_MARKER = [0x57, 0x45, 0x42, 0x50];
|
|
896
|
-
function matchesSignature(buffer, signature, offset = 0) {
|
|
897
|
-
if (buffer.length < offset + signature.length)
|
|
898
|
-
return false;
|
|
899
|
-
return signature.every((byte, i) => buffer[offset + i] === byte);
|
|
900
|
-
}
|
|
901
|
-
function parsePng(buffer) {
|
|
902
|
-
if (buffer.length < 24 || !matchesSignature(buffer, PNG_SIGNATURE))
|
|
903
|
-
return null;
|
|
904
|
-
return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
|
|
905
|
-
}
|
|
906
|
-
function parseJpeg(buffer) {
|
|
907
|
-
if (buffer.length < 2 || !matchesSignature(buffer, JPEG_SIGNATURE))
|
|
908
|
-
return null;
|
|
909
|
-
let offset = 2;
|
|
910
|
-
while (offset < buffer.length - 8) {
|
|
911
|
-
if (buffer[offset] !== 0xff) {
|
|
912
|
-
offset++;
|
|
913
|
-
continue;
|
|
914
|
-
}
|
|
915
|
-
const marker = buffer[offset + 1];
|
|
916
|
-
const isSOF = marker !== undefined &&
|
|
917
|
-
((marker >= 0xc0 && marker <= 0xc3) ||
|
|
918
|
-
(marker >= 0xc5 && marker <= 0xc7) ||
|
|
919
|
-
(marker >= 0xc9 && marker <= 0xcb) ||
|
|
920
|
-
(marker >= 0xcd && marker <= 0xcf));
|
|
921
|
-
if (isSOF) {
|
|
922
|
-
return {
|
|
923
|
-
width: buffer.readUInt16BE(offset + 7),
|
|
924
|
-
height: buffer.readUInt16BE(offset + 5),
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
if (offset + 3 >= buffer.length)
|
|
928
|
-
break;
|
|
929
|
-
offset += 2 + buffer.readUInt16BE(offset + 2);
|
|
930
|
-
}
|
|
931
|
-
return null;
|
|
932
|
-
}
|
|
933
|
-
function parseGif(buffer) {
|
|
934
|
-
if (buffer.length < 10 || !matchesSignature(buffer, GIF_SIGNATURE))
|
|
935
|
-
return null;
|
|
936
|
-
return { width: buffer.readUInt16LE(6), height: buffer.readUInt16LE(8) };
|
|
937
|
-
}
|
|
938
|
-
function parseBmp(buffer) {
|
|
939
|
-
if (buffer.length < 26 || !matchesSignature(buffer, BMP_SIGNATURE))
|
|
940
|
-
return null;
|
|
941
|
-
return {
|
|
942
|
-
width: buffer.readInt32LE(18),
|
|
943
|
-
height: Math.abs(buffer.readInt32LE(22)),
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
function parseWebp(buffer) {
|
|
947
|
-
if (buffer.length < 30)
|
|
948
|
-
return null;
|
|
949
|
-
if (!matchesSignature(buffer, WEBP_RIFF) ||
|
|
950
|
-
!matchesSignature(buffer, WEBP_MARKER, 8))
|
|
951
|
-
return null;
|
|
952
|
-
const chunkType = [buffer[12], buffer[13], buffer[14], buffer[15]];
|
|
953
|
-
// VP8 (lossy): 0x56 0x50 0x38 0x20
|
|
954
|
-
if (chunkType[0] === 0x56 &&
|
|
955
|
-
chunkType[1] === 0x50 &&
|
|
956
|
-
chunkType[2] === 0x38 &&
|
|
957
|
-
chunkType[3] === 0x20) {
|
|
958
|
-
return {
|
|
959
|
-
width: buffer.readUInt16LE(26) & 0x3fff,
|
|
960
|
-
height: buffer.readUInt16LE(28) & 0x3fff,
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
// VP8L (lossless): 0x56 0x50 0x38 0x4c
|
|
964
|
-
if (chunkType[0] === 0x56 &&
|
|
965
|
-
chunkType[1] === 0x50 &&
|
|
966
|
-
chunkType[2] === 0x38 &&
|
|
967
|
-
chunkType[3] === 0x4c) {
|
|
968
|
-
const bits = buffer.readUInt32LE(21);
|
|
969
|
-
return { width: (bits & 0x3fff) + 1, height: ((bits >> 14) & 0x3fff) + 1 };
|
|
970
|
-
}
|
|
971
|
-
// VP8X (extended): 0x56 0x50 0x38 0x58
|
|
972
|
-
if (chunkType[0] === 0x56 &&
|
|
973
|
-
chunkType[1] === 0x50 &&
|
|
974
|
-
chunkType[2] === 0x38 &&
|
|
975
|
-
chunkType[3] === 0x58) {
|
|
976
|
-
const width = (buffer[24] ?? 0) | ((buffer[25] ?? 0) << 8) | ((buffer[26] ?? 0) << 16);
|
|
977
|
-
const height = (buffer[27] ?? 0) | ((buffer[28] ?? 0) << 8) | ((buffer[29] ?? 0) << 16);
|
|
978
|
-
return { width: width + 1, height: height + 1 };
|
|
979
|
-
}
|
|
980
|
-
return null;
|
|
981
|
-
}
|
|
982
|
-
const IMAGE_PARSERS = {
|
|
983
|
-
'.png': parsePng,
|
|
984
|
-
'.jpg': parseJpeg,
|
|
985
|
-
'.jpeg': parseJpeg,
|
|
986
|
-
'.gif': parseGif,
|
|
987
|
-
'.bmp': parseBmp,
|
|
988
|
-
'.webp': parseWebp,
|
|
989
|
-
};
|
|
990
|
-
/**
|
|
991
|
-
* Parse image dimensions from common image format headers.
|
|
992
|
-
* Supports PNG, JPEG, GIF, BMP, and WebP.
|
|
993
|
-
*/
|
|
994
|
-
function parseImageDimensions(buffer, ext) {
|
|
995
|
-
try {
|
|
996
|
-
const parser = IMAGE_PARSERS[ext];
|
|
997
|
-
return parser ? parser(buffer) : null;
|
|
998
|
-
}
|
|
999
|
-
catch {
|
|
1000
|
-
return null;
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
866
|
//# sourceMappingURL=file-operations.js.map
|