@j0hanz/filesystem-context-mcp 1.0.1 → 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.
Files changed (79) hide show
  1. package/README.md +571 -532
  2. package/dist/__tests__/lib/errors.test.js +36 -1
  3. package/dist/__tests__/lib/errors.test.js.map +1 -1
  4. package/dist/__tests__/lib/file-operations.test.js +1 -1
  5. package/dist/__tests__/lib/file-operations.test.js.map +1 -1
  6. package/dist/__tests__/lib/formatters.test.d.ts +2 -0
  7. package/dist/__tests__/lib/formatters.test.d.ts.map +1 -0
  8. package/dist/__tests__/lib/formatters.test.js +251 -0
  9. package/dist/__tests__/lib/formatters.test.js.map +1 -0
  10. package/dist/__tests__/lib/image-parsing.test.d.ts +2 -0
  11. package/dist/__tests__/lib/image-parsing.test.d.ts.map +1 -0
  12. package/dist/__tests__/lib/image-parsing.test.js +265 -0
  13. package/dist/__tests__/lib/image-parsing.test.js.map +1 -0
  14. package/dist/__tests__/schemas/validators.test.d.ts +2 -0
  15. package/dist/__tests__/schemas/validators.test.d.ts.map +1 -0
  16. package/dist/__tests__/schemas/validators.test.js +142 -0
  17. package/dist/__tests__/schemas/validators.test.js.map +1 -0
  18. package/dist/config/types.d.ts +29 -3
  19. package/dist/config/types.d.ts.map +1 -1
  20. package/dist/config/types.js.map +1 -1
  21. package/dist/index.js +5 -12
  22. package/dist/index.js.map +1 -1
  23. package/dist/lib/constants.d.ts +8 -0
  24. package/dist/lib/constants.d.ts.map +1 -1
  25. package/dist/lib/constants.js +10 -0
  26. package/dist/lib/constants.js.map +1 -1
  27. package/dist/lib/errors.d.ts +2 -6
  28. package/dist/lib/errors.d.ts.map +1 -1
  29. package/dist/lib/errors.js +59 -58
  30. package/dist/lib/errors.js.map +1 -1
  31. package/dist/lib/file-operations.d.ts +0 -12
  32. package/dist/lib/file-operations.d.ts.map +1 -1
  33. package/dist/lib/file-operations.js +70 -207
  34. package/dist/lib/file-operations.js.map +1 -1
  35. package/dist/lib/fs-helpers.d.ts.map +1 -1
  36. package/dist/lib/fs-helpers.js +50 -11
  37. package/dist/lib/fs-helpers.js.map +1 -1
  38. package/dist/lib/image-parsing.d.ts +8 -0
  39. package/dist/lib/image-parsing.d.ts.map +1 -0
  40. package/dist/lib/image-parsing.js +119 -0
  41. package/dist/lib/image-parsing.js.map +1 -0
  42. package/dist/lib/path-validation.d.ts.map +1 -1
  43. package/dist/lib/path-validation.js +1 -4
  44. package/dist/lib/path-validation.js.map +1 -1
  45. package/dist/schemas/index.d.ts +1 -0
  46. package/dist/schemas/index.d.ts.map +1 -1
  47. package/dist/schemas/index.js +2 -0
  48. package/dist/schemas/index.js.map +1 -1
  49. package/dist/schemas/inputs.d.ts.map +1 -1
  50. package/dist/schemas/inputs.js +9 -4
  51. package/dist/schemas/inputs.js.map +1 -1
  52. package/dist/schemas/outputs.d.ts +12 -9
  53. package/dist/schemas/outputs.d.ts.map +1 -1
  54. package/dist/schemas/outputs.js +10 -3
  55. package/dist/schemas/outputs.js.map +1 -1
  56. package/dist/schemas/validators.d.ts +12 -0
  57. package/dist/schemas/validators.d.ts.map +1 -0
  58. package/dist/schemas/validators.js +35 -0
  59. package/dist/schemas/validators.js.map +1 -0
  60. package/dist/server.d.ts +3 -2
  61. package/dist/server.d.ts.map +1 -1
  62. package/dist/server.js +26 -15
  63. package/dist/server.js.map +1 -1
  64. package/dist/tools/analyze-directory.js +1 -1
  65. package/dist/tools/analyze-directory.js.map +1 -1
  66. package/dist/tools/directory-tree.js +1 -1
  67. package/dist/tools/directory-tree.js.map +1 -1
  68. package/dist/tools/list-directory.js +1 -1
  69. package/dist/tools/list-directory.js.map +1 -1
  70. package/dist/tools/read-file.d.ts.map +1 -1
  71. package/dist/tools/read-file.js +3 -0
  72. package/dist/tools/read-file.js.map +1 -1
  73. package/dist/tools/read-multiple-files.d.ts.map +1 -1
  74. package/dist/tools/read-multiple-files.js +3 -0
  75. package/dist/tools/read-multiple-files.js.map +1 -1
  76. package/dist/tools/search-content.d.ts.map +1 -1
  77. package/dist/tools/search-content.js +4 -3
  78. package/dist/tools/search-content.js.map +1 -1
  79. 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, MIME_TYPES, PARALLEL_CONCURRENCY, REGEX_MATCH_TIMEOUT_MS, } from './constants.js';
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 shouldStopBecauseOfTimeout(deadlineMs) {
13
- return deadlineMs !== undefined && Date.now() > deadlineMs;
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; // Potentially dangerous, needs full check
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; // High repetition count, needs full check
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 = MIME_TYPES[ext] ?? undefined;
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 skippedSymlinks = 0;
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
- skippedSymlinks++;
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
- skippedSymlinks,
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, // Security: never follow symlinks
315
- deep: maxDepth, // Limit search depth if specified
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, // Security: never follow symlinks
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 (shouldStopBecauseOfTimeout(deadlineMs)) {
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 (shouldStopBecauseOfTimeout(deadlineMs)) {
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
- // Still add to buffer for context
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
- if (contextLines > 0 && lineBuffer.length > 0) {
540
- newMatch.contextBefore = [...lineBuffer];
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 (contextLines > 0) {
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 (error) {
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 skippedSymlinks = 0;
606
+ let symlinksNotFollowed = 0;
605
607
  const fileTypes = {};
606
608
  const largestFiles = [];
607
609
  const recentlyModified = [];
608
- const excludeMatchers = excludePatterns.length > 0
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
- skippedSymlinks++;
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
- skippedSymlinks++;
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
- skippedSymlinks,
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 skippedSymlinks = 0;
717
+ let symlinksNotFollowed = 0;
730
718
  let truncated = false;
731
- const excludeMatchers = excludePatterns.length > 0
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
- skippedSymlinks++;
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
- skippedSymlinks++;
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
- skippedSymlinks,
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 = MIME_TYPES[ext] ?? 'application/octet-stream';
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