@j0hanz/filesystem-mcp 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +635 -377
  2. package/dist/cli.js +2 -2
  3. package/dist/completions.js +2 -3
  4. package/dist/index.js +2 -2
  5. package/dist/lib/file-operations/{common.d.ts → core.d.ts} +6 -0
  6. package/dist/lib/file-operations/{common.js → core.js} +45 -0
  7. package/dist/lib/file-operations/metadata.d.ts +73 -0
  8. package/dist/lib/file-operations/metadata.js +889 -0
  9. package/dist/lib/file-operations/{search-content.d.ts → search.d.ts} +35 -2
  10. package/dist/lib/file-operations/{search-content.js → search.js} +413 -15
  11. package/dist/lib/file-operations/{glob-engine.d.ts → traversal.d.ts} +18 -1
  12. package/dist/lib/file-operations/{glob-engine.js → traversal.js} +25 -2
  13. package/dist/lib/fs-helpers.js +1 -2
  14. package/dist/lib/observability.js +1 -1
  15. package/dist/lib/{path-validation.d.ts → paths.d.ts} +3 -0
  16. package/dist/lib/{path-validation.js → paths.js} +105 -1
  17. package/dist/lib/utils.d.ts +15 -0
  18. package/dist/lib/utils.js +33 -0
  19. package/dist/resources/generated-instructions.js +6 -6
  20. package/dist/resources/tool-catalog.js +9 -9
  21. package/dist/resources/tool-info.js +3 -3
  22. package/dist/resources/workflows.js +10 -23
  23. package/dist/schemas.js +15 -15
  24. package/dist/server/bootstrap.d.ts +19 -1
  25. package/dist/server/bootstrap.js +103 -9
  26. package/dist/server/roots-manager.d.ts +2 -2
  27. package/dist/server/roots-manager.js +3 -3
  28. package/dist/tools/apply-patch.js +4 -3
  29. package/dist/tools/calculate-hash.js +4 -4
  30. package/dist/tools/create-directory.js +1 -1
  31. package/dist/tools/delete-file.js +3 -3
  32. package/dist/tools/diff-files.js +2 -2
  33. package/dist/tools/edit-file.js +14 -13
  34. package/dist/tools/list-directory.js +5 -7
  35. package/dist/tools/move-file.js +2 -1
  36. package/dist/tools/read-multiple.js +3 -4
  37. package/dist/tools/read.js +3 -3
  38. package/dist/tools/replace-in-files.js +8 -10
  39. package/dist/tools/roots.js +4 -6
  40. package/dist/tools/search-content.js +7 -9
  41. package/dist/tools/search-files.js +4 -6
  42. package/dist/tools/shared.d.ts +2 -1
  43. package/dist/tools/shared.js +2 -1
  44. package/dist/tools/stat-many.js +4 -3
  45. package/dist/tools/stat.js +4 -3
  46. package/dist/tools/task-support.js +1 -1
  47. package/dist/tools/tree.js +3 -4
  48. package/dist/tools/write-file.js +1 -1
  49. package/package.json +4 -4
  50. package/dist/lib/file-operations/file-info.d.ts +0 -10
  51. package/dist/lib/file-operations/file-info.js +0 -143
  52. package/dist/lib/file-operations/gitignore.d.ts +0 -6
  53. package/dist/lib/file-operations/gitignore.js +0 -45
  54. package/dist/lib/file-operations/glob-helpers.d.ts +0 -18
  55. package/dist/lib/file-operations/glob-helpers.js +0 -23
  56. package/dist/lib/file-operations/list-directory.d.ts +0 -14
  57. package/dist/lib/file-operations/list-directory.js +0 -252
  58. package/dist/lib/file-operations/read-multiple-files.d.ts +0 -25
  59. package/dist/lib/file-operations/read-multiple-files.js +0 -252
  60. package/dist/lib/file-operations/search-files.d.ts +0 -27
  61. package/dist/lib/file-operations/search-files.js +0 -216
  62. package/dist/lib/file-operations/search-matcher.d.ts +0 -10
  63. package/dist/lib/file-operations/search-matcher.js +0 -72
  64. package/dist/lib/file-operations/search-worker.d.ts +0 -2
  65. package/dist/lib/file-operations/search-worker.js +0 -131
  66. package/dist/lib/file-operations/tree.d.ts +0 -28
  67. package/dist/lib/file-operations/tree.js +0 -265
  68. package/dist/lib/option-utils.d.ts +0 -3
  69. package/dist/lib/option-utils.js +0 -15
  70. package/dist/lib/path-format.d.ts +0 -1
  71. package/dist/lib/path-format.js +0 -7
  72. package/dist/lib/path-policy.d.ts +0 -2
  73. package/dist/lib/path-policy.js +0 -100
  74. package/dist/lib/progress-reporting.d.ts +0 -11
  75. package/dist/lib/progress-reporting.js +0 -13
  76. package/dist/lib/type-guards.d.ts +0 -1
  77. package/dist/lib/type-guards.js +0 -3
  78. package/dist/server/capabilities.d.ts +0 -10
  79. package/dist/server/capabilities.js +0 -48
  80. package/dist/server/logging.d.ts +0 -7
  81. package/dist/server/logging.js +0 -41
  82. package/dist/server/types.d.ts +0 -4
  83. package/dist/server/types.js +0 -1
@@ -1,10 +1,113 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as os from 'node:os';
3
3
  import * as path from 'node:path';
4
+ import { platform } from 'node:os';
4
5
  import { fileURLToPath } from 'node:url';
6
+ import { SENSITIVE_FILE_ALLOWLIST, SENSITIVE_FILE_DENYLIST, } from './constants.js';
5
7
  import { ErrorCode, isAbortError, isNodeError, McpError } from './errors.js';
6
8
  import { assertNotAborted, withAbort } from './fs-helpers.js';
7
- const IS_WINDOWS = os.platform() === 'win32';
9
+ const WINDOWS_PATH_SEPARATOR = '\\';
10
+ const POSIX_PATH_SEPARATOR = '/';
11
+ export function toPosixPath(value) {
12
+ return value.includes(WINDOWS_PATH_SEPARATOR)
13
+ ? value.replace(/\\/gu, POSIX_PATH_SEPARATOR)
14
+ : value;
15
+ }
16
+ const IS_WINDOWS = platform() === 'win32';
17
+ const WINDOWS_ABSOLUTE_RE = /^[a-z]:\//iu;
18
+ function normalizePathForMatch(input) {
19
+ return toPosixPath(path.normalize(input));
20
+ }
21
+ function normalizeForMatch(input) {
22
+ const normalized = normalizePathForMatch(input);
23
+ return IS_WINDOWS ? normalized.toLowerCase() : normalized;
24
+ }
25
+ function compilePatternGlobs(normalizedPattern) {
26
+ const globs = new Set([normalizedPattern]);
27
+ const isWindowsAbsolute = WINDOWS_ABSOLUTE_RE.test(normalizedPattern);
28
+ if (!normalizedPattern.startsWith('**/') && !isWindowsAbsolute) {
29
+ const withoutRoot = normalizedPattern.replace(/^\/+/u, '');
30
+ if (withoutRoot.length > 0) {
31
+ globs.add(`**/${withoutRoot}`);
32
+ }
33
+ }
34
+ return [...globs];
35
+ }
36
+ function compilePatterns(patterns) {
37
+ const unique = new Set();
38
+ for (const pattern of patterns) {
39
+ const trimmed = pattern.trim();
40
+ if (trimmed.length > 0) {
41
+ unique.add(trimmed);
42
+ }
43
+ }
44
+ const compiled = [];
45
+ for (const pattern of unique) {
46
+ const normalized = normalizeForMatch(pattern);
47
+ const matchesPath = normalized.includes('/');
48
+ compiled.push({
49
+ globs: matchesPath ? compilePatternGlobs(normalized) : [normalized],
50
+ matchesPath,
51
+ });
52
+ }
53
+ return compiled;
54
+ }
55
+ function toPatternSet(patterns) {
56
+ const pathGlobs = new Set();
57
+ const nameGlobs = new Set();
58
+ for (const pattern of patterns) {
59
+ const target = pattern.matchesPath ? pathGlobs : nameGlobs;
60
+ for (const glob of pattern.globs) {
61
+ target.add(glob);
62
+ }
63
+ }
64
+ return {
65
+ pathGlobs: [...pathGlobs],
66
+ nameGlobs: [...nameGlobs],
67
+ };
68
+ }
69
+ const DENY_PATTERNS = toPatternSet(compilePatterns(SENSITIVE_FILE_DENYLIST));
70
+ const ALLOW_PATTERNS = toPatternSet(compilePatterns(SENSITIVE_FILE_ALLOWLIST));
71
+ function uniquePair(primary, secondary) {
72
+ if (!secondary || secondary === primary)
73
+ return [primary];
74
+ return [primary, secondary];
75
+ }
76
+ function matchesAnyGlobs(globs, candidates) {
77
+ if (globs.length === 0 || candidates.length === 0)
78
+ return false;
79
+ for (const candidate of candidates) {
80
+ for (const glob of globs) {
81
+ if (path.posix.matchesGlob(candidate, glob))
82
+ return true;
83
+ }
84
+ }
85
+ return false;
86
+ }
87
+ export function isSensitivePath(requestedPath, resolvedPath) {
88
+ if (DENY_PATTERNS.pathGlobs.length === 0 &&
89
+ DENY_PATTERNS.nameGlobs.length === 0) {
90
+ return false;
91
+ }
92
+ const normalizedRequested = normalizeForMatch(requestedPath);
93
+ const normalizedResolved = resolvedPath
94
+ ? normalizeForMatch(resolvedPath)
95
+ : undefined;
96
+ const pathCandidates = uniquePair(normalizedRequested, normalizedResolved);
97
+ const nameCandidates = uniquePair(path.posix.basename(normalizedRequested), normalizedResolved ? path.posix.basename(normalizedResolved) : undefined);
98
+ if (matchesAnyGlobs(ALLOW_PATTERNS.pathGlobs, pathCandidates) ||
99
+ matchesAnyGlobs(ALLOW_PATTERNS.nameGlobs, nameCandidates)) {
100
+ return false;
101
+ }
102
+ return (matchesAnyGlobs(DENY_PATTERNS.pathGlobs, pathCandidates) ||
103
+ matchesAnyGlobs(DENY_PATTERNS.nameGlobs, nameCandidates));
104
+ }
105
+ export function assertAllowedFileAccess(requestedPath, resolvedPath) {
106
+ if (!isSensitivePath(requestedPath, resolvedPath))
107
+ return;
108
+ throw new McpError(ErrorCode.E_ACCESS_DENIED, `Access denied: sensitive file blocked by policy (${requestedPath}). ` +
109
+ 'Set FS_CONTEXT_ALLOW_SENSITIVE=1 or use FS_CONTEXT_ALLOWLIST to override.', requestedPath);
110
+ }
8
111
  const HOMEDIR = os.homedir();
9
112
  const PATH_SEPARATOR = path.sep;
10
113
  const DRIVE_LETTER_REGEX = /^[A-Za-z]:/;
@@ -393,6 +496,7 @@ export async function validatePathForWrite(requestedPath, signal) {
393
496
  allowedDirs,
394
497
  details: { normalizedPath: normalizedRequested },
395
498
  });
499
+ assertAllowedFileAccess(requestedPath, normalizedRequested);
396
500
  let current = normalizedRequested;
397
501
  for (;;) {
398
502
  try {
@@ -0,0 +1,15 @@
1
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
2
+ export declare function mergeOptions<T extends object>(defaults: T, overrides: Partial<T>): T;
3
+ export declare function omitOptionKeys<T extends object, K extends keyof T>(input: T, keys: readonly K[]): Omit<T, K>;
4
+ export declare function setIfDefined<T extends object, K extends keyof T>(target: T, key: K, value: T[K] | undefined): void;
5
+ export interface ProgressPayload {
6
+ current: number;
7
+ total?: number;
8
+ }
9
+ export type ProgressCallback = ((progress: ProgressPayload) => void) | undefined;
10
+ export interface PeriodicProgressOptions {
11
+ total?: number;
12
+ throttleModulo?: number;
13
+ force?: boolean;
14
+ }
15
+ export declare function reportPeriodicProgress(onProgress: ProgressCallback, current: number, options?: PeriodicProgressOptions): void;
@@ -0,0 +1,33 @@
1
+ // type-guards.ts
2
+ export function isRecord(value) {
3
+ return value !== null && typeof value === 'object';
4
+ }
5
+ // option-utils.ts
6
+ export function mergeOptions(defaults, overrides) {
7
+ return { ...defaults, ...overrides };
8
+ }
9
+ export function omitOptionKeys(input, keys) {
10
+ const output = { ...input };
11
+ for (const key of keys) {
12
+ Reflect.deleteProperty(output, key);
13
+ }
14
+ return output;
15
+ }
16
+ export function setIfDefined(target, key, value) {
17
+ if (value !== undefined) {
18
+ target[key] = value;
19
+ }
20
+ }
21
+ export function reportPeriodicProgress(onProgress, current, options = {}) {
22
+ if (!onProgress || current === 0)
23
+ return;
24
+ const throttleModulo = options.throttleModulo ?? 1;
25
+ const force = options.force ?? false;
26
+ if (!force && throttleModulo > 1 && current % throttleModulo !== 0) {
27
+ return;
28
+ }
29
+ onProgress({
30
+ current,
31
+ ...(options.total !== undefined ? { total: options.total } : {}),
32
+ });
33
+ }
@@ -2,7 +2,7 @@ import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
2
2
  import { buildCoreContextPack, getSharedConstraints, getToolContracts, } from './tool-info.js';
3
3
  import { buildWorkflowGuide } from './workflows.js';
4
4
  const INSTRUCTIONS_HEADER = `<role>
5
- Filesystem agent for local paths only. Operate inside allowed roots. Discover before action. Never guess paths.
5
+ Filesystem agent. Scope: allowed roots only. Discover paths before acting never guess.
6
6
  </role>
7
7
 
8
8
  <tools_overview>
@@ -16,10 +16,10 @@ Filesystem agent for local paths only. Operate inside allowed roots. Discover be
16
16
 
17
17
  <resources>
18
18
  - \`internal://instructions\`: Full usage reference.
19
- - \`internal://tool-catalog\`: Tool routing and data-flow rules.
19
+ - \`internal://tool-catalog\`: Tool routing and data flow.
20
20
  - \`internal://workflows\`: Standard execution sequences.
21
- - \`internal://tool-info/{name}\`: Per-tool nuances and gotchas (example: \`internal://tool-info/read\`).
22
- - \`filesystem-mcp://result/{id}\`: Cached large output. If \`resourceUri\` is returned, call \`resources/read\` immediately.
21
+ - \`internal://tool-info/{name}\`: Per-tool nuances (e.g. \`internal://tool-info/read\`).
22
+ - \`filesystem-mcp://result/{id}\`: Cached large output call \`resources/read\` immediately when \`resourceUri\` is returned.
23
23
  - \`filesystem-mcp://metrics\`: Per-tool runtime metrics.
24
24
  </resources>
25
25
 
@@ -35,8 +35,8 @@ ${getSharedConstraints()
35
35
  </constraints>
36
36
 
37
37
  <error_handling>
38
- - \`E_ACCESS_DENIED\` => call \`roots\`, then use an allowed path.
39
- - \`E_NOT_FOUND\` => call \`ls\` or \`find\`, then verify spelling.
38
+ - \`E_ACCESS_DENIED\` => call \`roots\`, use an allowed path.
39
+ - \`E_NOT_FOUND\` => call \`ls\` or \`find\`, verify spelling.
40
40
  - \`E_TOO_LARGE\` => use \`head\`, line ranges, or \`read_many\`.
41
41
  - \`E_TIMEOUT\` => reduce scope or result limits.
42
42
  </error_handling>
@@ -3,26 +3,26 @@ const CATALOG_GUIDE = `<tool_selection_guide>
3
3
  ## Cross-Tool Data Flow
4
4
 
5
5
  \`\`\`
6
- find (results[].path) -> grep.paths
7
- diff_files (patch text) -> apply_patch.patch
6
+ find(results[].path) -> grep.paths
7
+ diff_files(patch) -> apply_patch.patch
8
8
  \`\`\`
9
9
 
10
10
  ## Search Strategy
11
11
 
12
- - Use \`find\` for glob file discovery.
13
- - Use \`grep\` for text search.
14
- - Use \`search_and_replace\` only for replacement, never discovery.
12
+ - \`find\`: glob file discovery.
13
+ - \`grep\`: text content search.
14
+ - \`search_and_replace\`: replacement only, not discovery.
15
15
 
16
16
  ## Write Strategy
17
17
 
18
- - Use \`edit\` for precise, first-occurrence replacements in existing files.
19
- - Use \`write\` to create files or overwrite full contents.
20
- - Use \`search_and_replace\` for bulk multi-file replacements.
18
+ - \`edit\`: precise first-occurrence replacements.
19
+ - \`write\`: create files or overwrite full contents.
20
+ - \`search_and_replace\`: bulk multi-file replacements.
21
21
 
22
22
  ## Patch Management
23
23
 
24
24
  - Generate patches with \`diff_files\` first.
25
- - Run \`apply_patch\` with \`dryRun: true\` before writing.
25
+ - Validate with \`apply_patch(dryRun:true)\` before writing.
26
26
  - \`apply_patch\` accepts unified diffs only.
27
27
  </tool_selection_guide>
28
28
  `;
@@ -37,10 +37,10 @@ export function buildCoreContextPack() {
37
37
  }
38
38
  export function getSharedConstraints() {
39
39
  return [
40
- 'Use allowed roots only (provided by CLI negotiation).',
40
+ 'Use allowed roots only (from CLI negotiation).',
41
41
  'Sensitive paths are denylisted by default.',
42
- `Limits are enforced: max file size ${Math.floor(MAX_TEXT_FILE_SIZE / 1024 / 1024)}MB; search caps ${MAX_SEARCH_RESULTS} files and ${DEFAULT_SEARCH_CONTENT_RESULTS} lines.`,
43
- 'If a response includes `resourceUri`, call `resources/read` immediately; cached results expire on process restart.',
42
+ `Limits enforced: max file size ${Math.floor(MAX_TEXT_FILE_SIZE / 1024 / 1024)}MB; search caps ${MAX_SEARCH_RESULTS} files, ${DEFAULT_SEARCH_CONTENT_RESULTS} content matches.`,
43
+ 'If response includes `resourceUri`, call `resources/read` immediately cached results expire on restart.',
44
44
  ];
45
45
  }
46
46
  export function buildToolInfo(name) {
@@ -1,33 +1,20 @@
1
1
  export function buildWorkflowGuide() {
2
2
  return `<workflows>
3
- ### A: EXPLORE
4
- Use when: you need directory layout or file content.
5
- 1. \`roots\` (list allowed paths).
6
- 2. \`ls\` (flat view) or \`tree\` (recursive view).
7
- 3. \`stat\` or \`stat_many\` (type and size checks).
8
- 4. \`read\` or \`read_many\` (read content).
3
+ ### A: EXPLORE — directory layout or file content
4
+ 1. \`roots\` \`ls\` or \`tree\` \`stat\`/\`stat_many\` → \`read\`/\`read_many\`.
9
5
  > **Strict:** Resolve paths first. Never guess.
10
6
 
11
- ### B: SEARCH
12
- Use when: you need files by pattern or content.
13
- 1. \`find\` (glob candidates).
14
- 2. \`grep\` (content matches).
15
- 3. \`read\` (verify matched context).
16
- > **Strict:** Do content search with \`grep\`, not \`find\`.
7
+ ### B: SEARCH — files by pattern or content
8
+ 1. \`find\` (glob) \`grep\` (content) \`read\` (verify).
9
+ > **Strict:** Content search with \`grep\`, not \`find\`.
17
10
 
18
- ### C: EDIT
19
- Use when: you need to modify files or layout.
20
- 1. \`edit\` (targeted string replacement).
21
- 2. \`search_and_replace\` (bulk replacements).
22
- 3. \`mv\` or \`rm\` (layout changes).
23
- 4. \`mkdir\` (directory creation).
11
+ ### C: EDIT — modify files or layout
12
+ 1. \`edit\` (targeted replacement) or \`search_and_replace\` (bulk).
13
+ 2. \`mv\`/\`rm\` (layout) or \`mkdir\` (create dirs).
24
14
  > **Strict:** Confirm destructive ops (\`write\`, \`mv\`, \`rm\`, bulk replace).
25
15
 
26
- ### D: PATCH
27
- Use when: applying unified diffs from \`diff_files\`.
28
- 1. \`diff_files\` (generate).
29
- 2. \`apply_patch\` (dryRun: true).
30
- 3. \`apply_patch\` (dryRun: false).
16
+ ### D: PATCH — apply unified diffs
17
+ 1. \`diff_files\` \`apply_patch(dryRun:true)\` \`apply_patch\`.
31
18
  > **Tip:** Feed \`diff_files\` output directly to \`apply_patch\`.
32
19
  </workflows>`;
33
20
  }
package/dist/schemas.js CHANGED
@@ -16,8 +16,8 @@ function isSafeGlobPattern(value) {
16
16
  return true;
17
17
  }
18
18
  const MAX_PATH_LENGTH = 4096;
19
- const DESC_PATH_ROOT = 'Base directory (default: root). Absolute path required if multiple roots exist. Examples: "src", "src/components"';
20
- const DESC_PATH_REQUIRED = 'Absolute path to file or directory. Examples: "src/index.ts", "README.md"';
19
+ const DESC_PATH_ROOT = 'Base directory (default: root). Absolute path required if multiple roots.';
20
+ const DESC_PATH_REQUIRED = 'Absolute path to file or directory.';
21
21
  function defaultFalseBoolean(description) {
22
22
  return z.boolean().optional().default(false).describe(description);
23
23
  }
@@ -209,9 +209,9 @@ export const SearchContentInputSchema = z.strictObject({
209
209
  .string()
210
210
  .min(1, 'Pattern required')
211
211
  .max(1000, 'Max 1000 chars')
212
- .describe('Literal text to search for by default; treated as RE2 regex when isRegex is true.'),
213
- isRegex: defaultFalseBoolean('Treat pattern as a RE2 regular expression. RE2 does not support lookahead, lookbehind, or backreferences.'),
214
- caseSensitive: defaultFalseBoolean('Case-sensitive matching (default: false — searches are case-insensitive).'),
212
+ .describe('Search text. RE2 regex when `isRegex=true`.'),
213
+ isRegex: defaultFalseBoolean('Treat pattern as RE2 regex (no lookahead/lookbehind/backrefs).'),
214
+ caseSensitive: defaultFalseBoolean('Case-sensitive matching. Default: case-insensitive.'),
215
215
  wholeWord: defaultFalseBoolean('Match whole words only'),
216
216
  contextLines: z
217
217
  .number()
@@ -459,15 +459,15 @@ export const EditFileInputSchema = z.strictObject({
459
459
  .array(z.strictObject({
460
460
  oldText: z
461
461
  .string()
462
- .describe('Exact literal string to replace — must match character-for-character including whitespace and indentation. Include 3–5 lines of surrounding context to uniquely identify the location.'),
462
+ .describe('Exact literal string to replace (character-for-character). Include 3–5 lines of context for unique targeting.'),
463
463
  newText: z
464
464
  .string()
465
- .describe('Replacement string preserve the indentation style of surrounding code.'),
465
+ .describe('Replacement string. Preserve surrounding indentation style.'),
466
466
  }))
467
467
  .min(1, 'Min 1 edit required')
468
468
  .describe('List of replacements to apply sequentially. Each edit replaces the first occurrence of oldText.'),
469
- dryRun: defaultFalseBoolean('Preview edits without writing. Check unmatchedEdits in the response to verify all oldText values were found.'),
470
- ignoreWhitespace: defaultFalseBoolean('Ignore leading/trailing whitespace and treat all whitespace sequences as equivalent when matching oldText.'),
469
+ dryRun: defaultFalseBoolean('Preview edits without writing. Check `unmatchedEdits` in response.'),
470
+ ignoreWhitespace: defaultFalseBoolean('Treat all whitespace sequences as equivalent when matching oldText.'),
471
471
  });
472
472
  export const EditFileOutputSchema = z.strictObject({
473
473
  ok: z.boolean(),
@@ -564,7 +564,7 @@ export const ApplyPatchInputSchema = z.strictObject({
564
564
  path: RequiredPathSchema.describe('Path to file to patch'),
565
565
  patch: z
566
566
  .string()
567
- .describe('Unified diff content to apply — must include @@ hunk headers. Generate with `diff_files`.'),
567
+ .describe('Unified diff with @@ hunk headers. Generate with `diff_files`.'),
568
568
  fuzzFactor: z
569
569
  .number()
570
570
  .int({ error: 'Must be integer' })
@@ -581,7 +581,7 @@ export const ApplyPatchInputSchema = z.strictObject({
581
581
  .boolean()
582
582
  .optional()
583
583
  .default(false)
584
- .describe('Validate the patch can be applied without writing. Check `applied` in the response before committing.'),
584
+ .describe('Validate patch without writing. Check `applied` before committing.'),
585
585
  });
586
586
  export const ApplyPatchOutputSchema = z.strictObject({
587
587
  ok: z.boolean(),
@@ -602,18 +602,18 @@ export const SearchAndReplaceInputSchema = z.strictObject({
602
602
  searchPattern: z
603
603
  .string()
604
604
  .min(1, 'Search pattern required')
605
- .describe('Text to search for. Matched literally by default; treated as RE2 regex when isRegex is true.'),
605
+ .describe('Text to search for. Literal by default; RE2 regex when `isRegex=true`.'),
606
606
  replacement: z.string().describe('Replacement text'),
607
- isRegex: defaultFalseBoolean('Treat searchPattern as a RE2 regular expression. Supports capture group references ($1, $2) in replacement.'),
607
+ isRegex: defaultFalseBoolean('Treat searchPattern as RE2 regex. Supports capture groups ($1, $2) in replacement.'),
608
608
  dryRun: defaultFalseBoolean('Preview matches without writing. Check changedFiles and matches in the response before committing.'),
609
609
  includeHidden: z
610
610
  .boolean()
611
611
  .optional()
612
- .describe('Include hidden files and directories (starting with .) in the search scope. Default: false.'),
612
+ .describe('Include hidden files/directories (starting with .). Default: false.'),
613
613
  includeIgnored: z
614
614
  .boolean()
615
615
  .optional()
616
- .describe('Include files and directories ignored by .gitignore rules (e.g. node_modules, dist). Default: false.'),
616
+ .describe('Include .gitignore-ignored files (node_modules, dist). Default: false.'),
617
617
  returnDiff: z
618
618
  .boolean()
619
619
  .optional()
@@ -1,6 +1,24 @@
1
1
  import * as http from 'node:http';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import type { ServerOptions } from './types.js';
3
+ import type { LoggingLevel } from '@modelcontextprotocol/sdk/types.js';
4
+ export interface ServerOptions {
5
+ allowCwd?: boolean;
6
+ cliAllowedDirs?: string[];
7
+ }
8
+ interface CapabilityOptions {
9
+ enablePromptListChanged?: boolean;
10
+ enableTaskToolRequests?: boolean;
11
+ }
12
+ type ServerCapabilities = NonNullable<ConstructorParameters<typeof McpServer>[1]>['capabilities'];
13
+ type NonOptionalServerCapabilities = NonNullable<ServerCapabilities>;
14
+ export declare function buildServerCapabilities(options?: CapabilityOptions): NonOptionalServerCapabilities;
15
+ export declare function supportsTaskToolRequests(): boolean;
16
+ export interface LoggingState {
17
+ minimumLevel: LoggingLevel;
18
+ }
19
+ export declare function createLoggingState(minimumLevel?: LoggingLevel): LoggingState;
20
+ export declare function logToMcp(server: McpServer | undefined, level: LoggingLevel, data: string, minLevel?: LoggingLevel): void;
4
21
  export declare function createServer(options?: ServerOptions): Promise<McpServer>;
5
22
  export declare function startServer(server: McpServer): Promise<void>;
6
23
  export declare function startHttpServer(port: number, options: ServerOptions): Promise<http.Server>;
24
+ export {};
@@ -6,19 +6,104 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
7
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
8
8
  import { isInitializeRequest, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
9
- import { registerCompletions } from '../completions.js';
10
9
  import { DEFAULT_LOG_LEVEL, parseEnvInt, REQUIRED_MCP_PROTOCOL_VERSION, } from '../lib/constants.js';
11
10
  import { formatUnknownErrorMessage } from '../lib/errors.js';
12
11
  import { createInMemoryResourceStore } from '../lib/resource-store.js';
12
+ import { isRecord } from '../lib/utils.js';
13
+ import { registerCompletions } from '../completions.js';
13
14
  import { pkgInfo } from '../pkg-info.js';
14
15
  import { registerGetHelpPrompt } from '../prompts.js';
15
16
  import { registerInstructionResource, registerMetricsResource, registerResultResources, registerToolCatalogResource, registerToolInfoResource, registerWorkflowGuideResource, } from '../resources.js';
16
17
  import { buildServerInstructions } from '../resources/generated-instructions.js';
17
18
  import { registerAllTools } from '../tools.js';
18
19
  import { withDefaultIcons } from '../tools/shared.js';
19
- import { buildServerCapabilities, supportsTaskToolRequests, } from './capabilities.js';
20
- import { createLoggingState } from './logging.js';
21
20
  import { RootsManager } from './roots-manager.js';
21
+ let cachedTaskToolSupport;
22
+ function detectTaskToolSupport() {
23
+ if (cachedTaskToolSupport !== undefined) {
24
+ return cachedTaskToolSupport;
25
+ }
26
+ try {
27
+ // Instantiate a minimal, unconnected probe server to duck-type check for
28
+ // task tool support. The probe has no transport or active connections, so
29
+ // close() only releases in-memory state; fire-and-forget is safe here.
30
+ const probe = new McpServer({
31
+ name: 'filesystem-mcp-capability-probe',
32
+ version: '0.0.0',
33
+ }, { capabilities: { tools: {} } });
34
+ cachedTaskToolSupport =
35
+ typeof probe.experimental.tasks.registerToolTask === 'function';
36
+ probe.close().catch(() => { });
37
+ }
38
+ catch {
39
+ cachedTaskToolSupport = false;
40
+ }
41
+ return cachedTaskToolSupport;
42
+ }
43
+ export function buildServerCapabilities(options = {}) {
44
+ const capabilities = {
45
+ logging: {},
46
+ resources: {},
47
+ tools: {},
48
+ prompts: options.enablePromptListChanged ? { listChanged: true } : {},
49
+ completions: {},
50
+ };
51
+ if (options.enableTaskToolRequests) {
52
+ // NOTE: enabling task tool requests requires the caller to configure
53
+ // an InMemoryTaskStore and InMemoryTaskMessageQueue on the McpServer.
54
+ // InMemoryTaskStore accumulates completed task records with no TTL eviction —
55
+ // suitable for short-lived stdio sessions. Long-running HTTP servers should
56
+ // replace it with a TTL-evicting store to avoid unbounded memory growth.
57
+ capabilities.tasks = {
58
+ list: {},
59
+ cancel: {},
60
+ requests: { tools: { call: {} } },
61
+ };
62
+ }
63
+ return capabilities;
64
+ }
65
+ export function supportsTaskToolRequests() {
66
+ return detectTaskToolSupport();
67
+ }
68
+ const MCP_LOGGER_NAME = 'filesystem-mcp';
69
+ const LOG_LEVEL_ORDER = {
70
+ debug: 0,
71
+ info: 1,
72
+ notice: 2,
73
+ warning: 3,
74
+ error: 4,
75
+ critical: 5,
76
+ alert: 6,
77
+ emergency: 7,
78
+ };
79
+ export function createLoggingState(minimumLevel = 'debug') {
80
+ return { minimumLevel };
81
+ }
82
+ function canSendMcpLogs(server) {
83
+ const capabilities = server.server.getClientCapabilities();
84
+ if (!isRecord(capabilities))
85
+ return false;
86
+ if (!('logging' in capabilities))
87
+ return false;
88
+ return !!capabilities['logging'];
89
+ }
90
+ export function logToMcp(server, level, data, minLevel = 'debug') {
91
+ if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[minLevel]) {
92
+ return;
93
+ }
94
+ if (!server || !canSendMcpLogs(server)) {
95
+ console.error(data);
96
+ return;
97
+ }
98
+ const params = {
99
+ level,
100
+ logger: MCP_LOGGER_NAME,
101
+ data,
102
+ };
103
+ void server.sendLoggingMessage(params).catch((error) => {
104
+ console.error(`Failed to send MCP log: ${level} | ${data}`, formatUnknownErrorMessage(error));
105
+ });
106
+ }
22
107
  const { version: SERVER_VERSION, description: SERVER_DESCRIPTION, homepage: SERVER_HOMEPAGE, } = pkgInfo;
23
108
  const rootsManagers = new WeakMap();
24
109
  function getRootsManager(server) {
@@ -242,9 +327,11 @@ export async function startHttpServer(port, options) {
242
327
  if (typeof authHeader === 'string' &&
243
328
  authHeader.startsWith(bearerPrefix)) {
244
329
  const userKey = authHeader.slice(bearerPrefix.length);
245
- const expectedHash = createHash('sha256').update(apiKey).digest();
246
- const actualHash = createHash('sha256').update(userKey).digest();
247
- authorized = timingSafeEqual(expectedHash, actualHash);
330
+ if (userKey.length <= 4096) {
331
+ const expectedHash = createHash('sha256').update(apiKey).digest();
332
+ const actualHash = createHash('sha256').update(userKey).digest();
333
+ authorized = timingSafeEqual(expectedHash, actualHash);
334
+ }
248
335
  }
249
336
  if (!authorized) {
250
337
  res.writeHead(401, {
@@ -283,7 +370,7 @@ export async function startHttpServer(port, options) {
283
370
  }
284
371
  const body = await readRequestBody(req);
285
372
  if (isInitializeRequest(body)) {
286
- const maxSessions = parseInt(process.env['FILESYSTEM_MCP_MAX_HTTP_SESSIONS'] ?? '', 10) || 100;
373
+ const maxSessions = parseEnvInt('FILESYSTEM_MCP_MAX_HTTP_SESSIONS', 100, 1, 10_000);
287
374
  if (sessions.size >= maxSessions) {
288
375
  sendJsonRpcError(res, 503, -32000, 'Too many sessions');
289
376
  return;
@@ -316,8 +403,15 @@ export async function startHttpServer(port, options) {
316
403
  }
317
404
  }
318
405
  else {
319
- res.writeHead(405, { Allow: 'GET, POST, DELETE' });
320
- res.end('Method Not Allowed');
406
+ res.writeHead(405, {
407
+ Allow: 'GET, POST, DELETE',
408
+ 'Content-Type': 'application/json',
409
+ });
410
+ res.end(JSON.stringify({
411
+ jsonrpc: '2.0',
412
+ error: { code: -32000, message: 'Method Not Allowed' },
413
+ id: null,
414
+ }));
321
415
  }
322
416
  }
323
417
  catch (error) {
@@ -1,6 +1,6 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type LoggingState } from './logging.js';
3
- import type { ServerOptions } from './types.js';
2
+ import { type LoggingState } from './bootstrap.js';
3
+ import type { ServerOptions } from './bootstrap.js';
4
4
  export declare class RootsManager {
5
5
  private rootsUpdateTimeout;
6
6
  private rootDirectories;
@@ -3,9 +3,9 @@ import { InitializedNotificationSchema, RootsListChangedNotificationSchema, } fr
3
3
  import { z } from 'zod';
4
4
  import { formatUnknownErrorMessage } from '../lib/errors.js';
5
5
  import { assertNotAborted, createTimedAbortSignal, withAbort, } from '../lib/fs-helpers.js';
6
- import { getAllowedDirectories, getValidRootDirectories, isPathWithinDirectories, normalizePath, setAllowedDirectoriesResolved, } from '../lib/path-validation.js';
7
- import { isRecord } from '../lib/type-guards.js';
8
- import { logToMcp } from './logging.js';
6
+ import { getAllowedDirectories, getValidRootDirectories, isPathWithinDirectories, normalizePath, setAllowedDirectoriesResolved, } from '../lib/paths.js';
7
+ import { isRecord } from '../lib/utils.js';
8
+ import { logToMcp } from './bootstrap.js';
9
9
  const ROOTS_TIMEOUT_MS = 5000;
10
10
  const ROOTS_DEBOUNCE_MS = 100;
11
11
  function normalizeCLIDirectories(dirs) {
@@ -4,7 +4,7 @@ import { applyPatch } from 'diff';
4
4
  import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
5
5
  import { ErrorCode, McpError } from '../lib/errors.js';
6
6
  import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
7
- import { validateExistingPath } from '../lib/path-validation.js';
7
+ import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
8
8
  import { ApplyPatchInputSchema, ApplyPatchOutputSchema } from '../schemas.js';
9
9
  import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
10
10
  import { registerToolTaskIfAvailable } from './task-support.js';
@@ -12,8 +12,8 @@ export const APPLY_PATCH_TOOL = {
12
12
  name: 'apply_patch',
13
13
  title: 'Apply Patch',
14
14
  description: 'Apply a unified diff patch to a file. ' +
15
- 'Generate the patch with `diff_files`, then validate with `dryRun: true` before writing. ' +
16
- 'On failure, regenerate a fresh patch via `diff_files` against the current file content and retry.',
15
+ 'Workflow: `diff_files` \u2192 `apply_patch(dryRun:true)` \u2192 `apply_patch`. ' +
16
+ 'On failure, regenerate the patch from current file content.',
17
17
  inputSchema: ApplyPatchInputSchema,
18
18
  outputSchema: ApplyPatchOutputSchema,
19
19
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
@@ -36,6 +36,7 @@ function assertPatchHasHunks(patch) {
36
36
  async function handleApplyPatch(args, signal) {
37
37
  const maxFileSize = MAX_TEXT_FILE_SIZE;
38
38
  const validPath = await validateExistingPath(args.path, signal);
39
+ assertAllowedFileAccess(args.path, validPath);
39
40
  const stats = await withAbort(fs.stat(validPath), signal);
40
41
  assertPatchTargetSizeWithinLimit(validPath, stats.size, maxFileSize);
41
42
  const content = await fs.readFile(validPath, { encoding: 'utf-8', signal });
@@ -4,11 +4,11 @@ import { createHash } from 'node:crypto';
4
4
  import { createReadStream } from 'node:fs';
5
5
  import { PARALLEL_CONCURRENCY } from '../lib/constants.js';
6
6
  import { ErrorCode } from '../lib/errors.js';
7
- import { isIgnoredByGitignore, loadRootGitignore, } from '../lib/file-operations/gitignore.js';
8
- import { globEntries } from '../lib/file-operations/glob-engine.js';
7
+ import { isIgnoredByGitignore, loadRootGitignore, } from '../lib/file-operations/core.js';
8
+ import { globEntries } from '../lib/file-operations/traversal.js';
9
9
  import { assertNotAborted, withAbort } from '../lib/fs-helpers.js';
10
- import { validateExistingPath } from '../lib/path-validation.js';
11
- import { reportPeriodicProgress } from '../lib/progress-reporting.js';
10
+ import { validateExistingPath } from '../lib/paths.js';
11
+ import { reportPeriodicProgress } from '../lib/utils.js';
12
12
  import { CalculateHashInputSchema, CalculateHashOutputSchema, } from '../schemas.js';
13
13
  import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
14
14
  import { registerToolTaskIfAvailable } from './task-support.js';
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import { ErrorCode, McpError } from '../lib/errors.js';
3
3
  import { withAbort } from '../lib/fs-helpers.js';
4
- import { validatePathForWrite } from '../lib/path-validation.js';
4
+ import { validatePathForWrite } from '../lib/paths.js';
5
5
  import { CreateDirectoryInputSchema, CreateDirectoryOutputSchema, } from '../schemas.js';
6
6
  import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, IDEMPOTENT_WRITE_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
7
7
  import { registerToolTaskIfAvailable } from './task-support.js';