@j0hanz/filesystem-mcp 1.7.3 → 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 (76) 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} +29 -3
  10. package/dist/lib/file-operations/{search-content.js → search.js} +409 -82
  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.d.ts +1 -0
  14. package/dist/lib/fs-helpers.js +10 -2
  15. package/dist/lib/observability.js +1 -1
  16. package/dist/lib/{path-validation.d.ts → paths.d.ts} +4 -0
  17. package/dist/lib/{path-validation.js → paths.js} +112 -1
  18. package/dist/lib/utils.d.ts +15 -0
  19. package/dist/lib/utils.js +33 -0
  20. package/dist/resources/generated-instructions.js +6 -6
  21. package/dist/resources/tool-catalog.js +9 -9
  22. package/dist/resources/tool-info.js +3 -3
  23. package/dist/resources/workflows.js +10 -23
  24. package/dist/schemas.js +15 -15
  25. package/dist/server/bootstrap.d.ts +19 -1
  26. package/dist/server/bootstrap.js +103 -9
  27. package/dist/server/roots-manager.d.ts +2 -2
  28. package/dist/server/roots-manager.js +3 -3
  29. package/dist/tools/apply-patch.js +7 -3
  30. package/dist/tools/calculate-hash.js +15 -15
  31. package/dist/tools/create-directory.js +1 -1
  32. package/dist/tools/delete-file.js +7 -4
  33. package/dist/tools/diff-files.js +2 -2
  34. package/dist/tools/edit-file.js +14 -13
  35. package/dist/tools/list-directory.js +5 -7
  36. package/dist/tools/move-file.js +2 -1
  37. package/dist/tools/read-multiple.js +13 -26
  38. package/dist/tools/read.js +3 -3
  39. package/dist/tools/replace-in-files.js +17 -20
  40. package/dist/tools/roots.js +4 -6
  41. package/dist/tools/search-content.js +9 -11
  42. package/dist/tools/search-files.js +4 -6
  43. package/dist/tools/shared.d.ts +18 -1
  44. package/dist/tools/shared.js +39 -19
  45. package/dist/tools/stat-many.js +14 -24
  46. package/dist/tools/stat.js +4 -3
  47. package/dist/tools/task-support.js +1 -1
  48. package/dist/tools/tree.js +3 -4
  49. package/dist/tools/write-file.js +1 -1
  50. package/package.json +9 -5
  51. package/dist/lib/file-operations/file-info.d.ts +0 -10
  52. package/dist/lib/file-operations/file-info.js +0 -143
  53. package/dist/lib/file-operations/gitignore.d.ts +0 -6
  54. package/dist/lib/file-operations/gitignore.js +0 -45
  55. package/dist/lib/file-operations/list-directory.d.ts +0 -14
  56. package/dist/lib/file-operations/list-directory.js +0 -256
  57. package/dist/lib/file-operations/read-multiple-files.d.ts +0 -25
  58. package/dist/lib/file-operations/read-multiple-files.js +0 -252
  59. package/dist/lib/file-operations/search-files.d.ts +0 -27
  60. package/dist/lib/file-operations/search-files.js +0 -218
  61. package/dist/lib/file-operations/search-worker.d.ts +0 -2
  62. package/dist/lib/file-operations/search-worker.js +0 -129
  63. package/dist/lib/file-operations/tree.d.ts +0 -28
  64. package/dist/lib/file-operations/tree.js +0 -269
  65. package/dist/lib/path-format.d.ts +0 -1
  66. package/dist/lib/path-format.js +0 -7
  67. package/dist/lib/path-policy.d.ts +0 -2
  68. package/dist/lib/path-policy.js +0 -100
  69. package/dist/lib/type-guards.d.ts +0 -1
  70. package/dist/lib/type-guards.js +0 -3
  71. package/dist/server/capabilities.d.ts +0 -10
  72. package/dist/server/capabilities.js +0 -48
  73. package/dist/server/logging.d.ts +0 -7
  74. package/dist/server/logging.js +0 -41
  75. package/dist/server/types.d.ts +0 -4
  76. package/dist/server/types.js +0 -1
@@ -1,30 +1,28 @@
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
- import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
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';
10
10
  const MAX_INLINE_MATCHES = parseInt(process.env['FS_CONTEXT_MAX_INLINE_MATCHES'] ?? '', 10) || 50;
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
  };
@@ -242,7 +240,7 @@ export function registerSearchContentTool(server, options = {}) {
242
240
  suffix += ' [truncated — max files]';
243
241
  }
244
242
  }
245
- const finalCurrent = Math.max((sc.filesScanned ?? 0) + 1, progress.getCurrent() + 1);
243
+ const finalCurrent = resolveFinalProgressCurrent(progress, (sc.filesScanned ?? 0) + 1);
246
244
  progress.complete(`🔎︎ grep: ${pattern} • ${suffix}`, finalCurrent);
247
245
  return result;
248
246
  }
@@ -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
  };
@@ -123,7 +124,18 @@ export interface ToolProgressSession {
123
124
  fail: (message: string, minimumCurrent?: number) => void;
124
125
  getCurrent: () => number;
125
126
  }
127
+ export interface BatchProgressCallbacks {
128
+ progress: ToolProgressSession;
129
+ onItemComplete: () => void;
130
+ }
126
131
  export declare function createToolProgressSession(extra: ToolExtra, startMessage: string): ToolProgressSession;
132
+ export declare function createBatchProgressCallbacks(extra: ToolExtra, params: {
133
+ toolLabel: string;
134
+ context: string;
135
+ totalItems: number;
136
+ itemVerb: string;
137
+ }): BatchProgressCallbacks;
138
+ export declare function resolveFinalProgressCurrent(progress: ToolProgressSession, ...candidates: number[]): number;
127
139
  export declare function wrapToolHandler<Args, Result>(handler: (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>, options: {
128
140
  guard?: (() => boolean) | undefined;
129
141
  progressMessage?: (args: Args) => string;
@@ -142,3 +154,8 @@ export declare function resolvePathOrRoot(pathValue: string | undefined): string
142
154
  export declare function encodeOffsetCursor(offset: number): string;
143
155
  export declare function decodeOffsetCursor(cursor: string): number;
144
156
  export declare function buildBatchPathContext(paths: readonly string[], unitLabel?: string): string;
157
+ export declare function buildBatchCompletionSuffix(summary: {
158
+ total?: number;
159
+ failed?: number;
160
+ succeeded?: number;
161
+ } | undefined, successWord: string, singularWord?: string): string;
@@ -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
  };
@@ -293,6 +294,15 @@ export function createToolProgressSession(extra, startMessage) {
293
294
  cursor = value;
294
295
  return cursor;
295
296
  };
297
+ const finishProgress = (message, minimumCurrent) => {
298
+ const finalCurrent = Math.max(cursor + 1, minimumCurrent ?? 1, 1);
299
+ notifyProgress(extra, {
300
+ current: finalCurrent,
301
+ total: finalCurrent,
302
+ ...(message !== undefined ? { message } : {}),
303
+ });
304
+ cursor = finalCurrent;
305
+ };
296
306
  return {
297
307
  update: ({ current, total, message }) => {
298
308
  const normalized = setCursor(current);
@@ -309,27 +319,27 @@ export function createToolProgressSession(extra, startMessage) {
309
319
  message: messageForCurrent(next),
310
320
  });
311
321
  },
312
- complete: (message, minimumCurrent) => {
313
- const finalCurrent = Math.max(cursor + 1, minimumCurrent ?? 1, 1);
314
- notifyProgress(extra, {
315
- current: finalCurrent,
316
- total: finalCurrent,
317
- message,
318
- });
319
- cursor = finalCurrent;
320
- },
321
- fail: (message, minimumCurrent) => {
322
- const finalCurrent = Math.max(cursor + 1, minimumCurrent ?? 1, 1);
323
- notifyProgress(extra, {
324
- current: finalCurrent,
325
- total: finalCurrent,
326
- message,
327
- });
328
- cursor = finalCurrent;
329
- },
322
+ complete: finishProgress,
323
+ fail: finishProgress,
330
324
  getCurrent: () => cursor,
331
325
  };
332
326
  }
327
+ export function createBatchProgressCallbacks(extra, params) {
328
+ const progress = createToolProgressSession(extra, `${params.toolLabel}: ${params.context}`);
329
+ const onItemComplete = () => {
330
+ progress.increment((current) => `${params.toolLabel}: ${params.context} [${current}/${params.totalItems} ${params.itemVerb}]`);
331
+ };
332
+ return { progress, onItemComplete };
333
+ }
334
+ export function resolveFinalProgressCurrent(progress, ...candidates) {
335
+ let finalCurrent = progress.getCurrent() + 1;
336
+ for (const candidate of candidates) {
337
+ if (candidate > finalCurrent) {
338
+ finalCurrent = candidate;
339
+ }
340
+ }
341
+ return finalCurrent;
342
+ }
333
343
  async function withProgress(message, extra, run, getCompletionMessage) {
334
344
  if (!canReportProgress(extra)) {
335
345
  return run();
@@ -430,3 +440,13 @@ export function buildBatchPathContext(paths, unitLabel = 'paths') {
430
440
  : '';
431
441
  return `${paths.length} ${normalizedLabel} [${first}${extraPaths}]`;
432
442
  }
443
+ export function buildBatchCompletionSuffix(summary, successWord, singularWord) {
444
+ const total = summary?.total ?? 0;
445
+ const failed = summary?.failed ?? 0;
446
+ const succeeded = summary?.succeeded ?? 0;
447
+ if (failed) {
448
+ return `${succeeded}/${total} ${successWord}, ${failed} failed`;
449
+ }
450
+ const word = total === 1 && singularWord ? singularWord : successWord;
451
+ return `${total} ${word}`;
452
+ }
@@ -1,36 +1,21 @@
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
- import { buildBatchPathContext, buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
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,
15
16
  taskSupport: 'optional',
16
17
  nuances: ['Use before read/search when file size/type uncertainty exists.'],
17
18
  };
18
- function buildStatManyCompletionSuffix(summary) {
19
- const total = summary?.total ?? 0;
20
- const failed = summary?.failed ?? 0;
21
- const succeeded = summary?.succeeded ?? 0;
22
- if (failed) {
23
- return `${succeeded}/${total} OK, ${failed} failed`;
24
- }
25
- return `${total} OK`;
26
- }
27
- function createStatManyProgressCallbacks(extra, context, totalPaths) {
28
- const progress = createToolProgressSession(extra, `🕮 stat_many: ${context}`);
29
- const onProgress = () => {
30
- progress.increment((current) => `🕮 stat_many: ${context} [${current}/${totalPaths} scanned]`);
31
- };
32
- return { progress, onProgress };
33
- }
34
19
  function formatFileInfoDetail(info) {
35
20
  const lines = [
36
21
  `${info.name} (${info.type})`,
@@ -90,13 +75,18 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
90
75
  context: { path: primaryPath },
91
76
  run: async (signal) => {
92
77
  const context = buildBatchPathContext(args.paths);
93
- const { progress, onProgress } = createStatManyProgressCallbacks(extra, context, args.paths.length);
78
+ const { progress, onItemComplete } = createBatchProgressCallbacks(extra, {
79
+ toolLabel: '🕮 stat_many',
80
+ context,
81
+ totalItems: args.paths.length,
82
+ itemVerb: 'scanned',
83
+ });
94
84
  try {
95
- const result = await handleGetMultipleFileInfo(args, signal, onProgress);
85
+ const result = await handleGetMultipleFileInfo(args, signal, onItemComplete);
96
86
  const sc = result.structuredContent;
97
- const suffix = buildStatManyCompletionSuffix(sc.summary);
87
+ const suffix = buildBatchCompletionSuffix(sc.summary, 'OK');
98
88
  const total = sc.summary?.total ?? 0;
99
- const finalCurrent = Math.max(total, progress.getCurrent() + 1);
89
+ const finalCurrent = resolveFinalProgressCurrent(progress, total);
100
90
  progress.complete(`🕮 stat_many: ${context} • ${suffix}`, finalCurrent);
101
91
  return result;
102
92
  }
@@ -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.7.3",
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",
@@ -35,8 +35,11 @@
35
35
  "type-check:diagnostics": "tsc --noEmit --extendedDiagnostics",
36
36
  "type-check:trace": "node -e \"require('fs').rmSync('.ts-trace',{recursive:true,force:true})\" && tsc --noEmit --generateTrace .ts-trace",
37
37
  "lint": "eslint .",
38
+ "lint:tests": "eslint src/__tests__",
38
39
  "lint:fix": "eslint . --fix",
39
40
  "test": "node scripts/tasks.mjs test",
41
+ "test:fast": "node --test --import tsx/esm src/__tests__/**/*.test.ts node-tests/**/*.test.ts",
42
+ "test:coverage": "node scripts/tasks.mjs test --coverage",
40
43
  "knip": "knip",
41
44
  "knip:fix": "knip --fix",
42
45
  "inspector": "npm run build && npx -y @modelcontextprotocol/inspector node dist/index.js ${workspaceFolder}",
@@ -65,18 +68,19 @@
65
68
  "commander": "^14.0.3",
66
69
  "diff": "^8.0.3",
67
70
  "ignore": "^7.0.5",
68
- "re2": "^1.23.2",
71
+ "re2": "^1.23.3",
69
72
  "safe-regex2": "^5.0.0",
70
73
  "zod": "^4.3.6"
71
74
  },
72
75
  "devDependencies": {
73
76
  "@eslint/js": "^10.0.1",
74
- "eslint": "^10.0.2",
75
77
  "@trivago/prettier-plugin-sort-imports": "^6.0.2",
76
78
  "@types/node": "^24",
79
+ "eslint": "^10.0.2",
77
80
  "eslint-config-prettier": "^10.1.8",
78
- "eslint-plugin-de-morgan": "^2.0.0",
79
- "eslint-plugin-depend": "^1.4.0",
81
+ "eslint-plugin-de-morgan": "^2.1.1",
82
+ "eslint-plugin-depend": "^1.5.0",
83
+ "eslint-plugin-sonarjs": "^4.0.0",
80
84
  "eslint-plugin-unused-imports": "^4.4.1",
81
85
  "jscpd": "^4.0.8",
82
86
  "knip": "^5.85.0",
@@ -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;
@@ -1,45 +0,0 @@
1
- import * as fs from 'node:fs/promises';
2
- import * as path from 'node:path';
3
- import ignore, {} from 'ignore';
4
- import { isNodeError } from '../errors.js';
5
- import { toPosixPath } from '../path-format.js';
6
- function parseGitignoreLines(contents) {
7
- const lines = [];
8
- for (const line of contents.split(/\r?\n/u)) {
9
- const trimmed = line.trim();
10
- if (trimmed.length > 0) {
11
- lines.push(trimmed);
12
- }
13
- }
14
- return lines;
15
- }
16
- export async function loadRootGitignore(root, signal) {
17
- const gitignorePath = path.join(root, '.gitignore');
18
- let contents;
19
- try {
20
- contents = await fs.readFile(gitignorePath, {
21
- encoding: 'utf-8',
22
- signal,
23
- });
24
- }
25
- catch (error) {
26
- if (isNodeError(error) && error.code === 'ENOENT') {
27
- return null;
28
- }
29
- throw error;
30
- }
31
- const matcher = ignore();
32
- matcher.add(parseGitignoreLines(contents));
33
- return matcher;
34
- }
35
- export function isIgnoredByGitignore(matcher, root, absolutePath, options = {}) {
36
- let relative = options.relativePath;
37
- relative ??= path.relative(root, absolutePath);
38
- if (relative.length === 0)
39
- return false;
40
- const normalized = toPosixPath(relative);
41
- if (options.isDirectory) {
42
- return matcher.ignores(normalized.endsWith('/') ? normalized : `${normalized}/`);
43
- }
44
- return matcher.ignores(normalized);
45
- }
@@ -1,14 +0,0 @@
1
- import type { ListDirectoryResult } from '../../config.js';
2
- interface ListDirectoryOptions {
3
- includeHidden?: boolean;
4
- excludePatterns?: readonly string[];
5
- maxDepth?: number;
6
- maxEntries?: number;
7
- sortBy?: 'name' | 'size' | 'modified' | 'type';
8
- includeSymlinkTargets?: boolean;
9
- pattern?: string;
10
- timeoutMs?: number;
11
- signal?: AbortSignal;
12
- }
13
- export declare function listDirectory(dirPath: string, options?: ListDirectoryOptions): Promise<ListDirectoryResult>;
14
- export {};