@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
@@ -2,7 +2,7 @@ import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { ErrorCode, isNodeError, McpError } from '../lib/errors.js';
4
4
  import { withAbort } from '../lib/fs-helpers.js';
5
- import { isAllowedDirectoryRoot, validatePathForWrite, } from '../lib/path-validation.js';
5
+ import { isAllowedDirectoryRoot, validatePathForWrite } from '../lib/paths.js';
6
6
  import { DeleteFileInputSchema, DeleteFileOutputSchema } from '../schemas.js';
7
7
  import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  import { registerToolTaskIfAvailable } from './task-support.js';
@@ -14,8 +14,8 @@ export const DELETE_FILE_TOOL = {
14
14
  outputSchema: DeleteFileOutputSchema,
15
15
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
16
16
  gotchas: [
17
- 'Deletion is permanent there is no undo or recycle bin.',
18
- 'Non-empty directory delete requires `recursive=true`; else returns actionable input error.',
17
+ 'No undodeletion is permanent.',
18
+ 'Non-empty directories require `recursive=true`.',
19
19
  ],
20
20
  };
21
21
  async function handleDeleteFile(args, signal) {
@@ -4,7 +4,7 @@ import { createTwoFilesPatch } 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 { withAbort } from '../lib/fs-helpers.js';
7
- import { validateExistingPath } from '../lib/path-validation.js';
7
+ import { validateExistingPath } from '../lib/paths.js';
8
8
  import { DiffFilesInputSchema, DiffFilesOutputSchema } from '../schemas.js';
9
9
  import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
10
10
  import { registerToolTaskIfAvailable } from './task-support.js';
@@ -13,7 +13,7 @@ export const DIFF_FILES_TOOL = {
13
13
  title: 'Diff Files',
14
14
  description: 'Generate a unified diff between two files. ' +
15
15
  'Output feeds directly into `apply_patch`. ' +
16
- 'Check `isIdentical` in the response — if true, the files are already in sync and no patch is needed.',
16
+ '`isIdentical=true` means files match \u2014 no patch needed.',
17
17
  inputSchema: DiffFilesInputSchema,
18
18
  outputSchema: DiffFilesOutputSchema,
19
19
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
@@ -1,28 +1,24 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import RE2 from 're2';
4
- import { ErrorCode } from '../lib/errors.js';
5
- import { atomicWriteFile } from '../lib/fs-helpers.js';
6
- import { validateExistingPath } from '../lib/path-validation.js';
4
+ import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
5
+ import { ErrorCode, McpError } from '../lib/errors.js';
6
+ import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
7
+ import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
7
8
  import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
8
9
  import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
9
10
  import { registerToolTaskIfAvailable } from './task-support.js';
10
11
  export const EDIT_FILE_TOOL = {
11
12
  name: 'edit',
12
13
  title: 'Edit File',
13
- description: 'Edit a file by replacing text. Sequentially applies a list of string replacements. ' +
14
- 'Replaces the first occurrence of each `oldText`. ' +
15
- '`oldText` must match exactly — include 3–5 lines of surrounding context to uniquely target the location. ' +
16
- 'Use `dryRun: true` to validate edits before writing.',
14
+ description: 'Apply sequential literal string replacements to a file (first occurrence per edit). ' +
15
+ '`oldText` must match exactly \u2014 include 3\u20135 lines of context for unique targeting. ' +
16
+ 'Use `dryRun:true` to preview.',
17
17
  inputSchema: EditFileInputSchema,
18
18
  outputSchema: EditFileOutputSchema,
19
19
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
20
- nuances: [
21
- 'Apply sequential literal replacements (first occurrence per edit).',
22
- ],
23
- gotchas: [
24
- '`oldText` must match exactly; unmatched items are reported in `unmatchedEdits`.',
25
- ],
20
+ nuances: ['Each edit applies to the output of the previous edit.'],
21
+ gotchas: ['Unmatched `oldText` entries listed in `unmatchedEdits`.'],
26
22
  };
27
23
  function escapeRegExp(string) {
28
24
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -88,6 +84,11 @@ function applyEdits(content, edits, ignoreWhitespace) {
88
84
  }
89
85
  export async function handleEditFile(args, signal) {
90
86
  const validPath = await validateExistingPath(args.path, signal);
87
+ assertAllowedFileAccess(args.path, validPath);
88
+ const stats = await withAbort(fs.stat(validPath), signal);
89
+ if (stats.size > MAX_TEXT_FILE_SIZE) {
90
+ throw new McpError(ErrorCode.E_TOO_LARGE, `File too large for edit: ${args.path} (${stats.size} bytes > ${MAX_TEXT_FILE_SIZE} bytes)`, args.path, { size: stats.size, maxFileSize: MAX_TEXT_FILE_SIZE });
91
+ }
91
92
  const content = await fs.readFile(validPath, { encoding: 'utf-8', signal });
92
93
  const { content: newContent, appliedEdits, unmatchedEdits, lineRange, } = applyEdits(content, args.edits, args.ignoreWhitespace);
93
94
  const structured = {
@@ -1,19 +1,17 @@
1
1
  import * as path from 'node:path';
2
- import { formatOperationSummary, joinLines } from '../config.js';
3
2
  import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
4
3
  import { ErrorCode } from '../lib/errors.js';
5
- import { listDirectory } from '../lib/file-operations/list-directory.js';
4
+ import { listDirectory } from '../lib/file-operations/metadata.js';
5
+ import { formatOperationSummary, joinLines } from '../config.js';
6
6
  import { ListDirectoryInputSchema, ListDirectoryOutputSchema, } from '../schemas.js';
7
7
  import { buildToolErrorResponse, buildToolResponse, decodeOffsetCursor, encodeOffsetCursor, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  import { registerToolTaskIfAvailable } from './task-support.js';
9
9
  export const LIST_DIRECTORY_TOOL = {
10
10
  name: 'ls',
11
11
  title: 'List Directory',
12
- description: 'List the immediate contents of a directory (non-recursive). ' +
13
- 'Returns name, relative path, type (file/directory/symlink), size, and modified date. ' +
14
- 'Omit path to list the workspace root. ' +
15
- 'Use includeIgnored=true to include ignored directories like node_modules. ' +
16
- 'For recursive searches, use find instead.',
12
+ description: 'List immediate directory contents (non-recursive): name, path, type, size, modified date. ' +
13
+ 'Omit path for workspace root. `includeIgnored=true` for node_modules etc. ' +
14
+ 'For recursive search, use `find`.',
17
15
  inputSchema: ListDirectoryInputSchema,
18
16
  outputSchema: ListDirectoryOutputSchema,
19
17
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
@@ -2,7 +2,7 @@ import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { ErrorCode, formatUnknownErrorMessage, isNodeError, McpError, } from '../lib/errors.js';
4
4
  import { withAbort } from '../lib/fs-helpers.js';
5
- import { validateExistingPath, validatePathForWrite, } from '../lib/path-validation.js';
5
+ import { assertAllowedFileAccess, validateExistingPath, validatePathForWrite, } from '../lib/paths.js';
6
6
  import { MoveFileInputSchema, MoveFileOutputSchema } from '../schemas.js';
7
7
  import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  import { registerToolTaskIfAvailable } from './task-support.js';
@@ -48,6 +48,7 @@ export async function handleMoveFile(args, signal) {
48
48
  let validSource;
49
49
  try {
50
50
  validSource = await validateExistingPath(src, signal);
51
+ assertAllowedFileAccess(src, validSource);
51
52
  }
52
53
  catch (error) {
53
54
  failed.push({
@@ -1,16 +1,15 @@
1
1
  import * as path from 'node:path';
2
2
  import { DEFAULT_READ_MANY_MAX_TOTAL_SIZE, DEFAULT_SEARCH_TIMEOUT_MS, } from '../lib/constants.js';
3
3
  import { ErrorCode } from '../lib/errors.js';
4
- import { readMultipleFiles } from '../lib/file-operations/read-multiple-files.js';
4
+ import { readMultipleFiles } from '../lib/file-operations/metadata.js';
5
5
  import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
6
6
  import { buildBatchCompletionSuffix, buildBatchPathContext, buildResourceLink, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
7
7
  import { registerToolTaskIfAvailable } from './task-support.js';
8
8
  export const READ_MULTIPLE_FILES_TOOL = {
9
9
  name: 'read_many',
10
10
  title: 'Read Multiple Files',
11
- description: 'Read multiple text files in a single request. ' +
12
- 'Returns contents and metadata for each file. ' +
13
- 'For single file, use read for simpler output.',
11
+ description: 'Read multiple text files in one request with contents and metadata. ' +
12
+ 'For a single file, use `read`.',
14
13
  inputSchema: ReadMultipleFilesInputSchema,
15
14
  outputSchema: ReadMultipleFilesOutputSchema,
16
15
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
@@ -8,9 +8,9 @@ import { registerToolTaskIfAvailable } from './task-support.js';
8
8
  export const READ_FILE_TOOL = {
9
9
  name: 'read',
10
10
  title: 'Read File',
11
- description: 'Read the text contents of a file. ' +
12
- 'Use head parameter to preview the first N lines of large files. ' +
13
- 'For multiple files, use read_many for efficiency.',
11
+ description: 'Read text file contents. ' +
12
+ 'Use `head` to preview first N lines of large files. ' +
13
+ 'For multiple files, use `read_many`.',
14
14
  inputSchema: ReadFileInputSchema,
15
15
  outputSchema: ReadFileOutputSchema,
16
16
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
@@ -5,28 +5,26 @@ import RE2 from 're2';
5
5
  import safeRegex from 'safe-regex2';
6
6
  import { DEFAULT_EXCLUDE_PATTERNS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../lib/constants.js';
7
7
  import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
8
- import { globEntries } from '../lib/file-operations/glob-engine.js';
8
+ import { globEntries } from '../lib/file-operations/traversal.js';
9
9
  import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
10
- import { validateExistingPath, validatePathForWrite, } from '../lib/path-validation.js';
11
- import { reportPeriodicProgress } from '../lib/progress-reporting.js';
10
+ import { validateExistingPath, validatePathForWrite } from '../lib/paths.js';
11
+ import { reportPeriodicProgress } from '../lib/utils.js';
12
12
  import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
13
13
  import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, resolveFinalProgressCurrent, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
14
14
  import { registerToolTaskIfAvailable } from './task-support.js';
15
15
  export const SEARCH_AND_REPLACE_TOOL = {
16
16
  name: 'search_and_replace',
17
17
  title: 'Search and Replace',
18
- description: 'Search and replace text across multiple files matching a glob pattern. ' +
19
- 'Replaces ALL occurrences in each file (unlike `edit` which replaces only the first). ' +
20
- 'Use `filePattern` to scope which files are touched. ' +
21
- 'Always run with `dryRun: true` first to verify matches before writing. ' +
22
- 'Returns a unified diff of changes in `dryRun` mode. ' +
23
- 'Literal mode (default) matches exact text; `isRegex: true` enables RE2 regex with capture groups ($1, $2).',
18
+ description: 'Bulk search-and-replace across files matching a glob. ' +
19
+ 'Replaces ALL occurrences per file (unlike `edit`: first only). ' +
20
+ 'Always `dryRun:true` first \u2014 returns a unified diff. ' +
21
+ 'Literal matching by default; `isRegex:true` enables RE2 with capture groups ($1, $2).',
24
22
  inputSchema: SearchAndReplaceInputSchema,
25
23
  outputSchema: SearchAndReplaceOutputSchema,
26
24
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
27
25
  taskSupport: 'optional',
28
26
  gotchas: [
29
- 'Literal mode is default; `isRegex=true` enables RE2 + capture replacements (`$1`, `$2`).',
27
+ 'Replaces ALL occurrences not just the first. Use `edit` for single replacements.',
30
28
  ],
31
29
  nuances: [
32
30
  'Changed-file sample and failure sample are capped/truncated in output.',
@@ -1,19 +1,17 @@
1
- import { joinLines } from '../config.js';
2
1
  import { ErrorCode } from '../lib/errors.js';
3
- import { getAllowedDirectories } from '../lib/path-validation.js';
2
+ import { getAllowedDirectories } from '../lib/paths.js';
3
+ import { joinLines } from '../config.js';
4
4
  import { ListAllowedDirectoriesInputSchema, ListAllowedDirectoriesOutputSchema, } from '../schemas.js';
5
5
  import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
6
6
  import { registerToolTaskIfAvailable } from './task-support.js';
7
7
  export const LIST_ALLOWED_DIRECTORIES_TOOL = {
8
8
  name: 'roots',
9
9
  title: 'Workspace Roots',
10
- description: 'List the workspace roots this server can access. ' +
11
- 'Call this first to see available directories. ' +
12
- 'All other tools only work within these directories.',
10
+ description: 'List allowed workspace roots. Call first \u2014 all other tools are scoped to these directories.',
13
11
  inputSchema: ListAllowedDirectoriesInputSchema,
14
12
  outputSchema: ListAllowedDirectoriesOutputSchema,
15
13
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
16
- nuances: ['Other tools are constrained to these roots.'],
14
+ nuances: ['Returns absolute paths of all allowed directories.'],
17
15
  };
18
16
  function buildTextRoots(dirs) {
19
17
  if (dirs.length === 0) {
@@ -1,9 +1,9 @@
1
1
  import * as path from 'node:path';
2
2
  import RE2 from 're2';
3
- import { formatOperationSummary, joinLines } from '../config.js';
4
3
  import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
5
4
  import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
6
- import { searchContent } from '../lib/file-operations/search-content.js';
5
+ import { searchContent } from '../lib/file-operations/search.js';
6
+ import { formatOperationSummary, joinLines } from '../config.js';
7
7
  import { SearchContentInputSchema, SearchContentOutputSchema, } from '../schemas.js';
8
8
  import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
9
9
  import { registerToolTaskIfAvailable } from './task-support.js';
@@ -11,20 +11,18 @@ const MAX_INLINE_MATCHES = parseInt(process.env['FS_CONTEXT_MAX_INLINE_MATCHES']
11
11
  export const SEARCH_CONTENT_TOOL = {
12
12
  name: 'grep',
13
13
  title: 'Search Content',
14
- description: 'Search for text within file contents (grep-like). ' +
15
- 'Returns matching lines. ' +
16
- 'Path may be a directory or a single file. ' +
17
- 'Use `filePattern` to scope by file type (e.g. `**/*.ts`) and avoid noisy results. ' +
18
- 'Use includeHidden=true to include hidden files and directories.',
14
+ description: 'Search file contents for text (grep-like). Returns matching lines. ' +
15
+ 'Scope with `filePattern` (e.g. `**/*.ts`) to reduce noise. ' +
16
+ '`includeHidden=true` for dotfiles.',
19
17
  inputSchema: SearchContentInputSchema,
20
18
  outputSchema: SearchContentOutputSchema,
21
19
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
22
20
  nuances: [
23
- 'Inline match rows are capped (first 50); full structured results are externalized via `resourceUri`.',
21
+ 'Inline results capped at 50 matches; full results via `resourceUri`.',
24
22
  'Skips binary and oversized files.',
25
23
  ],
26
24
  gotchas: [
27
- 'Skips binary and oversized files silently — check file type with `stat` if no matches appear.',
25
+ 'Skips binary/oversized files silently — verify with `stat` if no matches.',
28
26
  ],
29
27
  taskSupport: 'optional',
30
28
  };
@@ -1,18 +1,16 @@
1
1
  import * as path from 'node:path';
2
- import { formatOperationSummary, joinLines } from '../config.js';
3
2
  import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_TIMEOUT_MS, } from '../lib/constants.js';
4
3
  import { ErrorCode } from '../lib/errors.js';
5
- import { searchFiles } from '../lib/file-operations/search-files.js';
4
+ import { searchFiles } from '../lib/file-operations/search.js';
5
+ import { formatOperationSummary, joinLines } from '../config.js';
6
6
  import { SearchFilesInputSchema, SearchFilesOutputSchema } from '../schemas.js';
7
7
  import { buildToolErrorResponse, buildToolResponse, createProgressReporter, decodeOffsetCursor, encodeOffsetCursor, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  import { registerToolTaskIfAvailable } from './task-support.js';
9
9
  export const SEARCH_FILES_TOOL = {
10
10
  name: 'find',
11
11
  title: 'Find Files',
12
- description: 'Find files by glob pattern (e.g., **/*.ts). ' +
13
- 'Returns a list of matching files with metadata. ' +
14
- 'For text search inside files, use grep. ' +
15
- 'To bulk-edit the matched files, pass the same glob pattern to search_and_replace.',
12
+ description: 'Find files by glob pattern (e.g. `**/*.ts`). Returns matching files with metadata. ' +
13
+ 'For content search, use `grep`. For bulk edits, pass the same glob to `search_and_replace`.',
16
14
  inputSchema: SearchFilesInputSchema,
17
15
  outputSchema: SearchFilesOutputSchema,
18
16
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
@@ -1,8 +1,8 @@
1
1
  import type { ContentBlock, Icon, ProgressNotificationParams } from '@modelcontextprotocol/sdk/types.js';
2
2
  import { z } from 'zod';
3
- import type { FileInfo } from '../config.js';
4
3
  import { ErrorCode } from '../lib/errors.js';
5
4
  import type { ResourceStore } from '../lib/resource-store.js';
5
+ import type { FileInfo } from '../config.js';
6
6
  export { type ToolContract } from './contract.js';
7
7
  export declare const READ_ONLY_TOOL_ANNOTATIONS: {
8
8
  readonly readOnlyHint: true;
@@ -12,6 +12,7 @@ export declare const READ_ONLY_TOOL_ANNOTATIONS: {
12
12
  };
13
13
  export declare const DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS: {
14
14
  readonly readOnlyHint: false;
15
+ readonly idempotentHint: false;
15
16
  readonly destructiveHint: true;
16
17
  readonly openWorldHint: false;
17
18
  };
@@ -5,7 +5,7 @@ import { parseTrueEnvFlag } from '../lib/constants.js';
5
5
  import { createDetailedError, ErrorCode, formatDetailedError, getSuggestion, McpError, } from '../lib/errors.js';
6
6
  import { createTimedAbortSignal } from '../lib/fs-helpers.js';
7
7
  import { withToolDiagnostics } from '../lib/observability.js';
8
- import { getAllowedDirectories } from '../lib/path-validation.js';
8
+ import { getAllowedDirectories } from '../lib/paths.js';
9
9
  export {} from './contract.js';
10
10
  const MAX_INLINE_CONTENT_CHARS = parseInt(process.env['FS_CONTEXT_MAX_INLINE_CHARS'] ?? '', 10) || 20_000;
11
11
  const MAX_INLINE_PREVIEW_CHARS = 4_000;
@@ -24,6 +24,7 @@ export const READ_ONLY_TOOL_ANNOTATIONS = {
24
24
  };
25
25
  export const DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS = {
26
26
  readOnlyHint: false,
27
+ idempotentHint: false,
27
28
  destructiveHint: true,
28
29
  openWorldHint: false,
29
30
  };
@@ -1,14 +1,15 @@
1
- import { formatBytes, joinLines } from '../config.js';
2
1
  import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
3
2
  import { ErrorCode } from '../lib/errors.js';
4
- import { getMultipleFileInfo } from '../lib/file-operations/file-info.js';
3
+ import { getMultipleFileInfo } from '../lib/file-operations/metadata.js';
4
+ import { formatBytes, joinLines } from '../config.js';
5
5
  import { GetMultipleFileInfoInputSchema, GetMultipleFileInfoOutputSchema, } from '../schemas.js';
6
6
  import { buildBatchCompletionSuffix, buildBatchPathContext, buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
7
7
  import { registerToolTaskIfAvailable } from './task-support.js';
8
8
  export const GET_MULTIPLE_FILE_INFO_TOOL = {
9
9
  name: 'stat_many',
10
10
  title: 'Get Multiple File Info',
11
- description: 'Get metadata (including tokenEstimate) for multiple files or directories in one request. Use tokenEstimate (size÷4) to pre-screen token cost before reading.',
11
+ description: 'Get metadata for multiple files/directories in one request. ' +
12
+ 'Use `tokenEstimate` (size\u00f74) to pre-screen token cost before reading.',
12
13
  inputSchema: GetMultipleFileInfoInputSchema,
13
14
  outputSchema: GetMultipleFileInfoOutputSchema,
14
15
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
@@ -1,14 +1,15 @@
1
1
  import * as path from 'node:path';
2
- import { formatBytes, joinLines } from '../config.js';
3
2
  import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
4
3
  import { ErrorCode } from '../lib/errors.js';
5
- import { getFileInfo } from '../lib/file-operations/file-info.js';
4
+ import { getFileInfo } from '../lib/file-operations/metadata.js';
5
+ import { formatBytes, joinLines } from '../config.js';
6
6
  import { GetFileInfoInputSchema, GetFileInfoOutputSchema } from '../schemas.js';
7
7
  import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  export const GET_FILE_INFO_TOOL = {
9
9
  name: 'stat',
10
10
  title: 'Get File Info',
11
- description: 'Get metadata (size, modified time, permissions, mime type, tokenEstimate) for a file or directory. Use tokenEstimate (size÷4) to pre-screen token cost before reading.',
11
+ description: 'Get file/directory metadata: size, modified, permissions, mime, tokenEstimate. ' +
12
+ 'Use `tokenEstimate` (size\u00f74) to pre-screen token cost before reading.',
12
13
  inputSchema: GetFileInfoInputSchema,
13
14
  outputSchema: GetFileInfoOutputSchema,
14
15
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
@@ -1,7 +1,7 @@
1
1
  import { channel } from 'node:diagnostics_channel';
2
2
  import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
3
3
  import { ErrorCode, McpError } from '../lib/errors.js';
4
- import { isRecord } from '../lib/type-guards.js';
4
+ import { isRecord } from '../lib/utils.js';
5
5
  import { buildToolErrorResponse, maybeStripStructuredContentFromResult, withDefaultIcons, } from './shared.js';
6
6
  function isExperimentalTaskRegistration(value) {
7
7
  if (!value || typeof value !== 'object')
@@ -1,16 +1,15 @@
1
1
  import * as path from 'node:path';
2
2
  import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
3
3
  import { ErrorCode } from '../lib/errors.js';
4
- import { formatTreeAscii, treeDirectory } from '../lib/file-operations/tree.js';
4
+ import { formatTreeAscii, treeDirectory, } from '../lib/file-operations/metadata.js';
5
5
  import { TreeInputSchema, TreeOutputSchema } from '../schemas.js';
6
6
  import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
7
7
  import { registerToolTaskIfAvailable } from './task-support.js';
8
8
  export const TREE_TOOL = {
9
9
  name: 'tree',
10
10
  title: 'Tree',
11
- description: 'Render a directory tree (bounded recursion). ' +
12
- 'Returns an ASCII tree for quick scanning and a structured JSON tree for programmatic use. ' +
13
- 'Note: maxDepth=0 returns only the root node with empty children array.',
11
+ description: 'Render a directory tree (bounded recursion). Returns ASCII tree + structured JSON. ' +
12
+ '`maxDepth=0` returns only the root node.',
14
13
  inputSchema: TreeInputSchema,
15
14
  outputSchema: TreeOutputSchema,
16
15
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
@@ -2,7 +2,7 @@ import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { ErrorCode } from '../lib/errors.js';
4
4
  import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
5
- import { validatePathForWrite } from '../lib/path-validation.js';
5
+ import { validatePathForWrite } from '../lib/paths.js';
6
6
  import { WriteFileInputSchema, WriteFileOutputSchema } from '../schemas.js';
7
7
  import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  import { registerToolTaskIfAvailable } from './task-support.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "mcpName": "io.github.j0hanz/filesystem-mcp",
5
5
  "description": "MCP Server that enables LLMs to interact with the local filesystem.",
6
6
  "type": "module",
@@ -68,7 +68,7 @@
68
68
  "commander": "^14.0.3",
69
69
  "diff": "^8.0.3",
70
70
  "ignore": "^7.0.5",
71
- "re2": "^1.23.2",
71
+ "re2": "^1.23.3",
72
72
  "safe-regex2": "^5.0.0",
73
73
  "zod": "^4.3.6"
74
74
  },
@@ -78,8 +78,8 @@
78
78
  "@types/node": "^24",
79
79
  "eslint": "^10.0.2",
80
80
  "eslint-config-prettier": "^10.1.8",
81
- "eslint-plugin-de-morgan": "^2.0.0",
82
- "eslint-plugin-depend": "^1.4.0",
81
+ "eslint-plugin-de-morgan": "^2.1.1",
82
+ "eslint-plugin-depend": "^1.5.0",
83
83
  "eslint-plugin-sonarjs": "^4.0.0",
84
84
  "eslint-plugin-unused-imports": "^4.4.1",
85
85
  "jscpd": "^4.0.8",
@@ -1,10 +0,0 @@
1
- import type { FileInfo, GetMultipleFileInfoResult } from '../../config.js';
2
- interface FileInfoOptions {
3
- includeMimeType?: boolean | undefined;
4
- signal?: AbortSignal | undefined;
5
- onProgress?: () => void;
6
- }
7
- export declare function getFileInfo(filePath: string, options?: FileInfoOptions): Promise<FileInfo>;
8
- type GetMultipleFileInfoOptions = FileInfoOptions;
9
- export declare function getMultipleFileInfo(paths: readonly string[], options?: GetMultipleFileInfoOptions): Promise<GetMultipleFileInfoResult>;
10
- export {};
@@ -1,143 +0,0 @@
1
- import * as fsp from 'node:fs/promises';
2
- import * as path from 'node:path';
3
- import { getMimeType, PARALLEL_CONCURRENCY } from '../constants.js';
4
- import { isAbortError } from '../errors.js';
5
- import { assertNotAborted, getFileType, isHidden, processInParallel, withAbort, } from '../fs-helpers.js';
6
- import { assertAllowedFileAccess } from '../path-policy.js';
7
- import { validateExistingPathDetailed } from '../path-validation.js';
8
- import { applyIndexedErrors, applyIndexedValues } from './common.js';
9
- const PERM_STRINGS = [
10
- '---',
11
- '--x',
12
- '-w-',
13
- '-wx',
14
- 'r--',
15
- 'r-x',
16
- 'rw-',
17
- 'rwx',
18
- ];
19
- const UNKNOWN_PATH = '(unknown)';
20
- function getPermissions(mode) {
21
- const ownerIndex = (mode >> 6) & 0b111;
22
- const groupIndex = (mode >> 3) & 0b111;
23
- const otherIndex = mode & 0b111;
24
- const owner = PERM_STRINGS[ownerIndex] ?? '---';
25
- const group = PERM_STRINGS[groupIndex] ?? '---';
26
- const other = PERM_STRINGS[otherIndex] ?? '---';
27
- return `${owner}${group}${other}`;
28
- }
29
- function buildFileInfoResult(name, requestedPath, isSymlink, stats, mimeType, symlinkTarget) {
30
- const tokenEstimate = stats.isFile() ? Math.ceil(stats.size / 4) : undefined;
31
- return {
32
- name,
33
- path: requestedPath,
34
- type: isSymlink ? 'symlink' : getFileType(stats),
35
- size: stats.size,
36
- ...(tokenEstimate !== undefined ? { tokenEstimate } : {}),
37
- created: stats.birthtime,
38
- modified: stats.mtime,
39
- accessed: stats.atime,
40
- permissions: getPermissions(stats.mode),
41
- isHidden: isHidden(name),
42
- ...(mimeType !== undefined ? { mimeType } : {}),
43
- ...(symlinkTarget !== undefined ? { symlinkTarget } : {}),
44
- };
45
- }
46
- async function getSymlinkTarget(pathToRead, signal) {
47
- assertNotAborted(signal);
48
- try {
49
- return await withAbort(fsp.readlink(pathToRead), signal);
50
- }
51
- catch (error) {
52
- if (isAbortError(error))
53
- throw error;
54
- return undefined;
55
- }
56
- }
57
- export async function getFileInfo(filePath, options = {}) {
58
- const { signal } = options;
59
- assertNotAborted(signal);
60
- const { requestedPath, resolvedPath, isSymlink } = await validateExistingPathDetailed(filePath, signal);
61
- assertAllowedFileAccess(requestedPath, resolvedPath);
62
- const name = path.basename(requestedPath);
63
- const ext = path.extname(name).toLowerCase();
64
- const includeMimeType = options.includeMimeType !== false;
65
- const mimeType = includeMimeType && ext.length > 0 ? getMimeType(ext) : undefined;
66
- const symlinkTarget = isSymlink
67
- ? await getSymlinkTarget(requestedPath, signal)
68
- : undefined;
69
- const stats = await withAbort(fsp.stat(resolvedPath), signal);
70
- return buildFileInfoResult(name, requestedPath, isSymlink, stats, mimeType, symlinkTarget);
71
- }
72
- function buildEmptyResult() {
73
- return {
74
- results: [],
75
- summary: { total: 0, succeeded: 0, failed: 0, totalSize: 0 },
76
- };
77
- }
78
- async function processFileInfo(filePath, options) {
79
- const info = await getFileInfo(filePath, options);
80
- return { path: filePath, info };
81
- }
82
- function buildIndexedPathTasks(paths) {
83
- const tasks = [];
84
- for (let index = 0; index < paths.length; index += 1) {
85
- const filePath = paths[index];
86
- if (filePath === undefined)
87
- continue;
88
- tasks.push({ filePath, index });
89
- }
90
- return tasks;
91
- }
92
- async function readFileInfoInParallel(paths, options) {
93
- return processInParallel(buildIndexedPathTasks(paths), async ({ filePath, index }) => {
94
- const value = await processFileInfo(filePath, options);
95
- options.onProgress?.();
96
- return { index, value };
97
- }, PARALLEL_CONCURRENCY, options.signal);
98
- }
99
- function calculateSummary(results) {
100
- let succeeded = 0;
101
- let failed = 0;
102
- let totalSize = 0;
103
- for (const result of results) {
104
- if (result.info !== undefined) {
105
- succeeded++;
106
- totalSize += result.info.size;
107
- }
108
- else {
109
- failed++;
110
- }
111
- }
112
- return {
113
- total: results.length,
114
- succeeded,
115
- failed,
116
- totalSize,
117
- };
118
- }
119
- export async function getMultipleFileInfo(paths, options = {}) {
120
- if (paths.length === 0)
121
- return buildEmptyResult();
122
- const output = new Array(paths.length);
123
- for (let index = 0; index < paths.length; index += 1) {
124
- output[index] = { path: paths[index] ?? UNKNOWN_PATH };
125
- }
126
- const { results, errors } = await readFileInfoInParallel(paths, options);
127
- applyIndexedValues(output, results);
128
- applyIndexedErrors({
129
- output,
130
- errors,
131
- resolveIndex: (failureIndex) => failureIndex >= 0 && failureIndex < output.length
132
- ? failureIndex
133
- : undefined,
134
- buildValue: (resolvedIndex, error) => ({
135
- path: paths[resolvedIndex] ?? UNKNOWN_PATH,
136
- error: error.message,
137
- }),
138
- });
139
- return {
140
- results: output,
141
- summary: calculateSummary(output),
142
- };
143
- }
@@ -1,6 +0,0 @@
1
- import { type Ignore } from 'ignore';
2
- export declare function loadRootGitignore(root: string, signal?: AbortSignal): Promise<Ignore | null>;
3
- export declare function isIgnoredByGitignore(matcher: Ignore, root: string, absolutePath: string, options?: {
4
- isDirectory?: boolean;
5
- relativePath?: string;
6
- }): boolean;