@j0hanz/filesystem-mcp 1.1.2 → 1.2.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 +514 -188
  2. package/dist/cli.js +29 -12
  3. package/dist/completions.js +50 -24
  4. package/dist/config.d.ts +4 -2
  5. package/dist/config.js +2 -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 +15 -8
  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 +232 -30
  37. package/dist/schemas.js +52 -90
  38. package/dist/server.js +96 -44
  39. package/dist/tools/apply-patch.js +23 -22
  40. package/dist/tools/calculate-hash.js +41 -43
  41. package/dist/tools/create-directory.js +17 -19
  42. package/dist/tools/delete-file.js +35 -37
  43. package/dist/tools/diff-files.js +15 -19
  44. package/dist/tools/edit-file.js +15 -18
  45. package/dist/tools/list-directory.js +24 -23
  46. package/dist/tools/move-file.js +17 -19
  47. package/dist/tools/read-multiple.js +55 -66
  48. package/dist/tools/read.js +26 -30
  49. package/dist/tools/replace-in-files.js +27 -33
  50. package/dist/tools/roots.js +8 -8
  51. package/dist/tools/search-content.js +73 -72
  52. package/dist/tools/search-files.js +44 -50
  53. package/dist/tools/shared.d.ts +44 -6
  54. package/dist/tools/shared.js +86 -64
  55. package/dist/tools/stat-many.js +44 -66
  56. package/dist/tools/stat.js +10 -37
  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 +12 -28
  60. package/dist/tools/write-file.js +17 -19
  61. package/dist/tools.js +23 -18
  62. package/package.json +6 -7
@@ -2,26 +2,22 @@ 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
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';
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
19
  outputSchema: CalculateHashOutputSchema,
20
- annotations: {
21
- readOnlyHint: true,
22
- idempotentHint: true,
23
- openWorldHint: false,
24
- },
20
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
25
21
  };
26
22
  async function hashFile(filePath, encoding, signal) {
27
23
  assertNotAborted(signal);
@@ -64,9 +60,8 @@ function reportHashProgress(onProgress, current, force = false) {
64
60
  async function hashDirectory(dirPath, options = {}) {
65
61
  const { signal, onProgress } = options;
66
62
  const gitignoreMatcher = await loadRootGitignore(dirPath, signal);
67
- // Enumerate all files in directory.
68
- const entries = [];
69
- let filesHashed = 0;
63
+ // Phase 1: collect all file paths that pass gitignore filtering.
64
+ const filteredPaths = [];
70
65
  for await (const entry of globEntries({
71
66
  cwd: dirPath,
72
67
  pattern: '**/*',
@@ -84,12 +79,25 @@ async function hashDirectory(dirPath, options = {}) {
84
79
  isIgnoredByGitignore(gitignoreMatcher, dirPath, entry.path)) {
85
80
  continue;
86
81
  }
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++;
82
+ filteredPaths.push({
83
+ filePath: entry.path,
84
+ relativePath: toStableRelativePath(dirPath, entry.path),
85
+ });
86
+ }
87
+ assertNotAborted(signal);
88
+ // Phase 2: hash files concurrently with bounded pool.
89
+ const concurrency = Math.min(PARALLEL_CONCURRENCY, 8);
90
+ const entries = [];
91
+ let filesHashed = 0;
92
+ for (let i = 0; i < filteredPaths.length; i += concurrency) {
93
+ assertNotAborted(signal);
94
+ const batch = filteredPaths.slice(i, i + concurrency);
95
+ const batchResults = await Promise.all(batch.map(async ({ filePath, relativePath }) => {
96
+ const fileHash = await hashFile(filePath, undefined, signal);
97
+ return { path: relativePath, hash: fileHash };
98
+ }));
99
+ entries.push(...batchResults);
100
+ filesHashed += batchResults.length;
93
101
  reportHashProgress(onProgress, filesHashed);
94
102
  }
95
103
  reportHashProgress(onProgress, filesHashed, true);
@@ -139,43 +147,33 @@ async function handleCalculateHash(args, signal, onProgress) {
139
147
  }
140
148
  }
141
149
  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 {
150
+ const handler = (args, extra) => executeToolWithDiagnostics({
151
+ toolName: 'calculate_hash',
152
+ extra,
153
+ timedSignal: {},
154
+ context: { path: args.path },
155
+ run: async (signal) => {
156
+ notifyProgress(extra, {
157
+ current: 0,
158
+ message: `🕮 calculate_hash: ${path.basename(args.path)}`,
159
+ });
149
160
  const result = await handleCalculateHash(args, signal, createProgressReporter(extra));
150
161
  const sc = result.structuredContent;
151
162
  const totalFiles = sc.ok ? (sc.fileCount ?? 1) : 1;
152
163
  const finalCurrent = totalFiles + 1;
153
- const suffix = sc.ok
154
- ? `${(sc.hash ?? '').slice(0, 8)}...`
155
- : 'failed';
164
+ const suffix = sc.ok ? `${(sc.hash ?? '').slice(0, 8)}...` : 'failed';
156
165
  notifyProgress(extra, {
157
166
  current: finalCurrent,
158
167
  message: `🕮 calculate_hash: ${path.basename(args.path)} ➟ ${suffix}`,
159
168
  });
160
169
  return result;
161
- }
162
- finally {
163
- cleanup();
164
- }
165
- }, (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path)), { path: args.path });
170
+ },
171
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
172
+ });
166
173
  const wrappedHandler = wrapToolHandler(handler, {
167
174
  guard: options.isInitialized,
168
175
  });
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));
176
+ if (registerToolTaskIfAvailable(server, 'calculate_hash', CALCULATE_HASH_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
178
177
  return;
179
- }
180
178
  server.registerTool('calculate_hash', withDefaultIcons({ ...CALCULATE_HASH_TOOL }, options.iconInfo), wrappedHandler);
181
179
  }
@@ -1,21 +1,17 @@
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
6
  import { CreateDirectoryInputSchema, CreateDirectoryOutputSchema, } from '../schemas.js';
8
- import { buildToolErrorResponse, buildToolResponse, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.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
13
  outputSchema: CreateDirectoryOutputSchema,
14
- annotations: {
15
- readOnlyHint: false,
16
- idempotentHint: true,
17
- openWorldHint: false,
18
- },
14
+ annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
19
15
  };
20
16
  async function handleCreateDirectory(args, signal) {
21
17
  const validPath = await validatePathForWrite(args.path, signal);
@@ -26,17 +22,19 @@ async function handleCreateDirectory(args, signal) {
26
22
  });
27
23
  }
28
24
  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, {
25
+ const handler = (args, extra) => executeToolWithDiagnostics({
26
+ toolName: 'mkdir',
27
+ extra,
28
+ timedSignal: {},
29
+ context: { path: args.path },
30
+ run: (signal) => handleCreateDirectory(args, signal),
31
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
32
+ });
33
+ const wrappedHandler = wrapToolHandler(handler, {
39
34
  guard: options.isInitialized,
40
35
  progressMessage: (args) => `🛠 mkdir: ${path.basename(args.path)}`,
41
- }));
36
+ });
37
+ if (registerToolTaskIfAvailable(server, 'mkdir', CREATE_DIRECTORY_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
38
+ return;
39
+ server.registerTool('mkdir', withDefaultIcons({ ...CREATE_DIRECTORY_TOOL }, options.iconInfo), wrappedHandler);
42
40
  }
@@ -1,21 +1,17 @@
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
6
  import { DeleteFileInputSchema, DeleteFileOutputSchema } from '../schemas.js';
8
- import { buildToolErrorResponse, buildToolResponse, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.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
13
  outputSchema: DeleteFileOutputSchema,
14
- annotations: {
15
- readOnlyHint: false,
16
- destructiveHint: true,
17
- openWorldHint: false,
18
- },
14
+ annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
19
15
  };
20
16
  async function handleDeleteFile(args, signal) {
21
17
  const validPath = await validatePathForWrite(args.path, signal);
@@ -51,36 +47,38 @@ async function handleDeleteFile(args, signal) {
51
47
  });
52
48
  }
53
49
  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);
50
+ const handler = (args, extra) => executeToolWithDiagnostics({
51
+ toolName: 'rm',
52
+ extra,
53
+ timedSignal: {},
54
+ context: { path: args.path },
55
+ run: (signal) => handleDeleteFile(args, signal),
56
+ onError: (error) => {
57
+ if (isNodeError(error)) {
58
+ if (error.code === 'ENOENT') {
59
+ return buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, args.path);
60
+ }
61
+ if (error.code === 'ENOTEMPTY') {
62
+ return buildToolErrorResponse(new Error(`Directory is not empty: ${args.path}. Use recursive: true to delete non-empty directories.`), ErrorCode.E_INVALID_INPUT, args.path);
63
+ }
64
+ if (error.code === 'EISDIR') {
65
+ return buildToolErrorResponse(new Error(`Path is a directory: ${args.path}. Use recursive: true to delete directories.`), ErrorCode.E_INVALID_INPUT, args.path);
66
+ }
67
+ if (error.code === 'EEXIST') {
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 === 'EPERM' || error.code === 'EACCES') {
71
+ return buildToolErrorResponse(error, ErrorCode.E_PERMISSION_DENIED, args.path);
72
+ }
75
73
  }
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, {
74
+ return buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path);
75
+ },
76
+ });
77
+ const wrappedHandler = wrapToolHandler(handler, {
83
78
  guard: options.isInitialized,
84
79
  progressMessage: (args) => `🛠 rm: ${path.basename(args.path)}`,
85
- }));
80
+ });
81
+ if (registerToolTaskIfAvailable(server, 'rm', DELETE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
82
+ return;
83
+ server.registerTool('rm', withDefaultIcons({ ...DELETE_FILE_TOOL }, options.iconInfo), wrappedHandler);
86
84
  }
@@ -3,21 +3,18 @@ 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
8
  import { DiffFilesInputSchema, DiffFilesOutputSchema } from '../schemas.js';
10
- import { buildResourceLink, buildToolErrorResponse, buildToolResponse, maybeExternalizeTextContent, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.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
16
  outputSchema: DiffFilesOutputSchema,
16
- annotations: {
17
- readOnlyHint: true,
18
- idempotentHint: true,
19
- openWorldHint: false,
20
- },
17
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
21
18
  };
22
19
  function assertDiffFileSizeWithinLimit(filePath, size, maxFileSize) {
23
20
  if (size <= maxFileSize)
@@ -25,7 +22,7 @@ function assertDiffFileSizeWithinLimit(filePath, size, maxFileSize) {
25
22
  throw new McpError(ErrorCode.E_TOO_LARGE, `File too large for diff: ${filePath} (${size} bytes > ${maxFileSize} bytes).`, filePath, { size, maxFileSize });
26
23
  }
27
24
  async function handleDiffFiles(args, signal, resourceStore) {
28
- const maxFileSize = args.maxFileSize ?? MAX_TEXT_FILE_SIZE;
25
+ const maxFileSize = MAX_TEXT_FILE_SIZE;
29
26
  const [originalPath, modifiedPath] = await Promise.all([
30
27
  validateExistingPath(args.original, signal),
31
28
  validateExistingPath(args.modified, signal),
@@ -75,15 +72,14 @@ async function handleDiffFiles(args, signal, resourceStore) {
75
72
  ]);
76
73
  }
77
74
  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 });
75
+ const handler = (args, extra) => executeToolWithDiagnostics({
76
+ toolName: 'diff_files',
77
+ extra,
78
+ timedSignal: {},
79
+ context: { path: args.original },
80
+ run: (signal) => handleDiffFiles(args, signal, options.resourceStore),
81
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.original),
82
+ });
87
83
  server.registerTool('diff_files', withDefaultIcons({ ...DIFF_FILES_TOOL }, options.iconInfo), wrapToolHandler(handler, {
88
84
  guard: options.isInitialized,
89
85
  progressMessage: (args) => {
@@ -1,21 +1,19 @@
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
6
  import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
8
- import { buildToolErrorResponse, buildToolResponse, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.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
15
  outputSchema: EditFileOutputSchema,
15
- annotations: {
16
- readOnlyHint: false,
17
- openWorldHint: false,
18
- },
16
+ annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
19
17
  };
20
18
  function applyEdits(content, edits) {
21
19
  let newContent = content;
@@ -37,7 +35,7 @@ function applyEdits(content, edits) {
37
35
  minLine = startLine;
38
36
  if (maxLine === undefined || endLine > maxLine)
39
37
  maxLine = endLine;
40
- newContent = newContent.replace(edit.oldText, edit.newText);
38
+ newContent = newContent.replace(edit.oldText, () => edit.newText);
41
39
  appliedEdits += 1;
42
40
  }
43
41
  const result = {
@@ -73,15 +71,14 @@ async function handleEditFile(args, signal) {
73
71
  return buildToolResponse(message, structured);
74
72
  }
75
73
  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 });
74
+ const handler = (args, extra) => executeToolWithDiagnostics({
75
+ toolName: 'edit',
76
+ extra,
77
+ timedSignal: {},
78
+ context: { path: args.path },
79
+ run: (signal) => handleEditFile(args, signal),
80
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
81
+ });
85
82
  server.registerTool('edit', withDefaultIcons({ ...EDIT_FILE_TOOL }, options.iconInfo), wrapToolHandler(handler, {
86
83
  guard: options.isInitialized,
87
84
  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
6
  import { ListDirectoryInputSchema, ListDirectoryOutputSchema, } from '../schemas.js';
8
- import { buildToolErrorResponse, buildToolResponse, resolvePathOrRoot, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.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). ' +
@@ -15,11 +14,7 @@ const LIST_DIRECTORY_TOOL = {
15
14
  'For recursive searches, use find instead.',
16
15
  inputSchema: ListDirectoryInputSchema,
17
16
  outputSchema: ListDirectoryOutputSchema,
18
- annotations: {
19
- readOnlyHint: true,
20
- idempotentHint: true,
21
- openWorldHint: false,
22
- },
17
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
23
18
  };
24
19
  function buildListTextResult(result) {
25
20
  const { entries, summary, path } = result;
@@ -29,13 +24,11 @@ function buildListTextResult(result) {
29
24
  }
30
25
  return `${path} (no matches)`;
31
26
  }
32
- const lines = [
33
- path,
34
- ...entries.map((entry) => {
35
- const suffix = entry.type === 'directory' ? '/' : '';
36
- return ` ${entry.relativePath}${suffix}`;
37
- }),
38
- ];
27
+ const lines = [path];
28
+ for (const entry of entries) {
29
+ const suffix = entry.type === 'directory' ? '/' : '';
30
+ lines.push(` ${entry.relativePath}${suffix}`);
31
+ }
39
32
  let truncatedReason;
40
33
  if (summary.truncated) {
41
34
  if (summary.stoppedReason === 'maxEntries') {
@@ -62,20 +55,22 @@ function buildStructuredListEntry(entry) {
62
55
  }
63
56
  function buildStructuredListResult(result) {
64
57
  const { entries, summary, path: resultPath } = result;
58
+ const structuredEntries = [];
59
+ for (const entry of entries) {
60
+ structuredEntries.push(buildStructuredListEntry(entry));
61
+ }
65
62
  return {
66
63
  ok: true,
67
64
  path: resultPath,
68
- entries: entries.map(buildStructuredListEntry),
65
+ entries: structuredEntries,
69
66
  totalEntries: summary.totalEntries,
70
- truncated: summary.truncated,
71
- entriesScanned: summary.entriesScanned,
72
- entriesVisible: summary.entriesVisible,
67
+ ...(summary.truncated ? { truncated: summary.truncated } : {}),
73
68
  totalFiles: summary.totalFiles,
74
69
  totalDirectories: summary.totalDirectories,
75
- maxDepthReached: summary.maxDepthReached,
76
- stoppedReason: summary.stoppedReason,
77
- skippedInaccessible: summary.skippedInaccessible,
78
- symlinksNotFollowed: summary.symlinksNotFollowed,
70
+ ...(summary.stoppedReason ? { stoppedReason: summary.stoppedReason } : {}),
71
+ ...(summary.skippedInaccessible
72
+ ? { skippedInaccessible: summary.skippedInaccessible }
73
+ : {}),
79
74
  };
80
75
  }
81
76
  async function handleListDirectory(args, signal) {
@@ -94,7 +89,13 @@ async function handleListDirectory(args, signal) {
94
89
  return buildToolResponse(buildListTextResult(result), buildStructuredListResult(result));
95
90
  }
96
91
  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 ?? '.' });
92
+ const handler = (args, extra) => executeToolWithDiagnostics({
93
+ toolName: 'ls',
94
+ extra,
95
+ context: { path: args.path ?? '.' },
96
+ run: (signal) => handleListDirectory(args, signal),
97
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, args.path ?? '.'),
98
+ });
98
99
  server.registerTool('ls', withDefaultIcons({ ...LIST_DIRECTORY_TOOL }, options.iconInfo), wrapToolHandler(handler, {
99
100
  guard: options.isInitialized,
100
101
  progressMessage: (args) => {
@@ -1,21 +1,17 @@
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
6
  import { MoveFileInputSchema, MoveFileOutputSchema } from '../schemas.js';
8
- import { buildToolErrorResponse, buildToolResponse, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.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
13
  outputSchema: MoveFileOutputSchema,
14
- annotations: {
15
- readOnlyHint: false,
16
- destructiveHint: true,
17
- openWorldHint: false,
18
- },
14
+ annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
19
15
  };
20
16
  async function handleMoveFile(args, signal) {
21
17
  const validSource = await validateExistingPath(args.source, signal);
@@ -42,17 +38,19 @@ async function handleMoveFile(args, signal) {
42
38
  });
43
39
  }
44
40
  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, {
41
+ const handler = (args, extra) => executeToolWithDiagnostics({
42
+ toolName: 'mv',
43
+ extra,
44
+ timedSignal: {},
45
+ context: { path: args.source },
46
+ run: (signal) => handleMoveFile(args, signal),
47
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.source),
48
+ });
49
+ const wrappedHandler = wrapToolHandler(handler, {
55
50
  guard: options.isInitialized,
56
51
  progressMessage: (args) => `🛠 mv: ${path.basename(args.source)} ➟ ${path.basename(args.destination)}`,
57
- }));
52
+ });
53
+ if (registerToolTaskIfAvailable(server, 'mv', MOVE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
54
+ return;
55
+ server.registerTool('mv', withDefaultIcons({ ...MOVE_FILE_TOOL }, options.iconInfo), wrappedHandler);
58
56
  }