@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.
- package/README.md +514 -188
- package/dist/cli.js +29 -12
- package/dist/completions.js +50 -24
- package/dist/config.d.ts +3 -2
- package/dist/config.js +1 -1
- package/dist/index.js +14 -12
- package/dist/instructions.md +109 -97
- package/dist/lib/constants.js +25 -14
- package/dist/lib/errors.js +11 -6
- package/dist/lib/file-operations/common.d.ts +4 -0
- package/dist/lib/file-operations/common.js +9 -0
- package/dist/lib/file-operations/file-info.js +22 -10
- package/dist/lib/file-operations/gitignore.js +14 -11
- package/dist/lib/file-operations/glob-engine.d.ts +1 -0
- package/dist/lib/file-operations/glob-engine.js +46 -33
- package/dist/lib/file-operations/list-directory.js +31 -35
- package/dist/lib/file-operations/read-multiple-files.js +70 -62
- package/dist/lib/file-operations/search-content.js +83 -64
- package/dist/lib/file-operations/search-files.js +32 -30
- package/dist/lib/file-operations/search-worker.js +22 -12
- package/dist/lib/file-operations/tree.js +43 -34
- package/dist/lib/fs-helpers.js +61 -124
- package/dist/lib/observability.js +29 -28
- package/dist/lib/path-format.d.ts +1 -0
- package/dist/lib/path-format.js +7 -0
- package/dist/lib/path-policy.js +22 -20
- package/dist/lib/path-validation.js +13 -7
- package/dist/lib/resource-store.d.ts +2 -0
- package/dist/lib/resource-store.js +26 -5
- package/dist/lib/type-guards.d.ts +1 -0
- package/dist/lib/type-guards.js +3 -0
- package/dist/prompts.d.ts +1 -5
- package/dist/prompts.js +9 -16
- package/dist/resources.d.ts +1 -5
- package/dist/resources.js +12 -26
- package/dist/schemas.d.ts +213 -30
- package/dist/schemas.js +52 -90
- package/dist/server.js +85 -44
- package/dist/tools/apply-patch.js +24 -24
- package/dist/tools/calculate-hash.js +42 -45
- package/dist/tools/create-directory.js +18 -21
- package/dist/tools/delete-file.js +36 -39
- package/dist/tools/diff-files.js +16 -21
- package/dist/tools/edit-file.js +16 -20
- package/dist/tools/list-directory.js +25 -25
- package/dist/tools/move-file.js +18 -21
- package/dist/tools/read-multiple.js +56 -68
- package/dist/tools/read.js +27 -32
- package/dist/tools/replace-in-files.js +28 -35
- package/dist/tools/roots.js +9 -10
- package/dist/tools/search-content.js +74 -74
- package/dist/tools/search-files.js +45 -52
- package/dist/tools/shared.d.ts +44 -6
- package/dist/tools/shared.js +86 -64
- package/dist/tools/stat-many.js +45 -68
- package/dist/tools/stat.js +11 -39
- package/dist/tools/task-support.d.ts +9 -1
- package/dist/tools/task-support.js +86 -81
- package/dist/tools/tree.js +13 -30
- package/dist/tools/write-file.js +18 -21
- package/dist/tools.js +23 -18
- 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,
|
|
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,
|
|
12
|
-
import { buildToolErrorResponse, buildToolResponse, createProgressReporter,
|
|
13
|
-
import {
|
|
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
|
-
|
|
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
|
-
//
|
|
68
|
-
const
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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) =>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
8
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
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
|
-
|
|
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) =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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 {
|
|
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,
|
|
8
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
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
|
-
|
|
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) =>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
}
|
package/dist/tools/diff-files.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
10
|
-
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, maybeExternalizeTextContent,
|
|
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
|
-
|
|
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 =
|
|
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) =>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
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) => {
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -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
|
|
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
|
|
8
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
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
|
-
|
|
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) =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
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 {
|
|
7
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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:
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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) =>
|
|
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) => {
|
package/dist/tools/move-file.js
CHANGED
|
@@ -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 {
|
|
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
|
|
8
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
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
|
-
|
|
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) =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
}
|