@j0hanz/filesystem-mcp 1.1.2 → 1.2.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 (62) hide show
  1. package/README.md +514 -188
  2. package/dist/cli.js +29 -12
  3. package/dist/completions.js +50 -24
  4. package/dist/config.d.ts +3 -2
  5. package/dist/config.js +1 -1
  6. package/dist/index.js +14 -12
  7. package/dist/instructions.md +109 -97
  8. package/dist/lib/constants.js +25 -14
  9. package/dist/lib/errors.js +11 -6
  10. package/dist/lib/file-operations/common.d.ts +4 -0
  11. package/dist/lib/file-operations/common.js +9 -0
  12. package/dist/lib/file-operations/file-info.js +22 -10
  13. package/dist/lib/file-operations/gitignore.js +14 -11
  14. package/dist/lib/file-operations/glob-engine.d.ts +1 -0
  15. package/dist/lib/file-operations/glob-engine.js +46 -33
  16. package/dist/lib/file-operations/list-directory.js +31 -35
  17. package/dist/lib/file-operations/read-multiple-files.js +70 -62
  18. package/dist/lib/file-operations/search-content.js +83 -64
  19. package/dist/lib/file-operations/search-files.js +32 -30
  20. package/dist/lib/file-operations/search-worker.js +22 -12
  21. package/dist/lib/file-operations/tree.js +43 -34
  22. package/dist/lib/fs-helpers.js +61 -124
  23. package/dist/lib/observability.js +29 -28
  24. package/dist/lib/path-format.d.ts +1 -0
  25. package/dist/lib/path-format.js +7 -0
  26. package/dist/lib/path-policy.js +22 -20
  27. package/dist/lib/path-validation.js +13 -7
  28. package/dist/lib/resource-store.d.ts +2 -0
  29. package/dist/lib/resource-store.js +26 -5
  30. package/dist/lib/type-guards.d.ts +1 -0
  31. package/dist/lib/type-guards.js +3 -0
  32. package/dist/prompts.d.ts +1 -5
  33. package/dist/prompts.js +9 -16
  34. package/dist/resources.d.ts +1 -5
  35. package/dist/resources.js +12 -26
  36. package/dist/schemas.d.ts +213 -30
  37. package/dist/schemas.js +52 -90
  38. package/dist/server.js +85 -44
  39. package/dist/tools/apply-patch.js +24 -24
  40. package/dist/tools/calculate-hash.js +42 -45
  41. package/dist/tools/create-directory.js +18 -21
  42. package/dist/tools/delete-file.js +36 -39
  43. package/dist/tools/diff-files.js +16 -21
  44. package/dist/tools/edit-file.js +16 -20
  45. package/dist/tools/list-directory.js +25 -25
  46. package/dist/tools/move-file.js +18 -21
  47. package/dist/tools/read-multiple.js +56 -68
  48. package/dist/tools/read.js +27 -32
  49. package/dist/tools/replace-in-files.js +28 -35
  50. package/dist/tools/roots.js +9 -10
  51. package/dist/tools/search-content.js +74 -74
  52. package/dist/tools/search-files.js +45 -52
  53. package/dist/tools/shared.d.ts +44 -6
  54. package/dist/tools/shared.js +86 -64
  55. package/dist/tools/stat-many.js +45 -68
  56. package/dist/tools/stat.js +11 -39
  57. package/dist/tools/task-support.d.ts +9 -1
  58. package/dist/tools/task-support.js +86 -81
  59. package/dist/tools/tree.js +13 -30
  60. package/dist/tools/write-file.js +18 -21
  61. package/dist/tools.js +23 -18
  62. package/package.json +6 -7
@@ -2,26 +2,21 @@ import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { createHash } from 'node:crypto';
4
4
  import { createReadStream } from 'node:fs';
5
+ import { PARALLEL_CONCURRENCY } from '../lib/constants.js';
5
6
  import { ErrorCode } from '../lib/errors.js';
6
7
  import { isIgnoredByGitignore, loadRootGitignore, } from '../lib/file-operations/gitignore.js';
7
8
  import { globEntries } from '../lib/file-operations/glob-engine.js';
8
- import { assertNotAborted, createTimedAbortSignal, withAbort, } from '../lib/fs-helpers.js';
9
- import { withToolDiagnostics } from '../lib/observability.js';
9
+ import { assertNotAborted, withAbort } from '../lib/fs-helpers.js';
10
10
  import { validateExistingPath } from '../lib/path-validation.js';
11
- import { CalculateHashInputSchema, CalculateHashOutputSchema, } from '../schemas.js';
12
- import { buildToolErrorResponse, buildToolResponse, createProgressReporter, getExperimentalTaskRegistration, notifyProgress, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.js';
13
- import { createToolTaskHandler } from './task-support.js';
11
+ import { CalculateHashInputSchema, } from '../schemas.js';
12
+ import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
13
+ import { registerToolTaskIfAvailable } from './task-support.js';
14
14
  const WINDOWS_PATH_SEPARATOR = /\\/gu;
15
15
  const CALCULATE_HASH_TOOL = {
16
16
  title: 'Calculate Hash',
17
17
  description: 'Calculate SHA-256 hash of a file or directory.',
18
18
  inputSchema: CalculateHashInputSchema,
19
- outputSchema: CalculateHashOutputSchema,
20
- annotations: {
21
- readOnlyHint: true,
22
- idempotentHint: true,
23
- openWorldHint: false,
24
- },
19
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
25
20
  };
26
21
  async function hashFile(filePath, encoding, signal) {
27
22
  assertNotAborted(signal);
@@ -64,9 +59,8 @@ function reportHashProgress(onProgress, current, force = false) {
64
59
  async function hashDirectory(dirPath, options = {}) {
65
60
  const { signal, onProgress } = options;
66
61
  const gitignoreMatcher = await loadRootGitignore(dirPath, signal);
67
- // Enumerate all files in directory.
68
- const entries = [];
69
- let filesHashed = 0;
62
+ // Phase 1: collect all file paths that pass gitignore filtering.
63
+ const filteredPaths = [];
70
64
  for await (const entry of globEntries({
71
65
  cwd: dirPath,
72
66
  pattern: '**/*',
@@ -84,12 +78,25 @@ async function hashDirectory(dirPath, options = {}) {
84
78
  isIgnoredByGitignore(gitignoreMatcher, dirPath, entry.path)) {
85
79
  continue;
86
80
  }
87
- // entry.path is already absolute, no need to join
88
- const fileHash = await hashFile(entry.path, undefined, signal);
89
- // Use posix separators so hashes are stable across OS path separators.
90
- const relativePath = toStableRelativePath(dirPath, entry.path);
91
- entries.push({ path: relativePath, hash: fileHash });
92
- filesHashed++;
81
+ filteredPaths.push({
82
+ filePath: entry.path,
83
+ relativePath: toStableRelativePath(dirPath, entry.path),
84
+ });
85
+ }
86
+ assertNotAborted(signal);
87
+ // Phase 2: hash files concurrently with bounded pool.
88
+ const concurrency = Math.min(PARALLEL_CONCURRENCY, 8);
89
+ const entries = [];
90
+ let filesHashed = 0;
91
+ for (let i = 0; i < filteredPaths.length; i += concurrency) {
92
+ assertNotAborted(signal);
93
+ const batch = filteredPaths.slice(i, i + concurrency);
94
+ const batchResults = await Promise.all(batch.map(async ({ filePath, relativePath }) => {
95
+ const fileHash = await hashFile(filePath, undefined, signal);
96
+ return { path: relativePath, hash: fileHash };
97
+ }));
98
+ entries.push(...batchResults);
99
+ filesHashed += batchResults.length;
93
100
  reportHashProgress(onProgress, filesHashed);
94
101
  }
95
102
  reportHashProgress(onProgress, filesHashed, true);
@@ -139,43 +146,33 @@ async function handleCalculateHash(args, signal, onProgress) {
139
146
  }
140
147
  }
141
148
  export function registerCalculateHashTool(server, options = {}) {
142
- const handler = (args, extra) => withToolDiagnostics('calculate_hash', () => withToolErrorHandling(async () => {
143
- notifyProgress(extra, {
144
- current: 0,
145
- message: `🕮 calculate_hash: ${path.basename(args.path)}`,
146
- });
147
- const { signal, cleanup } = createTimedAbortSignal(extra.signal);
148
- try {
149
+ const handler = (args, extra) => executeToolWithDiagnostics({
150
+ toolName: 'calculate_hash',
151
+ extra,
152
+ timedSignal: {},
153
+ context: { path: args.path },
154
+ run: async (signal) => {
155
+ notifyProgress(extra, {
156
+ current: 0,
157
+ message: `🕮 calculate_hash: ${path.basename(args.path)}`,
158
+ });
149
159
  const result = await handleCalculateHash(args, signal, createProgressReporter(extra));
150
160
  const sc = result.structuredContent;
151
161
  const totalFiles = sc.ok ? (sc.fileCount ?? 1) : 1;
152
162
  const finalCurrent = totalFiles + 1;
153
- const suffix = sc.ok
154
- ? `${(sc.hash ?? '').slice(0, 8)}...`
155
- : 'failed';
163
+ const suffix = sc.ok ? `${(sc.hash ?? '').slice(0, 8)}...` : 'failed';
156
164
  notifyProgress(extra, {
157
165
  current: finalCurrent,
158
166
  message: `🕮 calculate_hash: ${path.basename(args.path)} ➟ ${suffix}`,
159
167
  });
160
168
  return result;
161
- }
162
- finally {
163
- cleanup();
164
- }
165
- }, (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path)), { path: args.path });
169
+ },
170
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
171
+ });
166
172
  const wrappedHandler = wrapToolHandler(handler, {
167
173
  guard: options.isInitialized,
168
174
  });
169
- const taskOptions = options.isInitialized
170
- ? { guard: options.isInitialized }
171
- : undefined;
172
- const tasks = getExperimentalTaskRegistration(server);
173
- if (tasks?.registerToolTask) {
174
- tasks.registerToolTask('calculate_hash', withDefaultIcons({
175
- ...CALCULATE_HASH_TOOL,
176
- execution: { taskSupport: 'optional' },
177
- }, options.iconInfo), createToolTaskHandler(wrappedHandler, taskOptions));
175
+ if (registerToolTaskIfAvailable(server, 'calculate_hash', CALCULATE_HASH_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
178
176
  return;
179
- }
180
177
  server.registerTool('calculate_hash', withDefaultIcons({ ...CALCULATE_HASH_TOOL }, options.iconInfo), wrappedHandler);
181
178
  }
@@ -1,21 +1,16 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { ErrorCode } from '../lib/errors.js';
4
- import { createTimedAbortSignal, withAbort } from '../lib/fs-helpers.js';
5
- import { withToolDiagnostics } from '../lib/observability.js';
4
+ import { withAbort } from '../lib/fs-helpers.js';
6
5
  import { validatePathForWrite } from '../lib/path-validation.js';
7
- import { CreateDirectoryInputSchema, CreateDirectoryOutputSchema, } from '../schemas.js';
8
- import { buildToolErrorResponse, buildToolResponse, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.js';
6
+ import { CreateDirectoryInputSchema, } from '../schemas.js';
7
+ import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, IDEMPOTENT_WRITE_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
8
+ import { registerToolTaskIfAvailable } from './task-support.js';
9
9
  const CREATE_DIRECTORY_TOOL = {
10
10
  title: 'Create Directory',
11
11
  description: 'Create a new directory at the specified path (recursive)',
12
12
  inputSchema: CreateDirectoryInputSchema,
13
- outputSchema: CreateDirectoryOutputSchema,
14
- annotations: {
15
- readOnlyHint: false,
16
- idempotentHint: true,
17
- openWorldHint: false,
18
- },
13
+ annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
19
14
  };
20
15
  async function handleCreateDirectory(args, signal) {
21
16
  const validPath = await validatePathForWrite(args.path, signal);
@@ -26,17 +21,19 @@ async function handleCreateDirectory(args, signal) {
26
21
  });
27
22
  }
28
23
  export function registerCreateDirectoryTool(server, options = {}) {
29
- const handler = (args, extra) => withToolDiagnostics('mkdir', () => withToolErrorHandling(async () => {
30
- const { signal, cleanup } = createTimedAbortSignal(extra.signal);
31
- try {
32
- return await handleCreateDirectory(args, signal);
33
- }
34
- finally {
35
- cleanup();
36
- }
37
- }, (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path)), { path: args.path });
38
- server.registerTool('mkdir', withDefaultIcons({ ...CREATE_DIRECTORY_TOOL }, options.iconInfo), wrapToolHandler(handler, {
24
+ const handler = (args, extra) => executeToolWithDiagnostics({
25
+ toolName: 'mkdir',
26
+ extra,
27
+ timedSignal: {},
28
+ context: { path: args.path },
29
+ run: (signal) => handleCreateDirectory(args, signal),
30
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
31
+ });
32
+ const wrappedHandler = wrapToolHandler(handler, {
39
33
  guard: options.isInitialized,
40
34
  progressMessage: (args) => `🛠 mkdir: ${path.basename(args.path)}`,
41
- }));
35
+ });
36
+ if (registerToolTaskIfAvailable(server, 'mkdir', CREATE_DIRECTORY_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
37
+ return;
38
+ server.registerTool('mkdir', withDefaultIcons({ ...CREATE_DIRECTORY_TOOL }, options.iconInfo), wrappedHandler);
42
39
  }
@@ -1,21 +1,16 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { ErrorCode, isNodeError } from '../lib/errors.js';
4
- import { createTimedAbortSignal, withAbort } from '../lib/fs-helpers.js';
5
- import { withToolDiagnostics } from '../lib/observability.js';
4
+ import { withAbort } from '../lib/fs-helpers.js';
6
5
  import { validatePathForWrite } from '../lib/path-validation.js';
7
- import { DeleteFileInputSchema, DeleteFileOutputSchema } from '../schemas.js';
8
- import { buildToolErrorResponse, buildToolResponse, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.js';
6
+ import { DeleteFileInputSchema, } from '../schemas.js';
7
+ import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
8
+ import { registerToolTaskIfAvailable } from './task-support.js';
9
9
  const DELETE_FILE_TOOL = {
10
10
  title: 'Delete File',
11
11
  description: 'Delete a file or directory.',
12
12
  inputSchema: DeleteFileInputSchema,
13
- outputSchema: DeleteFileOutputSchema,
14
- annotations: {
15
- readOnlyHint: false,
16
- destructiveHint: true,
17
- openWorldHint: false,
18
- },
13
+ annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
19
14
  };
20
15
  async function handleDeleteFile(args, signal) {
21
16
  const validPath = await validatePathForWrite(args.path, signal);
@@ -51,36 +46,38 @@ async function handleDeleteFile(args, signal) {
51
46
  });
52
47
  }
53
48
  export function registerDeleteFileTool(server, options = {}) {
54
- const handler = (args, extra) => withToolDiagnostics('rm', () => withToolErrorHandling(async () => {
55
- const { signal, cleanup } = createTimedAbortSignal(extra.signal);
56
- try {
57
- return await handleDeleteFile(args, signal);
58
- }
59
- finally {
60
- cleanup();
61
- }
62
- }, (error) => {
63
- if (isNodeError(error)) {
64
- if (error.code === 'ENOENT') {
65
- return buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, args.path);
66
- }
67
- if (error.code === 'ENOTEMPTY') {
68
- return buildToolErrorResponse(new Error(`Directory is not empty: ${args.path}. Use recursive: true to delete non-empty directories.`), ErrorCode.E_INVALID_INPUT, args.path);
69
- }
70
- if (error.code === 'EISDIR') {
71
- return buildToolErrorResponse(new Error(`Path is a directory: ${args.path}. Use recursive: true to delete directories.`), ErrorCode.E_INVALID_INPUT, args.path);
72
- }
73
- if (error.code === 'EEXIST') {
74
- return buildToolErrorResponse(new Error(`Directory is not empty: ${args.path}. Use recursive: true to delete non-empty directories.`), ErrorCode.E_INVALID_INPUT, args.path);
49
+ const handler = (args, extra) => executeToolWithDiagnostics({
50
+ toolName: 'rm',
51
+ extra,
52
+ timedSignal: {},
53
+ context: { path: args.path },
54
+ run: (signal) => handleDeleteFile(args, signal),
55
+ onError: (error) => {
56
+ if (isNodeError(error)) {
57
+ if (error.code === 'ENOENT') {
58
+ return buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, args.path);
59
+ }
60
+ if (error.code === 'ENOTEMPTY') {
61
+ return buildToolErrorResponse(new Error(`Directory is not empty: ${args.path}. Use recursive: true to delete non-empty directories.`), ErrorCode.E_INVALID_INPUT, args.path);
62
+ }
63
+ if (error.code === 'EISDIR') {
64
+ return buildToolErrorResponse(new Error(`Path is a directory: ${args.path}. Use recursive: true to delete directories.`), ErrorCode.E_INVALID_INPUT, args.path);
65
+ }
66
+ if (error.code === 'EEXIST') {
67
+ return buildToolErrorResponse(new Error(`Directory is not empty: ${args.path}. Use recursive: true to delete non-empty directories.`), ErrorCode.E_INVALID_INPUT, args.path);
68
+ }
69
+ if (error.code === 'EPERM' || error.code === 'EACCES') {
70
+ return buildToolErrorResponse(error, ErrorCode.E_PERMISSION_DENIED, args.path);
71
+ }
75
72
  }
76
- if (error.code === 'EPERM' || error.code === 'EACCES') {
77
- return buildToolErrorResponse(error, ErrorCode.E_PERMISSION_DENIED, args.path);
78
- }
79
- }
80
- return buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path);
81
- }), { path: args.path });
82
- server.registerTool('rm', withDefaultIcons({ ...DELETE_FILE_TOOL }, options.iconInfo), wrapToolHandler(handler, {
73
+ return buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path);
74
+ },
75
+ });
76
+ const wrappedHandler = wrapToolHandler(handler, {
83
77
  guard: options.isInitialized,
84
78
  progressMessage: (args) => `🛠 rm: ${path.basename(args.path)}`,
85
- }));
79
+ });
80
+ if (registerToolTaskIfAvailable(server, 'rm', DELETE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
81
+ return;
82
+ server.registerTool('rm', withDefaultIcons({ ...DELETE_FILE_TOOL }, options.iconInfo), wrappedHandler);
86
83
  }
@@ -3,21 +3,17 @@ import * as path from 'node:path';
3
3
  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
- import { createTimedAbortSignal, withAbort } from '../lib/fs-helpers.js';
7
- import { withToolDiagnostics } from '../lib/observability.js';
6
+ import { withAbort } from '../lib/fs-helpers.js';
8
7
  import { validateExistingPath } from '../lib/path-validation.js';
9
- import { DiffFilesInputSchema, DiffFilesOutputSchema } from '../schemas.js';
10
- import { buildResourceLink, buildToolErrorResponse, buildToolResponse, maybeExternalizeTextContent, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.js';
8
+ import { DiffFilesInputSchema, } from '../schemas.js';
9
+ import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
11
10
  const DIFF_FILES_TOOL = {
12
11
  title: 'Diff Files',
13
- description: 'Generate a unified diff between two files.',
12
+ description: 'Generate a unified diff between two files. ' +
13
+ 'Output feeds directly into `apply_patch`. ' +
14
+ 'Check `isIdentical` in the response — if true, the files are already in sync and no patch is needed.',
14
15
  inputSchema: DiffFilesInputSchema,
15
- outputSchema: DiffFilesOutputSchema,
16
- annotations: {
17
- readOnlyHint: true,
18
- idempotentHint: true,
19
- openWorldHint: false,
20
- },
16
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
21
17
  };
22
18
  function assertDiffFileSizeWithinLimit(filePath, size, maxFileSize) {
23
19
  if (size <= maxFileSize)
@@ -25,7 +21,7 @@ function assertDiffFileSizeWithinLimit(filePath, size, maxFileSize) {
25
21
  throw new McpError(ErrorCode.E_TOO_LARGE, `File too large for diff: ${filePath} (${size} bytes > ${maxFileSize} bytes).`, filePath, { size, maxFileSize });
26
22
  }
27
23
  async function handleDiffFiles(args, signal, resourceStore) {
28
- const maxFileSize = args.maxFileSize ?? MAX_TEXT_FILE_SIZE;
24
+ const maxFileSize = MAX_TEXT_FILE_SIZE;
29
25
  const [originalPath, modifiedPath] = await Promise.all([
30
26
  validateExistingPath(args.original, signal),
31
27
  validateExistingPath(args.modified, signal),
@@ -75,15 +71,14 @@ async function handleDiffFiles(args, signal, resourceStore) {
75
71
  ]);
76
72
  }
77
73
  export function registerDiffFilesTool(server, options = {}) {
78
- const handler = (args, extra) => withToolDiagnostics('diff_files', () => withToolErrorHandling(async () => {
79
- const { signal, cleanup } = createTimedAbortSignal(extra.signal);
80
- try {
81
- return await handleDiffFiles(args, signal, options.resourceStore);
82
- }
83
- finally {
84
- cleanup();
85
- }
86
- }, (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.original)), { path: args.original });
74
+ const handler = (args, extra) => executeToolWithDiagnostics({
75
+ toolName: 'diff_files',
76
+ extra,
77
+ timedSignal: {},
78
+ context: { path: args.original },
79
+ run: (signal) => handleDiffFiles(args, signal, options.resourceStore),
80
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.original),
81
+ });
87
82
  server.registerTool('diff_files', withDefaultIcons({ ...DIFF_FILES_TOOL }, options.iconInfo), wrapToolHandler(handler, {
88
83
  guard: options.isInitialized,
89
84
  progressMessage: (args) => {
@@ -1,21 +1,18 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { ErrorCode } from '../lib/errors.js';
4
- import { atomicWriteFile, createTimedAbortSignal } from '../lib/fs-helpers.js';
5
- import { withToolDiagnostics } from '../lib/observability.js';
4
+ import { atomicWriteFile } from '../lib/fs-helpers.js';
6
5
  import { validateExistingPath } from '../lib/path-validation.js';
7
- import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
8
- import { buildToolErrorResponse, buildToolResponse, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.js';
6
+ import { EditFileInputSchema } from '../schemas.js';
7
+ import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
9
8
  const EDIT_FILE_TOOL = {
10
9
  title: 'Edit File',
11
10
  description: 'Edit a file by replacing text. Sequentially applies a list of string replacements. ' +
12
- 'Replaces the first occurrence of each `oldText`.',
11
+ 'Replaces the first occurrence of each `oldText`. ' +
12
+ '`oldText` must match exactly — include 3–5 lines of surrounding context to uniquely target the location. ' +
13
+ 'Use `dryRun: true` to validate edits before writing.',
13
14
  inputSchema: EditFileInputSchema,
14
- outputSchema: EditFileOutputSchema,
15
- annotations: {
16
- readOnlyHint: false,
17
- openWorldHint: false,
18
- },
15
+ annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
19
16
  };
20
17
  function applyEdits(content, edits) {
21
18
  let newContent = content;
@@ -37,7 +34,7 @@ function applyEdits(content, edits) {
37
34
  minLine = startLine;
38
35
  if (maxLine === undefined || endLine > maxLine)
39
36
  maxLine = endLine;
40
- newContent = newContent.replace(edit.oldText, edit.newText);
37
+ newContent = newContent.replace(edit.oldText, () => edit.newText);
41
38
  appliedEdits += 1;
42
39
  }
43
40
  const result = {
@@ -73,15 +70,14 @@ async function handleEditFile(args, signal) {
73
70
  return buildToolResponse(message, structured);
74
71
  }
75
72
  export function registerEditFileTool(server, options = {}) {
76
- const handler = (args, extra) => withToolDiagnostics('edit', () => withToolErrorHandling(async () => {
77
- const { signal, cleanup } = createTimedAbortSignal(extra.signal);
78
- try {
79
- return await handleEditFile(args, signal);
80
- }
81
- finally {
82
- cleanup();
83
- }
84
- }, (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path)), { path: args.path });
73
+ const handler = (args, extra) => executeToolWithDiagnostics({
74
+ toolName: 'edit',
75
+ extra,
76
+ timedSignal: {},
77
+ context: { path: args.path },
78
+ run: (signal) => handleEditFile(args, signal),
79
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
80
+ });
85
81
  server.registerTool('edit', withDefaultIcons({ ...EDIT_FILE_TOOL }, options.iconInfo), wrapToolHandler(handler, {
86
82
  guard: options.isInitialized,
87
83
  progressMessage: (args) => {
@@ -3,9 +3,8 @@ import { formatOperationSummary, joinLines } from '../config.js';
3
3
  import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
4
4
  import { ErrorCode } from '../lib/errors.js';
5
5
  import { listDirectory } from '../lib/file-operations/list-directory.js';
6
- import { withToolDiagnostics } from '../lib/observability.js';
7
- import { ListDirectoryInputSchema, ListDirectoryOutputSchema, } from '../schemas.js';
8
- import { buildToolErrorResponse, buildToolResponse, resolvePathOrRoot, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.js';
6
+ import { ListDirectoryInputSchema, } from '../schemas.js';
7
+ import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
9
8
  const LIST_DIRECTORY_TOOL = {
10
9
  title: 'List Directory',
11
10
  description: 'List the immediate contents of a directory (non-recursive). ' +
@@ -14,12 +13,7 @@ const LIST_DIRECTORY_TOOL = {
14
13
  'Use includeIgnored=true to include ignored directories like node_modules. ' +
15
14
  'For recursive searches, use find instead.',
16
15
  inputSchema: ListDirectoryInputSchema,
17
- outputSchema: ListDirectoryOutputSchema,
18
- annotations: {
19
- readOnlyHint: true,
20
- idempotentHint: true,
21
- openWorldHint: false,
22
- },
16
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
23
17
  };
24
18
  function buildListTextResult(result) {
25
19
  const { entries, summary, path } = result;
@@ -29,13 +23,11 @@ function buildListTextResult(result) {
29
23
  }
30
24
  return `${path} (no matches)`;
31
25
  }
32
- const lines = [
33
- path,
34
- ...entries.map((entry) => {
35
- const suffix = entry.type === 'directory' ? '/' : '';
36
- return ` ${entry.relativePath}${suffix}`;
37
- }),
38
- ];
26
+ const lines = [path];
27
+ for (const entry of entries) {
28
+ const suffix = entry.type === 'directory' ? '/' : '';
29
+ lines.push(` ${entry.relativePath}${suffix}`);
30
+ }
39
31
  let truncatedReason;
40
32
  if (summary.truncated) {
41
33
  if (summary.stoppedReason === 'maxEntries') {
@@ -62,20 +54,22 @@ function buildStructuredListEntry(entry) {
62
54
  }
63
55
  function buildStructuredListResult(result) {
64
56
  const { entries, summary, path: resultPath } = result;
57
+ const structuredEntries = [];
58
+ for (const entry of entries) {
59
+ structuredEntries.push(buildStructuredListEntry(entry));
60
+ }
65
61
  return {
66
62
  ok: true,
67
63
  path: resultPath,
68
- entries: entries.map(buildStructuredListEntry),
64
+ entries: structuredEntries,
69
65
  totalEntries: summary.totalEntries,
70
- truncated: summary.truncated,
71
- entriesScanned: summary.entriesScanned,
72
- entriesVisible: summary.entriesVisible,
66
+ ...(summary.truncated ? { truncated: summary.truncated } : {}),
73
67
  totalFiles: summary.totalFiles,
74
68
  totalDirectories: summary.totalDirectories,
75
- maxDepthReached: summary.maxDepthReached,
76
- stoppedReason: summary.stoppedReason,
77
- skippedInaccessible: summary.skippedInaccessible,
78
- symlinksNotFollowed: summary.symlinksNotFollowed,
69
+ ...(summary.stoppedReason ? { stoppedReason: summary.stoppedReason } : {}),
70
+ ...(summary.skippedInaccessible
71
+ ? { skippedInaccessible: summary.skippedInaccessible }
72
+ : {}),
79
73
  };
80
74
  }
81
75
  async function handleListDirectory(args, signal) {
@@ -94,7 +88,13 @@ async function handleListDirectory(args, signal) {
94
88
  return buildToolResponse(buildListTextResult(result), buildStructuredListResult(result));
95
89
  }
96
90
  export function registerListDirectoryTool(server, options = {}) {
97
- const handler = (args, extra) => withToolDiagnostics('ls', () => withToolErrorHandling(() => handleListDirectory(args, extra.signal), (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, args.path ?? '.')), { path: args.path ?? '.' });
91
+ const handler = (args, extra) => executeToolWithDiagnostics({
92
+ toolName: 'ls',
93
+ extra,
94
+ context: { path: args.path ?? '.' },
95
+ run: (signal) => handleListDirectory(args, signal),
96
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, args.path ?? '.'),
97
+ });
98
98
  server.registerTool('ls', withDefaultIcons({ ...LIST_DIRECTORY_TOOL }, options.iconInfo), wrapToolHandler(handler, {
99
99
  guard: options.isInitialized,
100
100
  progressMessage: (args) => {
@@ -1,21 +1,16 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { ErrorCode, isNodeError } from '../lib/errors.js';
4
- import { createTimedAbortSignal, withAbort } from '../lib/fs-helpers.js';
5
- import { withToolDiagnostics } from '../lib/observability.js';
4
+ import { withAbort } from '../lib/fs-helpers.js';
6
5
  import { validateExistingPath, validatePathForWrite, } from '../lib/path-validation.js';
7
- import { MoveFileInputSchema, MoveFileOutputSchema } from '../schemas.js';
8
- import { buildToolErrorResponse, buildToolResponse, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.js';
6
+ import { MoveFileInputSchema } from '../schemas.js';
7
+ import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
8
+ import { registerToolTaskIfAvailable } from './task-support.js';
9
9
  const MOVE_FILE_TOOL = {
10
10
  title: 'Move File',
11
11
  description: 'Move or rename a file or directory.',
12
12
  inputSchema: MoveFileInputSchema,
13
- outputSchema: MoveFileOutputSchema,
14
- annotations: {
15
- readOnlyHint: false,
16
- destructiveHint: true,
17
- openWorldHint: false,
18
- },
13
+ annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
19
14
  };
20
15
  async function handleMoveFile(args, signal) {
21
16
  const validSource = await validateExistingPath(args.source, signal);
@@ -42,17 +37,19 @@ async function handleMoveFile(args, signal) {
42
37
  });
43
38
  }
44
39
  export function registerMoveFileTool(server, options = {}) {
45
- const handler = (args, extra) => withToolDiagnostics('mv', () => withToolErrorHandling(async () => {
46
- const { signal, cleanup } = createTimedAbortSignal(extra.signal);
47
- try {
48
- return await handleMoveFile(args, signal);
49
- }
50
- finally {
51
- cleanup();
52
- }
53
- }, (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.source)), { path: args.source });
54
- server.registerTool('mv', withDefaultIcons({ ...MOVE_FILE_TOOL }, options.iconInfo), wrapToolHandler(handler, {
40
+ const handler = (args, extra) => executeToolWithDiagnostics({
41
+ toolName: 'mv',
42
+ extra,
43
+ timedSignal: {},
44
+ context: { path: args.source },
45
+ run: (signal) => handleMoveFile(args, signal),
46
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.source),
47
+ });
48
+ const wrappedHandler = wrapToolHandler(handler, {
55
49
  guard: options.isInitialized,
56
50
  progressMessage: (args) => `🛠 mv: ${path.basename(args.source)} ➟ ${path.basename(args.destination)}`,
57
- }));
51
+ });
52
+ if (registerToolTaskIfAvailable(server, 'mv', MOVE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
53
+ return;
54
+ server.registerTool('mv', withDefaultIcons({ ...MOVE_FILE_TOOL }, options.iconInfo), wrappedHandler);
58
55
  }