@j0hanz/filesystem-mcp 1.14.0 → 1.14.1

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 (66) hide show
  1. package/README.md +47 -47
  2. package/dist/cli.js +2 -2
  3. package/dist/completions.js +54 -51
  4. package/dist/config.d.ts +13 -13
  5. package/dist/config.js +12 -12
  6. package/dist/index.js +1 -1
  7. package/dist/lib/abort.d.ts +7 -0
  8. package/dist/lib/abort.js +81 -0
  9. package/dist/lib/constants.d.ts +3 -1
  10. package/dist/lib/constants.js +8 -2
  11. package/dist/lib/errors.d.ts +7 -3
  12. package/dist/lib/errors.js +59 -39
  13. package/dist/lib/file-operations/core.d.ts +3 -3
  14. package/dist/lib/file-operations/core.js +23 -20
  15. package/dist/lib/file-operations/metadata.d.ts +2 -2
  16. package/dist/lib/file-operations/metadata.js +60 -19
  17. package/dist/lib/file-operations/search.js +83 -84
  18. package/dist/lib/file-operations/traversal.js +13 -15
  19. package/dist/lib/fs-helpers.d.ts +3 -10
  20. package/dist/lib/fs-helpers.js +20 -98
  21. package/dist/lib/globs.js +1 -1
  22. package/dist/lib/logger.d.ts +28 -0
  23. package/dist/lib/logger.js +91 -0
  24. package/dist/lib/observability.d.ts +7 -0
  25. package/dist/lib/observability.js +19 -9
  26. package/dist/lib/paths.js +55 -55
  27. package/dist/lib/resource-store.js +4 -4
  28. package/dist/lib/utils.d.ts +0 -12
  29. package/dist/lib/utils.js +0 -13
  30. package/dist/resources/generated-instructions.js +40 -31
  31. package/dist/resources/tool-catalog.js +32 -26
  32. package/dist/resources/tool-info.js +34 -29
  33. package/dist/resources/workflows.js +39 -18
  34. package/dist/resources.d.ts +1 -1
  35. package/dist/resources.js +4 -4
  36. package/dist/schemas.d.ts +66 -66
  37. package/dist/schemas.js +21 -44
  38. package/dist/server/bootstrap.d.ts +12 -11
  39. package/dist/server/bootstrap.js +95 -86
  40. package/dist/server/roots-manager.d.ts +5 -2
  41. package/dist/server/roots-manager.js +8 -6
  42. package/dist/server/task-store.d.ts +10 -0
  43. package/dist/server/task-store.js +73 -0
  44. package/dist/tools/apply-patch.js +26 -18
  45. package/dist/tools/calculate-hash.js +11 -22
  46. package/dist/tools/create-directory.js +10 -8
  47. package/dist/tools/delete-file.js +17 -15
  48. package/dist/tools/diff-files.js +15 -15
  49. package/dist/tools/edit-file.js +7 -4
  50. package/dist/tools/list-directory.js +6 -6
  51. package/dist/tools/move-file.js +98 -81
  52. package/dist/tools/read-multiple.js +4 -4
  53. package/dist/tools/read.js +5 -5
  54. package/dist/tools/replace-in-files.js +20 -23
  55. package/dist/tools/roots.js +1 -1
  56. package/dist/tools/search-content.js +32 -41
  57. package/dist/tools/search-files.js +54 -41
  58. package/dist/tools/shared.d.ts +3 -0
  59. package/dist/tools/shared.js +70 -28
  60. package/dist/tools/stat-many.js +11 -7
  61. package/dist/tools/stat.js +4 -4
  62. package/dist/tools/task-support.d.ts +10 -9
  63. package/dist/tools/task-support.js +94 -23
  64. package/dist/tools/tree.js +3 -3
  65. package/dist/tools/write-file.js +10 -7
  66. package/package.json +9 -9
package/README.md CHANGED
@@ -577,17 +577,17 @@ _No parameters._
577
577
 
578
578
  List immediate directory contents: name, path, type, size, modified date.
579
579
 
580
- | Parameter | Type | Required | Description |
581
- | ----------------------- | ------- | -------- | ------------------------------------------------------------ |
582
- | `path` | string | no | Base directory (default: root) |
583
- | `includeHidden` | boolean | no | Include dotfiles. Default: `false` |
584
- | `includeIgnored` | boolean | no | Include ignored items (node_modules, .git). Default: `false` |
585
- | `maxDepth` | integer | no | Max recursion depth (1-100) when pattern is provided |
586
- | `maxEntries` | integer | no | Max entries before truncation. Default: 1000, Max: 10000 |
587
- | `sortBy` | enum | no | `name` \| `size` \| `modified` \| `type`. Default: `name` |
588
- | `pattern` | string | no | Glob filter (e.g. `**/*.ts`) |
589
- | `includeSymlinkTargets` | boolean | no | Resolve symlink targets. Default: `false` |
590
- | `cursor` | string | no | Pagination cursor from a previous response |
580
+ | Parameter | Type | Required | Description |
581
+ | ----------------------- | ------- | -------- | --------------------------------------------------------------------------- |
582
+ | `path` | string | no | Base directory (default: root) |
583
+ | `includeHidden` | boolean | no | Include dotfiles. Default: `false` |
584
+ | `includeIgnored` | boolean | no | Include ignored items (node_modules, .git). Default: `false` |
585
+ | `maxDepth` | integer | no | Max recursion depth (1-100) when pattern is provided |
586
+ | `maxEntries` | integer | no | Max entries before truncation. Default: 1000, Max: 10000 |
587
+ | `sortBy` | enum | no | `name` \| `size` \| `modified` \| `type`. Default: `name` |
588
+ | `pattern` | string | no | Relative glob filter (e.g. `**/*.ts`). Absolute paths and `..` are rejected |
589
+ | `includeSymlinkTargets` | boolean | no | Resolve symlink targets. Default: `false` |
590
+ | `cursor` | string | no | Pagination cursor from a previous response |
591
591
 
592
592
  ---
593
593
 
@@ -595,16 +595,16 @@ List immediate directory contents: name, path, type, size, modified date.
595
595
 
596
596
  Find files by glob pattern. Returns matching files with metadata.
597
597
 
598
- | Parameter | Type | Required | Description |
599
- | ---------------- | ------- | -------- | --------------------------------------------------------- |
600
- | `path` | string | no | Base directory (default: root) |
601
- | `pattern` | string | **yes** | Glob pattern (e.g. `**/*.ts`) |
602
- | `maxResults` | integer | no | Max results (1-100000). Default: 1000 |
603
- | `includeIgnored` | boolean | no | Include ignored items. Default: `false` |
604
- | `includeHidden` | boolean | no | Include dotfiles. Default: `false` |
605
- | `sortBy` | enum | no | `path` \| `name` \| `size` \| `modified`. Default: `path` |
606
- | `maxDepth` | integer | no | Max directory depth (0-1000) |
607
- | `cursor` | string | no | Pagination cursor |
598
+ | Parameter | Type | Required | Description |
599
+ | ---------------- | ------- | -------- | ---------------------------------------------------------------------------- |
600
+ | `path` | string | no | Base directory (default: root) |
601
+ | `pattern` | string | **yes** | Relative glob pattern (e.g. `**/*.ts`). Absolute paths and `..` are rejected |
602
+ | `maxResults` | integer | no | Max results (1-100000). Default: 1000 |
603
+ | `includeIgnored` | boolean | no | Include ignored items. Default: `false` |
604
+ | `includeHidden` | boolean | no | Include dotfiles. Default: `false` |
605
+ | `sortBy` | enum | no | `path` \| `name` \| `size` \| `modified`. Default: `path` |
606
+ | `maxDepth` | integer | no | Max directory depth (0-1000) |
607
+ | `cursor` | string | no | Pagination cursor |
608
608
 
609
609
  ---
610
610
 
@@ -676,18 +676,18 @@ Get metadata for multiple files/directories in one request.
676
676
 
677
677
  Search file contents (grep-like). Returns matching lines with optional context.
678
678
 
679
- | Parameter | Type | Required | Description |
680
- | ---------------- | ------- | -------- | ------------------------------------------------ |
681
- | `path` | string | no | Base directory (default: root) |
682
- | `pattern` | string | **yes** | Search text or RE2 regex when `isRegex=true` |
683
- | `isRegex` | boolean | no | Treat pattern as RE2 regex. Default: `false` |
684
- | `caseSensitive` | boolean | no | Case-sensitive matching. Default: `false` |
685
- | `wholeWord` | boolean | no | Match whole words only. Default: `false` |
686
- | `contextLines` | integer | no | Lines of context before/after (0-50). Default: 0 |
687
- | `maxResults` | integer | no | Max match rows (1-100000). Default: 100 |
688
- | `filePattern` | string | no | Glob for candidate files (e.g. `**/*.ts`) |
689
- | `includeHidden` | boolean | no | Include dotfiles. Default: `false` |
690
- | `includeIgnored` | boolean | no | Include ignored items. Default: `false` |
679
+ | Parameter | Type | Required | Description |
680
+ | ---------------- | ------- | -------- | ---------------------------------------------------------------------------------------- |
681
+ | `path` | string | no | Base directory (default: root) |
682
+ | `pattern` | string | **yes** | Search text or RE2 regex when `isRegex=true` |
683
+ | `isRegex` | boolean | no | Treat pattern as RE2 regex. Default: `false` |
684
+ | `caseSensitive` | boolean | no | Case-sensitive matching. Default: `false` |
685
+ | `wholeWord` | boolean | no | Match whole words only. Default: `false` |
686
+ | `contextLines` | integer | no | Lines of context before/after (0-50). Default: 0 |
687
+ | `maxResults` | integer | no | Max match rows (1-100000). Default: 100 |
688
+ | `filePattern` | string | no | Relative glob for candidate files (e.g. `**/*.ts`). Absolute paths and `..` are rejected |
689
+ | `includeHidden` | boolean | no | Include dotfiles. Default: `false` |
690
+ | `includeIgnored` | boolean | no | Include ignored items. Default: `false` |
691
691
 
692
692
  ---
693
693
 
@@ -720,7 +720,7 @@ Apply sequential literal string replacements (first occurrence per edit). Use `d
720
720
  | Parameter | Type | Required | Description |
721
721
  | ------------------ | ------- | -------- | ---------------------------------------------------------- |
722
722
  | `path` | string | **yes** | Absolute path to file |
723
- | `edits` | array | **yes** | List of `{oldText, newText}` replacements |
723
+ | `edits` | array | **yes** | List of non-empty `{oldText, newText}` replacements |
724
724
  | `dryRun` | boolean | no | Preview edits without writing. Default: `false` |
725
725
  | `ignoreWhitespace` | boolean | no | Treat whitespace sequences as equivalent. Default: `false` |
726
726
 
@@ -792,19 +792,19 @@ Apply a unified diff patch to one or more files. Single-file: throws on failure.
792
792
 
793
793
  Bulk search-and-replace across files matching a glob. Replaces **all** occurrences per file. Always `dryRun: true` first.
794
794
 
795
- | Parameter | Type | Required | Description |
796
- | ---------------- | ------- | -------- | --------------------------------------------------- |
797
- | `path` | string | no | Base directory (default: root) |
798
- | `filePattern` | string | **yes** | Glob pattern (e.g. `**/*.ts`) |
799
- | `searchPattern` | string | **yes** | Text to search. RE2 regex when `isRegex=true` |
800
- | `replacement` | string | **yes** | Replacement text. Supports `$1`, `$2` with regex |
801
- | `isRegex` | boolean | no | Treat as RE2 regex. Default: `false` |
802
- | `dryRun` | boolean | no | Preview matches with diff. Default: `false` |
803
- | `includeHidden` | boolean | no | Include dotfiles. Default: `false` |
804
- | `includeIgnored` | boolean | no | Include ignored items. Default: `false` |
805
- | `returnDiff` | boolean | no | Return diff even when not dry-run. Default: `false` |
806
- | `maxFiles` | integer | no | Max files to process before stopping (1-10000) |
807
- | `caseSensitive` | boolean | no | Case-sensitive matching. Default: `true` |
795
+ | Parameter | Type | Required | Description |
796
+ | ---------------- | ------- | -------- | ---------------------------------------------------------------------------- |
797
+ | `path` | string | no | Base directory (default: root) |
798
+ | `filePattern` | string | **yes** | Relative glob pattern (e.g. `**/*.ts`). Absolute paths and `..` are rejected |
799
+ | `searchPattern` | string | **yes** | Text to search. RE2 regex when `isRegex=true` |
800
+ | `replacement` | string | **yes** | Replacement text. Supports `$1`, `$2` with regex |
801
+ | `isRegex` | boolean | no | Treat as RE2 regex. Default: `false` |
802
+ | `dryRun` | boolean | no | Preview matches with diff. Default: `false` |
803
+ | `includeHidden` | boolean | no | Include dotfiles. Default: `false` |
804
+ | `includeIgnored` | boolean | no | Include ignored items. Default: `false` |
805
+ | `returnDiff` | boolean | no | Return diff even when not dry-run. Default: `false` |
806
+ | `maxFiles` | integer | no | Max files to process before stopping (1-10000) |
807
+ | `caseSensitive` | boolean | no | Case-sensitive matching. Default: `true` |
808
808
 
809
809
  ### Resources
810
810
 
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import * as fs from 'node:fs/promises';
1
+ import { stat } from 'node:fs/promises';
2
2
  import { getSystemErrorMessage, getSystemErrorName } from 'node:util';
3
3
  import { Command, CommanderError, InvalidArgumentError } from 'commander';
4
4
  import { processInParallel } from './lib/fs-helpers.js';
@@ -83,7 +83,7 @@ function assertDirectory(stats, inputPath) {
83
83
  async function validateDirectoryPath(inputPath) {
84
84
  const normalized = normalizePath(inputPath);
85
85
  try {
86
- const stats = await fs.stat(normalized);
86
+ const stats = await stat(normalized);
87
87
  assertDirectory(stats, inputPath);
88
88
  return normalized;
89
89
  }
@@ -1,6 +1,6 @@
1
- import * as fs from 'node:fs/promises';
2
- import * as path from 'node:path';
3
1
  import { CompleteRequestSchema } from '@modelcontextprotocol/sdk/types.js';
2
+ import { readdir, stat } from 'node:fs/promises';
3
+ import { basename, dirname, isAbsolute, join, parse, resolve, sep, } from 'node:path';
4
4
  import { getAllowedDirectories, isPathWithinDirectories, normalizePath, toPosixPath, } from './lib/paths.js';
5
5
  import { isRecord } from './lib/utils.js';
6
6
  import { getSortedToolContracts } from './resources/tool-info.js';
@@ -191,21 +191,21 @@ function rememberCompletionCacheValue(cache, key, value) {
191
191
  }
192
192
  }
193
193
  function hasTrailingSeparator(value) {
194
- return (value.endsWith(path.sep) || value.endsWith('/') || value.endsWith('\\'));
194
+ return value.endsWith(sep) || value.endsWith('/') || value.endsWith('\\');
195
195
  }
196
196
  function isAbsolutePathInput(value) {
197
- return (path.isAbsolute(value) ||
197
+ return (isAbsolute(value) ||
198
198
  /^[A-Za-z]:[\\/]/u.test(value) ||
199
199
  value.startsWith('\\\\'));
200
200
  }
201
201
  function resolveFromBase(base, rawValue, trailingSeparator) {
202
- const normalizedValue = normalizePath(path.resolve(base, rawValue));
202
+ const normalizedValue = normalizePath(resolve(base, rawValue));
203
203
  if (trailingSeparator) {
204
204
  return { searchDir: normalizedValue, prefix: '' };
205
205
  }
206
206
  return {
207
- searchDir: path.dirname(normalizedValue),
208
- prefix: path.basename(normalizedValue),
207
+ searchDir: dirname(normalizedValue),
208
+ prefix: basename(normalizedValue),
209
209
  };
210
210
  }
211
211
  function resolveNamedRootContext(currentValue, allowed) {
@@ -225,18 +225,18 @@ function resolveNamedRootPath(value, allowed) {
225
225
  const root = findAllowedRootByName(parsed.rootName, allowed);
226
226
  if (!root)
227
227
  return undefined;
228
- return normalizePath(path.resolve(root, parsed.remainder));
228
+ return normalizePath(resolve(root, parsed.remainder));
229
229
  }
230
230
  function parseNamedRootInput(value) {
231
231
  const normalizedInput = toPosixPath(value);
232
232
  const [rootName, ...rest] = normalizedInput.split('/');
233
233
  if (!rootName)
234
234
  return undefined;
235
- return { rootName, remainder: rest.join(path.sep) };
235
+ return { rootName, remainder: rest.join(sep) };
236
236
  }
237
237
  function findAllowedRootByName(rootName, allowed) {
238
238
  const normalizedRootName = rootName.toLowerCase();
239
- return allowed.find((candidate) => path.basename(candidate).toLowerCase() === normalizedRootName);
239
+ return allowed.find((candidate) => basename(candidate).toLowerCase() === normalizedRootName);
240
240
  }
241
241
  function chooseContextKeys(argumentName) {
242
242
  const normalized = argumentName.toLowerCase();
@@ -263,7 +263,7 @@ function resolveContextCandidatePath(candidate, allowed) {
263
263
  const base = allowed[0];
264
264
  if (!base)
265
265
  return undefined;
266
- return normalizePath(path.resolve(base, candidate));
266
+ return normalizePath(resolve(base, candidate));
267
267
  }
268
268
  return resolveNamedRootPath(candidate, allowed);
269
269
  }
@@ -271,14 +271,14 @@ async function toAllowedContextDirectory(resolved, allowed) {
271
271
  if (!isPathWithinDirectories(resolved, allowed))
272
272
  return undefined;
273
273
  try {
274
- const stats = await fs.stat(resolved);
274
+ const stats = await stat(resolved);
275
275
  if (stats.isDirectory())
276
276
  return resolved;
277
277
  }
278
278
  catch {
279
279
  // Fall back to parent path best-effort resolution.
280
280
  }
281
- const parent = path.dirname(resolved);
281
+ const parent = dirname(resolved);
282
282
  return isPathWithinDirectories(parent, allowed) ? parent : undefined;
283
283
  }
284
284
  async function resolveContextBaseDirectory(argumentName, contextArguments, allowed) {
@@ -300,7 +300,7 @@ async function resolveContextBaseDirectory(argumentName, contextArguments, allow
300
300
  return undefined;
301
301
  }
302
302
  function withDirectorySeparator(value) {
303
- return value.endsWith(path.sep) ? value : `${value}${path.sep}`;
303
+ return value.endsWith(sep) ? value : `${value}${sep}`;
304
304
  }
305
305
  function buildCompletionResult(values) {
306
306
  return {
@@ -314,8 +314,8 @@ function buildCompletionResponse(result) {
314
314
  }
315
315
  function sortCompletionMatches(matches) {
316
316
  matches.sort((left, right) => {
317
- const leftIsDir = left.endsWith(path.sep);
318
- const rightIsDir = right.endsWith(path.sep);
317
+ const leftIsDir = left.endsWith(sep);
318
+ const rightIsDir = right.endsWith(sep);
319
319
  if (leftIsDir && !rightIsDir)
320
320
  return -1;
321
321
  if (!leftIsDir && rightIsDir)
@@ -350,7 +350,7 @@ function collectAllowedRoots(allowed, predicate) {
350
350
  function getSearchContext(currentValue, allowed, contextBase) {
351
351
  const trailingSeparator = hasTrailingSeparator(currentValue);
352
352
  if (isAbsolutePathInput(currentValue)) {
353
- return resolveFromBase(path.parse(currentValue).root || path.sep, currentValue, trailingSeparator);
353
+ return resolveFromBase(parse(currentValue).root || sep, currentValue, trailingSeparator);
354
354
  }
355
355
  const namedRootContext = resolveNamedRootContext(currentValue, allowed);
356
356
  if (namedRootContext) {
@@ -376,13 +376,13 @@ async function findMatchesInDirectory(searchDir, prefix, allowed) {
376
376
  return matches;
377
377
  }
378
378
  try {
379
- const entries = await fs.readdir(searchDir, { withFileTypes: true });
379
+ const entries = await readdir(searchDir, { withFileTypes: true });
380
380
  const lowerPrefix = prefix.toLowerCase();
381
381
  for (const entry of entries) {
382
382
  if (entry.name.toLowerCase().startsWith(lowerPrefix)) {
383
- const fullPath = path.join(searchDir, entry.name);
383
+ const fullPath = join(searchDir, entry.name);
384
384
  const isDir = entry.isDirectory();
385
- matches.push(isDir ? `${fullPath}${path.sep}` : fullPath);
385
+ matches.push(isDir ? `${fullPath}${sep}` : fullPath);
386
386
  }
387
387
  }
388
388
  }
@@ -396,17 +396,17 @@ function findRootPrefixMatches(currentValue, allowed) {
396
396
  if (!rootPrefix) {
397
397
  return collectAllowedRoots(allowed, () => true);
398
398
  }
399
- return collectAllowedRoots(allowed, (root) => path.basename(root).toLowerCase().startsWith(rootPrefix));
399
+ return collectAllowedRoots(allowed, (root) => basename(root).toLowerCase().startsWith(rootPrefix));
400
400
  }
401
401
  function findMatchingRoots(searchDir, prefix, allowed) {
402
402
  const lowerPrefix = prefix.toLowerCase();
403
403
  const normalizedSearchDir = normalizePath(searchDir);
404
404
  return collectAllowedRoots(allowed, (root) => {
405
- const rootDir = path.dirname(root);
405
+ const rootDir = dirname(root);
406
406
  // Check if root is a direct child of searchDir
407
407
  if (normalizePath(rootDir) !== normalizedSearchDir)
408
408
  return false;
409
- return path.basename(root).toLowerCase().startsWith(lowerPrefix);
409
+ return basename(root).toLowerCase().startsWith(lowerPrefix);
410
410
  });
411
411
  }
412
412
  async function getPathCompletions(currentValue, options = {}) {
@@ -430,6 +430,34 @@ async function getPathCompletions(currentValue, options = {}) {
430
430
  return { values: [] };
431
431
  }
432
432
  }
433
+ function handleTopicAndToolCompletions(ref, argName, argumentValue, topicValues, toolNameValues) {
434
+ if (!isRecord(ref))
435
+ return undefined;
436
+ const currentValue = argumentValue.toLowerCase();
437
+ if (ref['type'] === 'ref/prompt' && argName === 'topic') {
438
+ const filtered = currentValue
439
+ ? topicValues.filter((v) => v.startsWith(currentValue))
440
+ : topicValues;
441
+ return buildCompletionResponse(buildCompletionResult(filtered));
442
+ }
443
+ if (ref['type'] === 'ref/prompt' &&
444
+ ref['name'] === 'get-tool-help' &&
445
+ argName === 'name') {
446
+ const filtered = currentValue
447
+ ? toolNameValues.filter((value) => value.startsWith(currentValue))
448
+ : toolNameValues;
449
+ return buildCompletionResponse(buildCompletionResult(filtered));
450
+ }
451
+ if (ref['type'] === 'ref/resource' &&
452
+ ref['uri'] === 'internal://tool-info/{name}' &&
453
+ argName === 'name') {
454
+ const filtered = currentValue
455
+ ? toolNameValues.filter((value) => value.startsWith(currentValue))
456
+ : toolNameValues;
457
+ return buildCompletionResponse(buildCompletionResult(filtered));
458
+ }
459
+ return undefined;
460
+ }
433
461
  export function registerCompletions(server, instructions = '') {
434
462
  const topicValues = extractTopicCompletions(instructions);
435
463
  const toolNameValues = extractToolNameCompletions();
@@ -437,34 +465,9 @@ export function registerCompletions(server, instructions = '') {
437
465
  const { params } = request;
438
466
  const { argument, ref } = params;
439
467
  const argName = argument.name.toLowerCase();
440
- // Handle prompt topic completions
441
- if (isRecord(ref) && ref['type'] === 'ref/prompt' && argName === 'topic') {
442
- const currentValue = argument.value.toLowerCase();
443
- const filtered = currentValue
444
- ? topicValues.filter((v) => v.startsWith(currentValue))
445
- : topicValues;
446
- return buildCompletionResponse(buildCompletionResult(filtered));
447
- }
448
- if (isRecord(ref) &&
449
- ref['type'] === 'ref/prompt' &&
450
- ref['name'] === 'get-tool-help' &&
451
- argName === 'name') {
452
- const currentValue = argument.value.toLowerCase();
453
- const filtered = currentValue
454
- ? toolNameValues.filter((value) => value.startsWith(currentValue))
455
- : toolNameValues;
456
- return buildCompletionResponse(buildCompletionResult(filtered));
457
- }
458
- if (isRecord(ref) &&
459
- ref['type'] === 'ref/resource' &&
460
- ref['uri'] === 'internal://tool-info/{name}' &&
461
- argName === 'name') {
462
- const currentValue = argument.value.toLowerCase();
463
- const filtered = currentValue
464
- ? toolNameValues.filter((value) => value.startsWith(currentValue))
465
- : toolNameValues;
466
- return buildCompletionResponse(buildCompletionResult(filtered));
467
- }
468
+ const predef = handleTopicAndToolCompletions(ref, argName, argument.value, topicValues, toolNameValues);
469
+ if (predef)
470
+ return predef;
468
471
  const enumResult = getEnumCompletions(argName, argument.value);
469
472
  if (enumResult) {
470
473
  return buildCompletionResponse(enumResult);
package/dist/config.d.ts CHANGED
@@ -83,7 +83,7 @@ export interface SearchContentResult {
83
83
  export interface MultipleFileInfoResult {
84
84
  readonly path: string;
85
85
  readonly info?: FileInfo;
86
- readonly error?: string;
86
+ readonly error?: Error;
87
87
  }
88
88
  export interface GetMultipleFileInfoResult {
89
89
  readonly results: readonly MultipleFileInfoResult[];
@@ -95,18 +95,18 @@ export interface GetMultipleFileInfoResult {
95
95
  };
96
96
  }
97
97
  export declare const ErrorCode: {
98
- readonly E_ACCESS_DENIED: "E_ACCESS_DENIED";
99
- readonly E_NOT_FOUND: "E_NOT_FOUND";
100
- readonly E_NOT_FILE: "E_NOT_FILE";
101
- readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
102
- readonly E_TOO_LARGE: "E_TOO_LARGE";
103
- readonly E_TIMEOUT: "E_TIMEOUT";
104
- readonly E_CANCELLED: "E_CANCELLED";
105
- readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
106
- readonly E_INVALID_INPUT: "E_INVALID_INPUT";
107
- readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
108
- readonly E_SYMLINK_NOT_ALLOWED: "E_SYMLINK_NOT_ALLOWED";
109
- readonly E_UNKNOWN: "E_UNKNOWN";
98
+ readonly ACCESS_DENIED: "ACCESS_DENIED";
99
+ readonly NOT_FOUND: "NOT_FOUND";
100
+ readonly NOT_FILE: "NOT_FILE";
101
+ readonly NOT_DIRECTORY: "NOT_DIRECTORY";
102
+ readonly TOO_LARGE: "TOO_LARGE";
103
+ readonly TIMEOUT: "TIMEOUT";
104
+ readonly CANCELLED: "CANCELLED";
105
+ readonly INVALID_PATTERN: "INVALID_PATTERN";
106
+ readonly INVALID_INPUT: "INVALID_INPUT";
107
+ readonly PERMISSION_DENIED: "PERMISSION_DENIED";
108
+ readonly SYMLINK_NOT_ALLOWED: "SYMLINK_NOT_ALLOWED";
109
+ readonly UNKNOWN: "UNKNOWN";
110
110
  };
111
111
  export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
112
112
  export declare function formatBytes(bytes: number): string;
package/dist/config.js CHANGED
@@ -1,16 +1,16 @@
1
1
  export const ErrorCode = {
2
- E_ACCESS_DENIED: 'E_ACCESS_DENIED',
3
- E_NOT_FOUND: 'E_NOT_FOUND',
4
- E_NOT_FILE: 'E_NOT_FILE',
5
- E_NOT_DIRECTORY: 'E_NOT_DIRECTORY',
6
- E_TOO_LARGE: 'E_TOO_LARGE',
7
- E_TIMEOUT: 'E_TIMEOUT',
8
- E_CANCELLED: 'E_CANCELLED',
9
- E_INVALID_PATTERN: 'E_INVALID_PATTERN',
10
- E_INVALID_INPUT: 'E_INVALID_INPUT',
11
- E_PERMISSION_DENIED: 'E_PERMISSION_DENIED',
12
- E_SYMLINK_NOT_ALLOWED: 'E_SYMLINK_NOT_ALLOWED',
13
- E_UNKNOWN: 'E_UNKNOWN',
2
+ ACCESS_DENIED: 'ACCESS_DENIED',
3
+ NOT_FOUND: 'NOT_FOUND',
4
+ NOT_FILE: 'NOT_FILE',
5
+ NOT_DIRECTORY: 'NOT_DIRECTORY',
6
+ TOO_LARGE: 'TOO_LARGE',
7
+ TIMEOUT: 'TIMEOUT',
8
+ CANCELLED: 'CANCELLED',
9
+ INVALID_PATTERN: 'INVALID_PATTERN',
10
+ INVALID_INPUT: 'INVALID_INPUT',
11
+ PERMISSION_DENIED: 'PERMISSION_DENIED',
12
+ SYMLINK_NOT_ALLOWED: 'SYMLINK_NOT_ALLOWED',
13
+ UNKNOWN: 'UNKNOWN',
14
14
  };
15
15
  const BYTE_UNIT_LABELS = ['B', 'KB', 'MB', 'GB', 'TB'];
16
16
  export function formatBytes(bytes) {
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import process from 'node:process';
3
+ import { createTimedAbortSignal } from './lib/abort.js';
3
4
  import { DEFAULT_SEARCH_TIMEOUT_MS } from './lib/constants.js';
4
5
  import { formatUnknownErrorMessage } from './lib/errors.js';
5
- import { createTimedAbortSignal } from './lib/fs-helpers.js';
6
6
  import { setAllowedDirectoriesResolved } from './lib/paths.js';
7
7
  import { CliExitError, parseArgs } from './cli.js';
8
8
  import { createServer, startHttpServer, startServer } from './server.js';
@@ -0,0 +1,7 @@
1
+ export declare function assertNotAborted(signal?: AbortSignal, message?: string): void;
2
+ export declare function withAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T>;
3
+ export declare function createTimedAbortSignal(baseSignal: AbortSignal | undefined, timeoutMs?: number): {
4
+ signal: AbortSignal;
5
+ cleanup: () => void;
6
+ };
7
+ export declare function withTimedAbortSignal<T>(baseSignal: AbortSignal | undefined, timeoutMs: number | undefined, run: (signal: AbortSignal) => Promise<T>): Promise<T>;
@@ -0,0 +1,81 @@
1
+ import { normalizeUnknownError } from './errors.js';
2
+ function createAbortError(message = 'Operation aborted') {
3
+ return new DOMException(message, 'AbortError');
4
+ }
5
+ const SHARED_NOOP_SIGNAL = new AbortController().signal;
6
+ function normalizeAbortReason(reason, message) {
7
+ if (reason instanceof Error)
8
+ return reason;
9
+ return createAbortError(message);
10
+ }
11
+ function isFiniteNumber(value) {
12
+ return typeof value === 'number' && Number.isFinite(value);
13
+ }
14
+ export function assertNotAborted(signal, message) {
15
+ if (!signal)
16
+ return;
17
+ try {
18
+ signal.throwIfAborted();
19
+ }
20
+ catch (reason) {
21
+ throw normalizeAbortReason(reason, message);
22
+ }
23
+ }
24
+ function getAbortError(signal, message) {
25
+ try {
26
+ signal.throwIfAborted();
27
+ }
28
+ catch (reason) {
29
+ return normalizeAbortReason(reason, message);
30
+ }
31
+ return createAbortError(message);
32
+ }
33
+ export function withAbort(promise, signal) {
34
+ if (!signal)
35
+ return promise;
36
+ signal.throwIfAborted();
37
+ return new Promise((resolve, reject) => {
38
+ const onAbort = () => {
39
+ reject(getAbortError(signal));
40
+ };
41
+ if (signal.aborted) {
42
+ onAbort();
43
+ return;
44
+ }
45
+ signal.addEventListener('abort', onAbort, { once: true });
46
+ promise.then((value) => {
47
+ signal.removeEventListener('abort', onAbort);
48
+ resolve(value);
49
+ }, (error) => {
50
+ signal.removeEventListener('abort', onAbort);
51
+ reject(normalizeUnknownError(error));
52
+ });
53
+ });
54
+ }
55
+ export function createTimedAbortSignal(baseSignal, timeoutMs) {
56
+ const timeoutSignal = isFiniteNumber(timeoutMs)
57
+ ? AbortSignal.timeout(timeoutMs)
58
+ : undefined;
59
+ if (baseSignal && timeoutSignal) {
60
+ return {
61
+ signal: AbortSignal.any([baseSignal, timeoutSignal]),
62
+ cleanup: () => { },
63
+ };
64
+ }
65
+ if (baseSignal) {
66
+ return { signal: baseSignal, cleanup: () => { } };
67
+ }
68
+ if (timeoutSignal) {
69
+ return { signal: timeoutSignal, cleanup: () => { } };
70
+ }
71
+ return { signal: SHARED_NOOP_SIGNAL, cleanup: () => { } };
72
+ }
73
+ export async function withTimedAbortSignal(baseSignal, timeoutMs, run) {
74
+ const { signal, cleanup } = createTimedAbortSignal(baseSignal, timeoutMs);
75
+ try {
76
+ return await run(signal);
77
+ }
78
+ finally {
79
+ cleanup();
80
+ }
81
+ }
@@ -1,9 +1,11 @@
1
1
  export declare function parseTrueEnvFlag(value: string | undefined): boolean;
2
2
  export declare function parseEnvInt(envVar: string, defaultValue: number, min: number, max: number): number;
3
- export declare const DEFAULT_LOG_LEVEL: "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency";
3
+ export declare const DEFAULT_LOG_LEVEL: "error" | "debug" | "info" | "notice" | "warning" | "critical" | "alert" | "emergency";
4
4
  export declare const DEFAULT_TASK_TTL_MS: number;
5
5
  export declare const MAX_TASK_TTL_MS: number;
6
6
  export declare const MAX_CONCURRENT_TASKS: number;
7
+ export declare const TASK_CANCEL_POLL_MS = 2000;
8
+ export declare const TASK_POLL_INTERVAL_MS = 100;
7
9
  export declare const PARALLEL_CONCURRENCY: number;
8
10
  export declare const MAX_SEARCHABLE_FILE_SIZE: number;
9
11
  export declare const MAX_TEXT_FILE_SIZE: number;
@@ -1,4 +1,5 @@
1
1
  import { availableParallelism } from 'node:os';
2
+ import { Logger } from './logger.js';
2
3
  const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes', 'y', 'on']);
3
4
  const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'n', 'off']);
4
5
  export function parseTrueEnvFlag(value) {
@@ -9,7 +10,7 @@ export function parseTrueEnvFlag(value) {
9
10
  const KIB = 1024;
10
11
  const MIB = 1024 * KIB;
11
12
  function logInvalidEnvValue(envVar, value, expected, defaultValue) {
12
- console.error(`[WARNING] Invalid ${envVar} value: ${value} (must be ${expected}). Using default: ${String(defaultValue)}`);
13
+ Logger.warn(`Invalid ${envVar} value: ${value} (must be ${expected}). Using default: ${String(defaultValue)}`);
13
14
  }
14
15
  // Helper for parsing environment variables (only used for configurable values)
15
16
  export function parseEnvInt(envVar, defaultValue, min, max) {
@@ -66,7 +67,7 @@ function parseEnvLogLevel(envVar, defaultValue) {
66
67
  if (VALID_LOG_LEVELS.includes(normalized)) {
67
68
  return normalized;
68
69
  }
69
- console.error(`[WARNING] Invalid ${envVar} value: ${value} (must be ${VALID_LOG_LEVELS.join('|')}). Using default: ${defaultValue}`);
70
+ Logger.warn(`Invalid ${envVar} value: ${value} (must be ${VALID_LOG_LEVELS.join('|')}). Using default: ${defaultValue}`);
70
71
  return defaultValue;
71
72
  }
72
73
  export const DEFAULT_LOG_LEVEL = parseEnvLogLevel('FILESYSTEM_MCP_LOG_LEVEL', 'info');
@@ -74,6 +75,11 @@ export const DEFAULT_LOG_LEVEL = parseEnvLogLevel('FILESYSTEM_MCP_LOG_LEVEL', 'i
74
75
  export const DEFAULT_TASK_TTL_MS = 5 * 60 * 1000;
75
76
  export const MAX_TASK_TTL_MS = parseEnvInt('FILESYSTEM_MCP_MAX_TASK_TTL_MS', 60 * 60 * 1000, 1_000, 24 * 60 * 60 * 1000);
76
77
  export const MAX_CONCURRENT_TASKS = parseEnvInt('FILESYSTEM_MCP_MAX_CONCURRENT_TASKS', 100, 1, 10_000);
78
+ // How often (ms) a background task checks the store for client cancellation.
79
+ export const TASK_CANCEL_POLL_MS = 2_000;
80
+ // Suggested poll interval (ms) returned to the SDK/client for automatic task polling.
81
+ // Filesystem operations complete quickly; a short interval avoids unnecessary latency.
82
+ export const TASK_POLL_INTERVAL_MS = 100;
77
83
  // Auto-tuned parallelism based on CPU cores (no env override)
78
84
  const BYTES_PER_PARALLEL_TASK = 64 * MIB;
79
85
  const BYTES_PER_SEARCH_WORKER = 128 * MIB;
@@ -15,9 +15,13 @@ export declare function isTimeoutLikeError(error: unknown): boolean;
15
15
  export declare class McpError extends Error {
16
16
  code: ErrorCode;
17
17
  path?: string | undefined;
18
- details?: Record<string, unknown> | undefined;
19
- constructor(code: ErrorCode, message: string, path?: string | undefined, details?: Record<string, unknown> | undefined, cause?: unknown);
18
+ details?: Record<string, unknown>;
19
+ constructor(code: ErrorCode, message: string, path?: string | undefined, details?: Record<string, unknown>, cause?: unknown);
20
+ static notFound(message: string, path?: string, details?: Record<string, unknown>, cause?: unknown): McpError;
21
+ static invalidInput(message: string, path?: string, details?: Record<string, unknown>, cause?: unknown): McpError;
22
+ static accessDenied(message: string, path?: string, details?: Record<string, unknown>, cause?: unknown): McpError;
23
+ static timeout(message: string, path?: string, details?: Record<string, unknown>, cause?: unknown): McpError;
20
24
  }
21
25
  export declare function createDetailedError(error: unknown, path?: string, additionalDetails?: Record<string, unknown>): DetailedError;
22
26
  export declare function formatDetailedError(error: DetailedError): string;
23
- export declare function getSuggestion(code: ErrorCode): string;
27
+ export declare function getSuggestion(code: ErrorCode): string | undefined;