@j0hanz/filesystem-mcp 1.2.4 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +8 -0
  2. package/dist/completions.d.ts +1 -1
  3. package/dist/completions.js +36 -1
  4. package/dist/lib/observability.d.ts +6 -0
  5. package/dist/lib/observability.js +1 -1
  6. package/dist/lib/resource-store.js +53 -0
  7. package/dist/prompts.js +34 -14
  8. package/dist/resources/generated-instructions.d.ts +1 -0
  9. package/dist/resources/generated-instructions.js +100 -0
  10. package/dist/resources.d.ts +1 -0
  11. package/dist/resources.js +36 -1
  12. package/dist/schemas.d.ts +6 -0
  13. package/dist/schemas.js +24 -0
  14. package/dist/server/bootstrap.js +45 -22
  15. package/dist/server.d.ts +1 -1
  16. package/dist/server.js +1 -1
  17. package/dist/tools/apply-patch.d.ts +2 -1
  18. package/dist/tools/apply-patch.js +7 -5
  19. package/dist/tools/calculate-hash.d.ts +2 -1
  20. package/dist/tools/calculate-hash.js +9 -5
  21. package/dist/tools/contract.d.ts +41 -0
  22. package/dist/tools/contract.js +1 -0
  23. package/dist/tools/create-directory.d.ts +2 -1
  24. package/dist/tools/create-directory.js +6 -5
  25. package/dist/tools/delete-file.d.ts +2 -1
  26. package/dist/tools/delete-file.js +9 -5
  27. package/dist/tools/diff-files.d.ts +2 -1
  28. package/dist/tools/diff-files.js +7 -4
  29. package/dist/tools/edit-file.d.ts +2 -1
  30. package/dist/tools/edit-file.js +19 -6
  31. package/dist/tools/list-directory.d.ts +2 -1
  32. package/dist/tools/list-directory.js +36 -7
  33. package/dist/tools/move-file.d.ts +2 -1
  34. package/dist/tools/move-file.js +7 -5
  35. package/dist/tools/read-multiple.d.ts +2 -1
  36. package/dist/tools/read-multiple.js +10 -5
  37. package/dist/tools/read.d.ts +2 -1
  38. package/dist/tools/read.js +9 -5
  39. package/dist/tools/replace-in-files.d.ts +2 -1
  40. package/dist/tools/replace-in-files.js +14 -7
  41. package/dist/tools/roots.d.ts +2 -1
  42. package/dist/tools/roots.js +7 -4
  43. package/dist/tools/search-content.d.ts +2 -1
  44. package/dist/tools/search-content.js +14 -6
  45. package/dist/tools/search-files.d.ts +2 -1
  46. package/dist/tools/search-files.js +39 -7
  47. package/dist/tools/shared.d.ts +2 -2
  48. package/dist/tools/shared.js +16 -1
  49. package/dist/tools/stat-many.d.ts +2 -1
  50. package/dist/tools/stat-many.js +7 -5
  51. package/dist/tools/stat.d.ts +2 -1
  52. package/dist/tools/stat.js +7 -4
  53. package/dist/tools/task-support.d.ts +2 -0
  54. package/dist/tools/task-support.js +61 -9
  55. package/dist/tools/tree.d.ts +2 -1
  56. package/dist/tools/tree.js +7 -5
  57. package/dist/tools/write-file.d.ts +2 -1
  58. package/dist/tools/write-file.js +12 -5
  59. package/dist/tools.d.ts +2 -0
  60. package/dist/tools.js +39 -18
  61. package/package.json +1 -2
  62. package/dist/instructions.md +0 -200
package/dist/server.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { createServer, startHttpServer, startServer } from './server/bootstrap.js';
1
+ export { createServer, startHttpServer, startServer, } from './server/bootstrap.js';
2
2
  export type { ServerOptions } from './server/types.js';
package/dist/server.js CHANGED
@@ -1 +1 @@
1
- export { createServer, startHttpServer, startServer } from './server/bootstrap.js';
1
+ export { createServer, startHttpServer, startServer, } from './server/bootstrap.js';
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const APPLY_PATCH_TOOL: ToolContract;
3
4
  export declare function registerApplyPatchTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -8,7 +8,8 @@ import { validateExistingPath } from '../lib/path-validation.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';
11
- const APPLY_PATCH_TOOL = {
11
+ export const APPLY_PATCH_TOOL = {
12
+ name: 'apply_patch',
12
13
  title: 'Apply Patch',
13
14
  description: 'Apply a unified diff patch to a file. ' +
14
15
  'Generate the patch with `diff_files`, then validate with `dryRun: true` before writing. ' +
@@ -16,6 +17,7 @@ const APPLY_PATCH_TOOL = {
16
17
  inputSchema: ApplyPatchInputSchema,
17
18
  outputSchema: ApplyPatchOutputSchema,
18
19
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
20
+ gotchas: ['Patch must include valid hunk headers; use `dryRun=true` first.'],
19
21
  };
20
22
  function assertPatchTargetSizeWithinLimit(filePath, size, maxFileSize) {
21
23
  if (size <= maxFileSize)
@@ -69,8 +71,7 @@ export function registerApplyPatchTool(server, options = {}) {
69
71
  run: (signal) => handleApplyPatch(args, signal),
70
72
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
71
73
  });
72
- const validatedHandler = withValidatedArgs(ApplyPatchInputSchema, handler);
73
- const wrappedHandler = wrapToolHandler(validatedHandler, {
74
+ const wrappedHandler = wrapToolHandler(handler, {
74
75
  guard: options.isInitialized,
75
76
  progressMessage: (args) => {
76
77
  const name = path.basename(args.path);
@@ -90,7 +91,8 @@ export function registerApplyPatchTool(server, options = {}) {
90
91
  return `🛠 apply_patch: ${name} • applied`;
91
92
  },
92
93
  });
93
- if (registerToolTaskIfAvailable(server, 'apply_patch', APPLY_PATCH_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
94
+ const validatedHandler = withValidatedArgs(ApplyPatchInputSchema, wrappedHandler);
95
+ if (registerToolTaskIfAvailable(server, 'apply_patch', APPLY_PATCH_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
94
96
  return;
95
- server.registerTool('apply_patch', withDefaultIcons({ ...APPLY_PATCH_TOOL }, options.iconInfo), wrappedHandler);
97
+ server.registerTool('apply_patch', withDefaultIcons({ ...APPLY_PATCH_TOOL }, options.iconInfo), validatedHandler);
96
98
  }
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const CALCULATE_HASH_TOOL: ToolContract;
3
4
  export declare function registerCalculateHashTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -12,12 +12,16 @@ import { CalculateHashInputSchema, CalculateHashOutputSchema, } from '../schemas
12
12
  import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
13
13
  import { registerToolTaskIfAvailable } from './task-support.js';
14
14
  const WINDOWS_PATH_SEPARATOR = /\\/gu;
15
- const CALCULATE_HASH_TOOL = {
15
+ export const CALCULATE_HASH_TOOL = {
16
+ name: 'calculate_hash',
16
17
  title: 'Calculate Hash',
17
18
  description: 'Calculate SHA-256 hash of a file or directory.',
18
19
  inputSchema: CalculateHashInputSchema,
19
20
  outputSchema: CalculateHashOutputSchema,
20
21
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
22
+ nuances: [
23
+ 'Directory hashing respects root `.gitignore` and sorts paths for stable output.',
24
+ ],
21
25
  };
22
26
  async function hashFile(filePath, encoding, signal) {
23
27
  assertNotAborted(signal);
@@ -204,11 +208,11 @@ export function registerCalculateHashTool(server, options = {}) {
204
208
  },
205
209
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
206
210
  });
207
- const validatedHandler = withValidatedArgs(CalculateHashInputSchema, handler);
208
- const wrappedHandler = wrapToolHandler(validatedHandler, {
211
+ const wrappedHandler = wrapToolHandler(handler, {
209
212
  guard: options.isInitialized,
210
213
  });
211
- if (registerToolTaskIfAvailable(server, 'calculate_hash', CALCULATE_HASH_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
214
+ const validatedHandler = withValidatedArgs(CalculateHashInputSchema, wrappedHandler);
215
+ if (registerToolTaskIfAvailable(server, 'calculate_hash', CALCULATE_HASH_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
212
216
  return;
213
- server.registerTool('calculate_hash', withDefaultIcons({ ...CALCULATE_HASH_TOOL }, options.iconInfo), wrappedHandler);
217
+ server.registerTool('calculate_hash', withDefaultIcons({ ...CALCULATE_HASH_TOOL }, options.iconInfo), validatedHandler);
214
218
  }
@@ -0,0 +1,41 @@
1
+ import type { ZodType } from 'zod';
2
+ export interface ToolContract {
3
+ /**
4
+ * The unique name of the tool (e.g., "read", "grep").
5
+ * This name is used in registration and client calls.
6
+ */
7
+ name: string;
8
+ /**
9
+ * A short human-readable title for documentation (e.g., "Read File").
10
+ */
11
+ title: string;
12
+ /**
13
+ * A detailed description of what the tool does.
14
+ */
15
+ description: string;
16
+ /**
17
+ * Zod schema for the tool's input arguments.
18
+ */
19
+ inputSchema: ZodType;
20
+ /**
21
+ * Zod schema for the tool's output result (optional).
22
+ */
23
+ outputSchema?: ZodType;
24
+ /**
25
+ * Optional annotations for tool behavior hints.
26
+ */
27
+ annotations?: {
28
+ readOnlyHint?: boolean;
29
+ idempotentHint?: boolean;
30
+ destructiveHint?: boolean;
31
+ openWorldHint?: boolean;
32
+ };
33
+ /**
34
+ * Specific usage nuances or edge cases for documentation.
35
+ */
36
+ nuances?: string[];
37
+ /**
38
+ * Common pitfalls or warnings for documentation.
39
+ */
40
+ gotchas?: string[];
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const CREATE_DIRECTORY_TOOL: ToolContract;
3
4
  export declare function registerCreateDirectoryTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -6,7 +6,8 @@ import { validatePathForWrite } from '../lib/path-validation.js';
6
6
  import { CreateDirectoryInputSchema, CreateDirectoryOutputSchema, } from '../schemas.js';
7
7
  import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, IDEMPOTENT_WRITE_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  import { registerToolTaskIfAvailable } from './task-support.js';
9
- const CREATE_DIRECTORY_TOOL = {
9
+ export const CREATE_DIRECTORY_TOOL = {
10
+ name: 'mkdir',
10
11
  title: 'Create Directory',
11
12
  description: 'Create a new directory at the specified path (recursive)',
12
13
  inputSchema: CreateDirectoryInputSchema,
@@ -30,8 +31,7 @@ export function registerCreateDirectoryTool(server, options = {}) {
30
31
  run: (signal) => handleCreateDirectory(args, signal),
31
32
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
32
33
  });
33
- const validatedHandler = withValidatedArgs(CreateDirectoryInputSchema, handler);
34
- const wrappedHandler = wrapToolHandler(validatedHandler, {
34
+ const wrappedHandler = wrapToolHandler(handler, {
35
35
  guard: options.isInitialized,
36
36
  progressMessage: (args) => {
37
37
  const name = path.basename(args.path) || args.path;
@@ -44,7 +44,8 @@ export function registerCreateDirectoryTool(server, options = {}) {
44
44
  return `🛠 mkdir: ${name} • created`;
45
45
  },
46
46
  });
47
- if (registerToolTaskIfAvailable(server, 'mkdir', CREATE_DIRECTORY_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
47
+ const validatedHandler = withValidatedArgs(CreateDirectoryInputSchema, wrappedHandler);
48
+ if (registerToolTaskIfAvailable(server, 'mkdir', CREATE_DIRECTORY_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
48
49
  return;
49
- server.registerTool('mkdir', withDefaultIcons({ ...CREATE_DIRECTORY_TOOL }, options.iconInfo), wrappedHandler);
50
+ server.registerTool('mkdir', withDefaultIcons({ ...CREATE_DIRECTORY_TOOL }, options.iconInfo), validatedHandler);
50
51
  }
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const DELETE_FILE_TOOL: ToolContract;
3
4
  export declare function registerDeleteFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -6,12 +6,16 @@ import { validatePathForWrite } from '../lib/path-validation.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';
9
- const DELETE_FILE_TOOL = {
9
+ export const DELETE_FILE_TOOL = {
10
+ name: 'rm',
10
11
  title: 'Delete File',
11
12
  description: 'Delete a file or directory.',
12
13
  inputSchema: DeleteFileInputSchema,
13
14
  outputSchema: DeleteFileOutputSchema,
14
15
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
16
+ gotchas: [
17
+ 'Non-empty directory delete requires `recursive=true`; else returns actionable input error.',
18
+ ],
15
19
  };
16
20
  async function handleDeleteFile(args, signal) {
17
21
  const validPath = await validatePathForWrite(args.path, signal);
@@ -74,8 +78,7 @@ export function registerDeleteFileTool(server, options = {}) {
74
78
  return buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path);
75
79
  },
76
80
  });
77
- const validatedHandler = withValidatedArgs(DeleteFileInputSchema, handler);
78
- const wrappedHandler = wrapToolHandler(validatedHandler, {
81
+ const wrappedHandler = wrapToolHandler(handler, {
79
82
  guard: options.isInitialized,
80
83
  progressMessage: (args) => `🛠 rm: ${path.basename(args.path)}`,
81
84
  completionMessage: (args, result) => {
@@ -85,7 +88,8 @@ export function registerDeleteFileTool(server, options = {}) {
85
88
  return `🛠 rm: ${name} • deleted`;
86
89
  },
87
90
  });
88
- if (registerToolTaskIfAvailable(server, 'rm', DELETE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
91
+ const validatedHandler = withValidatedArgs(DeleteFileInputSchema, wrappedHandler);
92
+ if (registerToolTaskIfAvailable(server, 'rm', DELETE_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
89
93
  return;
90
- server.registerTool('rm', withDefaultIcons({ ...DELETE_FILE_TOOL }, options.iconInfo), wrappedHandler);
94
+ server.registerTool('rm', withDefaultIcons({ ...DELETE_FILE_TOOL }, options.iconInfo), validatedHandler);
91
95
  }
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const DIFF_FILES_TOOL: ToolContract;
3
4
  export declare function registerDiffFilesTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -7,7 +7,8 @@ import { withAbort } from '../lib/fs-helpers.js';
7
7
  import { validateExistingPath } from '../lib/path-validation.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
- const DIFF_FILES_TOOL = {
10
+ export const DIFF_FILES_TOOL = {
11
+ name: 'diff_files',
11
12
  title: 'Diff Files',
12
13
  description: 'Generate a unified diff between two files. ' +
13
14
  'Output feeds directly into `apply_patch`. ' +
@@ -15,6 +16,7 @@ const DIFF_FILES_TOOL = {
15
16
  inputSchema: DiffFilesInputSchema,
16
17
  outputSchema: DiffFilesOutputSchema,
17
18
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
19
+ gotchas: ['`isIdentical=true` means no hunks (`@@`) and empty diff.'],
18
20
  };
19
21
  function assertDiffFileSizeWithinLimit(filePath, size, maxFileSize) {
20
22
  if (size <= maxFileSize)
@@ -80,8 +82,7 @@ export function registerDiffFilesTool(server, options = {}) {
80
82
  run: (signal) => handleDiffFiles(args, signal, options.resourceStore),
81
83
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.original),
82
84
  });
83
- const validatedHandler = withValidatedArgs(DiffFilesInputSchema, handler);
84
- server.registerTool('diff_files', withDefaultIcons({ ...DIFF_FILES_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
85
+ const wrappedHandler = wrapToolHandler(handler, {
85
86
  guard: options.isInitialized,
86
87
  progressMessage: (args) => {
87
88
  const name1 = path.basename(args.original);
@@ -101,5 +102,7 @@ export function registerDiffFilesTool(server, options = {}) {
101
102
  const hunks = (sc.diff?.match(/@@/g) ?? []).length;
102
103
  return `🕮 diff_files: ${n1} ⟷ ${n2} • ${hunks} hunk${hunks !== 1 ? 's' : ''}`;
103
104
  },
104
- }));
105
+ });
106
+ const validatedHandler = withValidatedArgs(DiffFilesInputSchema, wrappedHandler);
107
+ server.registerTool('diff_files', withDefaultIcons({ ...DIFF_FILES_TOOL }, options.iconInfo), validatedHandler);
105
108
  }
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const EDIT_FILE_TOOL: ToolContract;
3
4
  export declare function registerEditFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -5,7 +5,8 @@ import { atomicWriteFile } from '../lib/fs-helpers.js';
5
5
  import { validateExistingPath } from '../lib/path-validation.js';
6
6
  import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
7
7
  import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
- const EDIT_FILE_TOOL = {
8
+ export const EDIT_FILE_TOOL = {
9
+ name: 'edit',
9
10
  title: 'Edit File',
10
11
  description: 'Edit a file by replacing text. Sequentially applies a list of string replacements. ' +
11
12
  'Replaces the first occurrence of each `oldText`. ' +
@@ -14,6 +15,12 @@ const EDIT_FILE_TOOL = {
14
15
  inputSchema: EditFileInputSchema,
15
16
  outputSchema: EditFileOutputSchema,
16
17
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
18
+ nuances: [
19
+ 'Apply sequential literal replacements (first occurrence per edit).',
20
+ ],
21
+ gotchas: [
22
+ '`oldText` must match exactly; unmatched items are reported in `unmatchedEdits`.',
23
+ ],
17
24
  };
18
25
  function applyEdits(content, edits) {
19
26
  let newContent = content;
@@ -65,9 +72,14 @@ async function handleEditFile(args, signal) {
65
72
  if (appliedEdits > 0) {
66
73
  await atomicWriteFile(validPath, newContent, { encoding: 'utf-8', signal });
67
74
  }
75
+ const unmatchedNote = unmatchedEdits.length > 0
76
+ ? ` — ${unmatchedEdits.length} unmatched: [${unmatchedEdits
77
+ .map((s) => JSON.stringify(s.length > 40 ? `${s.slice(0, 40)}\u2026` : s))
78
+ .join(', ')}]`
79
+ : '';
68
80
  const message = appliedEdits === 0
69
- ? `No edits applied to ${args.path}`
70
- : `Successfully applied ${appliedEdits} edits to ${args.path}`;
81
+ ? `No edits applied to ${args.path}${unmatchedNote}`
82
+ : `Successfully applied ${appliedEdits} edits to ${args.path}${unmatchedNote}`;
71
83
  return buildToolResponse(message, structured);
72
84
  }
73
85
  export function registerEditFileTool(server, options = {}) {
@@ -79,8 +91,7 @@ export function registerEditFileTool(server, options = {}) {
79
91
  run: (signal) => handleEditFile(args, signal),
80
92
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
81
93
  });
82
- const validatedHandler = withValidatedArgs(EditFileInputSchema, handler);
83
- server.registerTool('edit', withDefaultIcons({ ...EDIT_FILE_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
94
+ const wrappedHandler = wrapToolHandler(handler, {
84
95
  guard: options.isInitialized,
85
96
  progressMessage: (args) => {
86
97
  const name = path.basename(args.path);
@@ -98,5 +109,7 @@ export function registerEditFileTool(server, options = {}) {
98
109
  }
99
110
  return `🛠 edit: ${name} • [${sc.appliedEdits ?? 0} edits]`;
100
111
  },
101
- }));
112
+ });
113
+ const validatedHandler = withValidatedArgs(EditFileInputSchema, wrappedHandler);
114
+ server.registerTool('edit', withDefaultIcons({ ...EDIT_FILE_TOOL }, options.iconInfo), validatedHandler);
102
115
  }
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const LIST_DIRECTORY_TOOL: ToolContract;
3
4
  export declare function registerListDirectoryTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -5,7 +5,8 @@ import { ErrorCode } from '../lib/errors.js';
5
5
  import { listDirectory } from '../lib/file-operations/list-directory.js';
6
6
  import { ListDirectoryInputSchema, ListDirectoryOutputSchema, } from '../schemas.js';
7
7
  import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
- const LIST_DIRECTORY_TOOL = {
8
+ export const LIST_DIRECTORY_TOOL = {
9
+ name: 'ls',
9
10
  title: 'List Directory',
10
11
  description: 'List the immediate contents of a directory (non-recursive). ' +
11
12
  'Returns name, relative path, type (file/directory/symlink), size, and modified date. ' +
@@ -15,6 +16,7 @@ const LIST_DIRECTORY_TOOL = {
15
16
  inputSchema: ListDirectoryInputSchema,
16
17
  outputSchema: ListDirectoryOutputSchema,
17
18
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
19
+ nuances: ['`pattern` enables filtered recursive traversal up to `maxDepth`.'],
18
20
  };
19
21
  function buildListTextResult(result) {
20
22
  const { entries, summary, path } = result;
@@ -53,7 +55,7 @@ function buildStructuredListEntry(entry) {
53
55
  modified: entry.modified?.toISOString(),
54
56
  };
55
57
  }
56
- function buildStructuredListResult(result) {
58
+ function buildStructuredListResult(result, nextCursor) {
57
59
  const { entries, summary, path: resultPath } = result;
58
60
  const structuredEntries = [];
59
61
  for (const entry of entries) {
@@ -71,22 +73,48 @@ function buildStructuredListResult(result) {
71
73
  ...(summary.skippedInaccessible
72
74
  ? { skippedInaccessible: summary.skippedInaccessible }
73
75
  : {}),
76
+ ...(nextCursor !== undefined ? { nextCursor } : {}),
74
77
  };
75
78
  }
79
+ function encodeCursor(offset) {
80
+ return Buffer.from(JSON.stringify({ offset })).toString('base64url');
81
+ }
82
+ function decodeCursor(cursor) {
83
+ try {
84
+ const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
85
+ if (typeof parsed === 'object' &&
86
+ parsed !== null &&
87
+ typeof parsed.offset === 'number') {
88
+ const { offset } = parsed;
89
+ return Number.isInteger(offset) && offset >= 0 ? offset : 0;
90
+ }
91
+ }
92
+ catch {
93
+ // ignore malformed cursor
94
+ }
95
+ return 0;
96
+ }
76
97
  async function handleListDirectory(args, signal) {
77
98
  const dirPath = resolvePathOrRoot(args.path);
99
+ const cursorOffset = args.cursor !== undefined ? decodeCursor(args.cursor) : 0;
100
+ const pageSize = args.maxEntries ?? 20_000;
78
101
  const options = {
79
102
  includeHidden: args.includeHidden,
80
103
  excludePatterns: args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS,
81
104
  sortBy: args.sortBy,
82
105
  includeSymlinkTargets: args.includeSymlinkTargets,
83
106
  ...(args.maxDepth !== undefined ? { maxDepth: args.maxDepth } : {}),
84
- ...(args.maxEntries !== undefined ? { maxEntries: args.maxEntries } : {}),
107
+ maxEntries: cursorOffset + pageSize,
85
108
  ...(args.pattern !== undefined ? { pattern: args.pattern } : {}),
86
109
  ...(signal ? { signal } : {}),
87
110
  };
88
111
  const result = await listDirectory(dirPath, options);
89
- return buildToolResponse(buildListTextResult(result), buildStructuredListResult(result));
112
+ const displayEntries = cursorOffset > 0 ? result.entries.slice(cursorOffset) : result.entries;
113
+ const nextCursor = result.summary.truncated && displayEntries.length > 0
114
+ ? encodeCursor(cursorOffset + displayEntries.length)
115
+ : undefined;
116
+ const displayResult = { ...result, entries: displayEntries };
117
+ return buildToolResponse(buildListTextResult(displayResult), buildStructuredListResult(displayResult, nextCursor));
90
118
  }
91
119
  export function registerListDirectoryTool(server, options = {}) {
92
120
  const handler = (args, extra) => executeToolWithDiagnostics({
@@ -96,8 +124,7 @@ export function registerListDirectoryTool(server, options = {}) {
96
124
  run: (signal) => handleListDirectory(args, signal),
97
125
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, args.path ?? '.'),
98
126
  });
99
- const validatedHandler = withValidatedArgs(ListDirectoryInputSchema, handler);
100
- server.registerTool('ls', withDefaultIcons({ ...LIST_DIRECTORY_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
127
+ const wrappedHandler = wrapToolHandler(handler, {
101
128
  guard: options.isInitialized,
102
129
  progressMessage: (args) => {
103
130
  if (args.path) {
@@ -115,5 +142,7 @@ export function registerListDirectoryTool(server, options = {}) {
115
142
  const count = sc.totalEntries ?? 0;
116
143
  return `≣ ls: ${base} • ${count} ${count === 1 ? 'entry' : 'entries'}`;
117
144
  },
118
- }));
145
+ });
146
+ const validatedHandler = withValidatedArgs(ListDirectoryInputSchema, wrappedHandler);
147
+ server.registerTool('ls', withDefaultIcons({ ...LIST_DIRECTORY_TOOL }, options.iconInfo), validatedHandler);
119
148
  }
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const MOVE_FILE_TOOL: ToolContract;
3
4
  export declare function registerMoveFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -6,12 +6,14 @@ import { validateExistingPath, validatePathForWrite, } from '../lib/path-validat
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';
9
- const MOVE_FILE_TOOL = {
9
+ export const MOVE_FILE_TOOL = {
10
+ name: 'mv',
10
11
  title: 'Move File',
11
12
  description: 'Move or rename a file or directory.',
12
13
  inputSchema: MoveFileInputSchema,
13
14
  outputSchema: MoveFileOutputSchema,
14
15
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
16
+ nuances: ['Cross-device moves fall back to copy+delete.'],
15
17
  };
16
18
  async function handleMoveFile(args, signal) {
17
19
  const validSource = await validateExistingPath(args.source, signal);
@@ -46,8 +48,7 @@ export function registerMoveFileTool(server, options = {}) {
46
48
  run: (signal) => handleMoveFile(args, signal),
47
49
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.source),
48
50
  });
49
- const validatedHandler = withValidatedArgs(MoveFileInputSchema, handler);
50
- const wrappedHandler = wrapToolHandler(validatedHandler, {
51
+ const wrappedHandler = wrapToolHandler(handler, {
51
52
  guard: options.isInitialized,
52
53
  progressMessage: (args) => `🛠 mv: ${path.basename(args.source)} → ${path.basename(args.destination)}`,
53
54
  completionMessage: (args, result) => {
@@ -58,7 +59,8 @@ export function registerMoveFileTool(server, options = {}) {
58
59
  return `🛠 mv: ${src} → ${dst} • moved`;
59
60
  },
60
61
  });
61
- if (registerToolTaskIfAvailable(server, 'mv', MOVE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
62
+ const validatedHandler = withValidatedArgs(MoveFileInputSchema, wrappedHandler);
63
+ if (registerToolTaskIfAvailable(server, 'mv', MOVE_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
62
64
  return;
63
- server.registerTool('mv', withDefaultIcons({ ...MOVE_FILE_TOOL }, options.iconInfo), wrappedHandler);
65
+ server.registerTool('mv', withDefaultIcons({ ...MOVE_FILE_TOOL }, options.iconInfo), validatedHandler);
64
66
  }
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const READ_MULTIPLE_FILES_TOOL: ToolContract;
3
4
  export declare function registerReadMultipleFilesTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -5,7 +5,8 @@ import { readMultipleFiles } from '../lib/file-operations/read-multiple-files.js
5
5
  import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
6
6
  import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
7
7
  import { registerToolTaskIfAvailable } from './task-support.js';
8
- const READ_MULTIPLE_FILES_TOOL = {
8
+ export const READ_MULTIPLE_FILES_TOOL = {
9
+ name: 'read_many',
9
10
  title: 'Read Multiple Files',
10
11
  description: 'Read multiple text files in a single request. ' +
11
12
  'Returns contents and metadata for each file. ' +
@@ -13,6 +14,10 @@ const READ_MULTIPLE_FILES_TOOL = {
13
14
  inputSchema: ReadMultipleFilesInputSchema,
14
15
  outputSchema: ReadMultipleFilesOutputSchema,
15
16
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
17
+ nuances: ['Total read budget is capped by `MAX_READ_MANY_TOTAL_SIZE`.'],
18
+ gotchas: [
19
+ 'Per-file `truncationReason` can be `head`, `range`, or `externalized`.',
20
+ ],
16
21
  };
17
22
  async function handleReadMultipleFiles(args, signal, resourceStore) {
18
23
  const options = {
@@ -121,8 +126,7 @@ export function registerReadMultipleFilesTool(server, options = {}) {
121
126
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, primaryPath),
122
127
  });
123
128
  };
124
- const validatedHandler = withValidatedArgs(ReadMultipleFilesInputSchema, handler);
125
- const wrappedHandler = wrapToolHandler(validatedHandler, {
129
+ const wrappedHandler = wrapToolHandler(handler, {
126
130
  guard: options.isInitialized,
127
131
  progressMessage: (args) => {
128
132
  const first = path.basename(args.paths[0] ?? '');
@@ -143,7 +147,8 @@ export function registerReadMultipleFilesTool(server, options = {}) {
143
147
  return `🕮 read_many: ${total} files read`;
144
148
  },
145
149
  });
146
- if (registerToolTaskIfAvailable(server, 'read_many', READ_MULTIPLE_FILES_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
150
+ const validatedHandler = withValidatedArgs(ReadMultipleFilesInputSchema, wrappedHandler);
151
+ if (registerToolTaskIfAvailable(server, 'read_many', READ_MULTIPLE_FILES_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
147
152
  return;
148
- server.registerTool('read_many', withDefaultIcons({ ...READ_MULTIPLE_FILES_TOOL }, options.iconInfo), wrappedHandler);
153
+ server.registerTool('read_many', withDefaultIcons({ ...READ_MULTIPLE_FILES_TOOL }, options.iconInfo), validatedHandler);
149
154
  }
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const READ_FILE_TOOL: ToolContract;
3
4
  export declare function registerReadFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -5,7 +5,8 @@ import { readFile } from '../lib/fs-helpers.js';
5
5
  import { ReadFileInputSchema, ReadFileOutputSchema } from '../schemas.js';
6
6
  import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
7
7
  import { registerToolTaskIfAvailable } from './task-support.js';
8
- const READ_FILE_TOOL = {
8
+ export const READ_FILE_TOOL = {
9
+ name: 'read',
9
10
  title: 'Read File',
10
11
  description: 'Read the text contents of a file. ' +
11
12
  'Use head parameter to preview the first N lines of large files. ' +
@@ -13,6 +14,9 @@ const READ_FILE_TOOL = {
13
14
  inputSchema: ReadFileInputSchema,
14
15
  outputSchema: ReadFileOutputSchema,
15
16
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
17
+ nuances: [
18
+ 'Large content is externalized to `filesystem-mcp://result/{id}` and preview is returned inline.',
19
+ ],
16
20
  };
17
21
  async function handleReadFile(args, signal, resourceStore) {
18
22
  const options = {
@@ -80,8 +84,7 @@ export function registerReadFileTool(server, options = {}) {
80
84
  run: (signal) => handleReadFile(args, signal, options.resourceStore),
81
85
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, args.path),
82
86
  });
83
- const validatedHandler = withValidatedArgs(ReadFileInputSchema, handler);
84
- const wrappedHandler = wrapToolHandler(validatedHandler, {
87
+ const wrappedHandler = wrapToolHandler(handler, {
85
88
  guard: options.isInitialized,
86
89
  progressMessage: (args) => {
87
90
  const name = path.basename(args.path);
@@ -105,7 +108,8 @@ export function registerReadFileTool(server, options = {}) {
105
108
  return `🕮 read: ${name} • ${sc.totalLines ?? '?'} lines`;
106
109
  },
107
110
  });
108
- if (registerToolTaskIfAvailable(server, 'read', READ_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
111
+ const validatedHandler = withValidatedArgs(ReadFileInputSchema, wrappedHandler);
112
+ if (registerToolTaskIfAvailable(server, 'read', READ_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
109
113
  return;
110
- server.registerTool('read', withDefaultIcons({ ...READ_FILE_TOOL }, options.iconInfo), wrappedHandler);
114
+ server.registerTool('read', withDefaultIcons({ ...READ_FILE_TOOL }, options.iconInfo), validatedHandler);
111
115
  }
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const SEARCH_AND_REPLACE_TOOL: ToolContract;
3
4
  export declare function registerSearchAndReplaceTool(server: McpServer, options?: ToolRegistrationOptions): void;