@j0hanz/filesystem-mcp 1.13.2 → 1.14.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.
- package/README.md +162 -145
- package/dist/cli.js +2 -2
- package/dist/completions.js +54 -51
- package/dist/config.d.ts +13 -14
- package/dist/config.js +12 -12
- package/dist/index.js +1 -1
- package/dist/lib/abort.d.ts +7 -0
- package/dist/lib/abort.js +81 -0
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.js +8 -2
- package/dist/lib/errors.d.ts +7 -3
- package/dist/lib/errors.js +64 -41
- package/dist/lib/file-operations/core.d.ts +3 -3
- package/dist/lib/file-operations/core.js +23 -20
- package/dist/lib/file-operations/metadata.d.ts +2 -2
- package/dist/lib/file-operations/metadata.js +69 -22
- package/dist/lib/file-operations/search.d.ts +0 -1
- package/dist/lib/file-operations/search.js +87 -95
- package/dist/lib/file-operations/traversal.js +13 -15
- package/dist/lib/fs-helpers.d.ts +3 -10
- package/dist/lib/fs-helpers.js +29 -108
- package/dist/lib/globs.d.ts +2 -0
- package/dist/lib/globs.js +19 -0
- package/dist/lib/logger.d.ts +28 -0
- package/dist/lib/logger.js +91 -0
- package/dist/lib/observability.d.ts +7 -0
- package/dist/lib/observability.js +19 -9
- package/dist/lib/paths.js +55 -55
- package/dist/lib/resource-store.js +4 -4
- package/dist/lib/utils.d.ts +0 -12
- package/dist/lib/utils.js +0 -13
- package/dist/lib/zod-codecs.d.ts +2 -0
- package/dist/lib/zod-codecs.js +18 -0
- package/dist/pkg-info.d.ts +1 -0
- package/dist/pkg-info.js +2 -2
- package/dist/prompts.js +3 -3
- package/dist/resources/generated-instructions.js +41 -41
- package/dist/resources/tool-catalog.js +33 -58
- package/dist/resources/tool-info.d.ts +0 -1
- package/dist/resources/tool-info.js +44 -67
- package/dist/resources/workflows.js +47 -19
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +4 -4
- package/dist/schemas.d.ts +185 -465
- package/dist/schemas.js +174 -206
- package/dist/server/bootstrap.d.ts +12 -11
- package/dist/server/bootstrap.js +95 -86
- package/dist/server/roots-manager.d.ts +5 -2
- package/dist/server/roots-manager.js +9 -7
- package/dist/server/task-store.d.ts +10 -0
- package/dist/server/task-store.js +73 -0
- package/dist/tools/apply-patch.js +39 -20
- package/dist/tools/calculate-hash.js +14 -27
- package/dist/tools/create-directory.js +11 -9
- package/dist/tools/delete-file.js +19 -19
- package/dist/tools/diff-files.js +16 -18
- package/dist/tools/edit-file.js +11 -5
- package/dist/tools/list-directory.js +16 -21
- package/dist/tools/move-file.js +105 -100
- package/dist/tools/read-multiple.js +15 -10
- package/dist/tools/read.js +6 -7
- package/dist/tools/replace-in-files.js +76 -115
- package/dist/tools/roots.js +3 -7
- package/dist/tools/search-content.js +158 -203
- package/dist/tools/search-files.js +59 -50
- package/dist/tools/shared.d.ts +10 -0
- package/dist/tools/shared.js +105 -36
- package/dist/tools/stat-many.js +15 -9
- package/dist/tools/stat.js +6 -6
- package/dist/tools/task-support.d.ts +10 -9
- package/dist/tools/task-support.js +94 -23
- package/dist/tools/tree.js +4 -4
- package/dist/tools/write-file.js +11 -12
- package/package.json +10 -9
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
1
|
import { createHash } from 'node:crypto';
|
|
4
2
|
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { stat } from 'node:fs/promises';
|
|
4
|
+
import { basename, relative, win32 } from 'node:path';
|
|
5
5
|
import { pipeline } from 'node:stream/promises';
|
|
6
|
+
import { assertNotAborted, withAbort } from '../lib/abort.js';
|
|
6
7
|
import { PARALLEL_CONCURRENCY } from '../lib/constants.js';
|
|
7
8
|
import { ErrorCode } from '../lib/errors.js';
|
|
8
9
|
import { isIgnoredByGitignore, loadRootGitignore, } from '../lib/file-operations/core.js';
|
|
9
10
|
import { globEntries } from '../lib/file-operations/traversal.js';
|
|
10
|
-
import { assertNotAborted, withAbort } from '../lib/fs-helpers.js';
|
|
11
11
|
import { validateExistingPath } from '../lib/paths.js';
|
|
12
|
-
import { reportPeriodicProgress } from '../lib/utils.js';
|
|
13
12
|
import { CalculateHashInputSchema, CalculateHashOutputSchema, } from '../schemas.js';
|
|
14
13
|
import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
15
14
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
@@ -32,8 +31,8 @@ async function hashFile(filePath, encoding, signal) {
|
|
|
32
31
|
return encoding ? hasher.digest(encoding) : hasher.digest();
|
|
33
32
|
}
|
|
34
33
|
function toStableRelativePath(root, entryPath) {
|
|
35
|
-
const relativePath =
|
|
36
|
-
return relativePath.includes(
|
|
34
|
+
const relativePath = relative(root, entryPath);
|
|
35
|
+
return relativePath.includes(win32.sep)
|
|
37
36
|
? relativePath.replace(WINDOWS_PATH_SEPARATOR, '/')
|
|
38
37
|
: relativePath;
|
|
39
38
|
}
|
|
@@ -93,18 +92,11 @@ async function hashDirectory(dirPath, options = {}) {
|
|
|
93
92
|
const fileHash = await hashFile(task.filePath, undefined, signal);
|
|
94
93
|
entries.push({ path: task.relativePath, hash: fileHash });
|
|
95
94
|
filesHashed++;
|
|
96
|
-
|
|
97
|
-
throttleModulo: 25,
|
|
98
|
-
total: totalFiles,
|
|
99
|
-
});
|
|
95
|
+
onProgress?.({ current: filesHashed, total: totalFiles });
|
|
100
96
|
}
|
|
101
97
|
};
|
|
102
98
|
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
103
|
-
|
|
104
|
-
throttleModulo: 25,
|
|
105
|
-
total: totalFiles,
|
|
106
|
-
force: true,
|
|
107
|
-
});
|
|
99
|
+
onProgress?.({ current: filesHashed, total: totalFiles });
|
|
108
100
|
assertNotAborted(signal);
|
|
109
101
|
// Sort by path with byte-wise semantics for deterministic ordering.
|
|
110
102
|
entries.sort(comparePaths);
|
|
@@ -123,7 +115,7 @@ async function hashDirectory(dirPath, options = {}) {
|
|
|
123
115
|
async function handleCalculateHash(args, signal, onProgress) {
|
|
124
116
|
const validPath = await validateExistingPath(args.path, signal);
|
|
125
117
|
// Check if path is a directory or file
|
|
126
|
-
const stats = await withAbort(
|
|
118
|
+
const stats = await withAbort(stat(validPath), signal);
|
|
127
119
|
if (stats.isDirectory()) {
|
|
128
120
|
// Hash directory: composite hash of all files
|
|
129
121
|
const { hash, fileCount } = await hashDirectory(validPath, {
|
|
@@ -141,10 +133,7 @@ async function handleCalculateHash(args, signal, onProgress) {
|
|
|
141
133
|
else {
|
|
142
134
|
// Hash single file
|
|
143
135
|
const hash = await hashFile(validPath, 'hex', signal);
|
|
144
|
-
|
|
145
|
-
throttleModulo: 25,
|
|
146
|
-
force: true,
|
|
147
|
-
});
|
|
136
|
+
onProgress?.({ current: 1 });
|
|
148
137
|
return buildToolResponse(hash, {
|
|
149
138
|
ok: true,
|
|
150
139
|
path: validPath,
|
|
@@ -157,10 +146,11 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
157
146
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
158
147
|
toolName: 'calculate_hash',
|
|
159
148
|
extra,
|
|
149
|
+
outputSchema: CalculateHashOutputSchema,
|
|
160
150
|
timedSignal: {},
|
|
161
151
|
context: { path: args.path },
|
|
162
152
|
run: async (signal) => {
|
|
163
|
-
const baseName =
|
|
153
|
+
const baseName = basename(args.path);
|
|
164
154
|
const progress = createToolProgressSession(extra, `🕮 hash: ${baseName}`);
|
|
165
155
|
const progressWithMessage = ({ current, total, }) => {
|
|
166
156
|
progress.update({
|
|
@@ -172,13 +162,10 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
172
162
|
try {
|
|
173
163
|
const result = await handleCalculateHash(args, signal, progressWithMessage);
|
|
174
164
|
const sc = result.structuredContent;
|
|
175
|
-
const totalFiles = sc.
|
|
165
|
+
const totalFiles = sc.fileCount ?? 1;
|
|
176
166
|
const finalCurrent = resolveFinalProgressCurrent(progress, totalFiles + 1);
|
|
177
167
|
let suffix;
|
|
178
|
-
if (
|
|
179
|
-
suffix = 'failed';
|
|
180
|
-
}
|
|
181
|
-
else if (sc.fileCount !== undefined && sc.fileCount > 1) {
|
|
168
|
+
if (sc.fileCount !== undefined && sc.fileCount > 1) {
|
|
182
169
|
suffix = `${sc.fileCount} files • ${(sc.hash ?? '').slice(0, 8)}…`;
|
|
183
170
|
}
|
|
184
171
|
else {
|
|
@@ -192,7 +179,7 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
192
179
|
throw error;
|
|
193
180
|
}
|
|
194
181
|
},
|
|
195
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.
|
|
182
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
|
|
196
183
|
});
|
|
197
184
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
198
185
|
guard: options.isInitialized,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { withAbort } from '../lib/abort.js';
|
|
3
4
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
4
|
-
import { withAbort } from '../lib/fs-helpers.js';
|
|
5
5
|
import { validatePathForWrite } from '../lib/paths.js';
|
|
6
6
|
import { CreateDirectoryInputSchema, CreateDirectoryOutputSchema, } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, IDEMPOTENT_WRITE_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
@@ -13,7 +13,6 @@ export const CREATE_DIRECTORY_TOOL = {
|
|
|
13
13
|
inputSchema: CreateDirectoryInputSchema,
|
|
14
14
|
outputSchema: CreateDirectoryOutputSchema,
|
|
15
15
|
annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
|
|
16
|
-
nuances: ['Succeeds silently if the directory already exists (idempotent).'],
|
|
17
16
|
taskSupport: 'forbidden',
|
|
18
17
|
};
|
|
19
18
|
async function handleCreateDirectory(args, signal) {
|
|
@@ -23,12 +22,14 @@ async function handleCreateDirectory(args, signal) {
|
|
|
23
22
|
if (args.paths)
|
|
24
23
|
allPaths.push(...args.paths);
|
|
25
24
|
if (allPaths.length === 0) {
|
|
26
|
-
throw new McpError(ErrorCode.
|
|
25
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'No paths provided to create.');
|
|
27
26
|
}
|
|
28
27
|
const validPaths = await Promise.all(allPaths.map((p) => validatePathForWrite(p, signal)));
|
|
29
|
-
await Promise.all(validPaths.map((p) => withAbort(
|
|
28
|
+
await Promise.all(validPaths.map((p) => withAbort(mkdir(p, { recursive: true }), signal)));
|
|
29
|
+
const createdPath = validPaths.length === 1 ? validPaths[0] : undefined;
|
|
30
30
|
return buildToolResponse(`Successfully created ${validPaths.length} director${validPaths.length === 1 ? 'y' : 'ies'}`, {
|
|
31
31
|
ok: true,
|
|
32
|
+
...(createdPath ? { path: createdPath } : {}),
|
|
32
33
|
paths: validPaths,
|
|
33
34
|
});
|
|
34
35
|
}
|
|
@@ -36,23 +37,24 @@ export function registerCreateDirectoryTool(server, options = {}) {
|
|
|
36
37
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
37
38
|
toolName: 'mkdir',
|
|
38
39
|
extra,
|
|
40
|
+
outputSchema: CreateDirectoryOutputSchema,
|
|
39
41
|
timedSignal: {},
|
|
40
42
|
context: { path: args.path ?? args.paths?.[0] },
|
|
41
43
|
run: (signal) => handleCreateDirectory(args, signal),
|
|
42
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.
|
|
44
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path ?? args.paths?.[0]),
|
|
43
45
|
});
|
|
44
46
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
45
47
|
guard: options.isInitialized,
|
|
46
48
|
progressMessage: (args) => {
|
|
47
49
|
if (args.path && !args.paths?.length) {
|
|
48
|
-
return `🛠 mkdir: ${
|
|
50
|
+
return `🛠 mkdir: ${basename(args.path)}`;
|
|
49
51
|
}
|
|
50
52
|
const count = (args.path ? 1 : 0) + (args.paths?.length ?? 0);
|
|
51
53
|
return `🛠 mkdir: ${count} directories`;
|
|
52
54
|
},
|
|
53
55
|
completionMessage: (args, result) => {
|
|
54
56
|
if (args.path && !args.paths?.length) {
|
|
55
|
-
const name =
|
|
57
|
+
const name = basename(args.path);
|
|
56
58
|
if (result.isError)
|
|
57
59
|
return `🛠 mkdir: ${name} • failed`;
|
|
58
60
|
return `🛠 mkdir: ${name}`;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { lstat, rm, rmdir } from 'node:fs/promises';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { withAbort } from '../lib/abort.js';
|
|
3
4
|
import { ErrorCode, isNodeError, McpError } from '../lib/errors.js';
|
|
4
|
-
import {
|
|
5
|
+
import { Logger } from '../lib/logger.js';
|
|
5
6
|
import { isAllowedDirectoryRoot, validatePathForWrite } from '../lib/paths.js';
|
|
6
7
|
import { DeleteFileInputSchema, DeleteFileOutputSchema } from '../schemas.js';
|
|
7
8
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
@@ -13,20 +14,17 @@ export const DELETE_FILE_TOOL = {
|
|
|
13
14
|
inputSchema: DeleteFileInputSchema,
|
|
14
15
|
outputSchema: DeleteFileOutputSchema,
|
|
15
16
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
16
|
-
gotchas: [
|
|
17
|
-
'No undo — deletion is permanent.',
|
|
18
|
-
'Non-empty directories require `recursive=true`.',
|
|
19
|
-
],
|
|
17
|
+
gotchas: ['Non-empty directories require `recursive=true`.'],
|
|
20
18
|
taskSupport: 'forbidden',
|
|
21
19
|
};
|
|
22
20
|
async function handleDeleteFile(args, signal) {
|
|
23
21
|
const validPath = await validatePathForWrite(args.path, signal);
|
|
24
22
|
if (isAllowedDirectoryRoot(validPath)) {
|
|
25
|
-
throw new McpError(ErrorCode.
|
|
23
|
+
throw new McpError(ErrorCode.ACCESS_DENIED, 'Deleting a workspace root directory is not allowed');
|
|
26
24
|
}
|
|
27
25
|
let stats;
|
|
28
26
|
try {
|
|
29
|
-
stats = await withAbort(
|
|
27
|
+
stats = await withAbort(lstat(validPath), signal);
|
|
30
28
|
}
|
|
31
29
|
catch (error) {
|
|
32
30
|
if (isNodeError(error) &&
|
|
@@ -42,14 +40,15 @@ async function handleDeleteFile(args, signal) {
|
|
|
42
40
|
if (stats.isDirectory() && !args.recursive) {
|
|
43
41
|
// Use rmdir for non-recursive directory deletes so non-empty directories
|
|
44
42
|
// consistently return ENOTEMPTY-style errors with actionable guidance.
|
|
45
|
-
await withAbort(
|
|
43
|
+
await withAbort(rmdir(validPath), signal);
|
|
46
44
|
}
|
|
47
45
|
else {
|
|
48
|
-
await withAbort(
|
|
46
|
+
await withAbort(rm(validPath, {
|
|
49
47
|
recursive: args.recursive,
|
|
50
48
|
force: args.ignoreIfNotExists,
|
|
51
49
|
}), signal);
|
|
52
50
|
}
|
|
51
|
+
Logger.info(`rm: ${args.path}`);
|
|
53
52
|
return buildToolResponse(`Successfully deleted: ${args.path}`, {
|
|
54
53
|
ok: true,
|
|
55
54
|
path: validPath,
|
|
@@ -59,35 +58,36 @@ export function registerDeleteFileTool(server, options = {}) {
|
|
|
59
58
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
60
59
|
toolName: 'rm',
|
|
61
60
|
extra,
|
|
61
|
+
outputSchema: DeleteFileOutputSchema,
|
|
62
62
|
timedSignal: {},
|
|
63
63
|
context: { path: args.path },
|
|
64
64
|
run: (signal) => handleDeleteFile(args, signal),
|
|
65
65
|
onError: (error) => {
|
|
66
66
|
if (isNodeError(error)) {
|
|
67
67
|
if (error.code === 'ENOENT') {
|
|
68
|
-
return buildToolErrorResponse(error, ErrorCode.
|
|
68
|
+
return buildToolErrorResponse(error, ErrorCode.NOT_FOUND, args.path);
|
|
69
69
|
}
|
|
70
70
|
if (error.code === 'ENOTEMPTY') {
|
|
71
|
-
return buildToolErrorResponse(new Error(
|
|
71
|
+
return buildToolErrorResponse(new Error('Directory not empty. Set recursive: true.'), ErrorCode.INVALID_INPUT, args.path);
|
|
72
72
|
}
|
|
73
73
|
if (error.code === 'EISDIR') {
|
|
74
|
-
return buildToolErrorResponse(new Error(
|
|
74
|
+
return buildToolErrorResponse(new Error('Path is a directory. Set recursive: true.'), ErrorCode.INVALID_INPUT, args.path);
|
|
75
75
|
}
|
|
76
76
|
if (error.code === 'EEXIST') {
|
|
77
|
-
return buildToolErrorResponse(new Error(
|
|
77
|
+
return buildToolErrorResponse(new Error('Directory not empty. Set recursive: true.'), ErrorCode.INVALID_INPUT, args.path);
|
|
78
78
|
}
|
|
79
79
|
if (error.code === 'EPERM' || error.code === 'EACCES') {
|
|
80
|
-
return buildToolErrorResponse(error, ErrorCode.
|
|
80
|
+
return buildToolErrorResponse(error, ErrorCode.PERMISSION_DENIED, args.path);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
return buildToolErrorResponse(error, ErrorCode.
|
|
83
|
+
return buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path);
|
|
84
84
|
},
|
|
85
85
|
});
|
|
86
86
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
87
87
|
guard: options.isInitialized,
|
|
88
|
-
progressMessage: (args) => `🛠 rm: ${
|
|
88
|
+
progressMessage: (args) => `🛠 rm: ${basename(args.path)}`,
|
|
89
89
|
completionMessage: (args, result) => {
|
|
90
|
-
const name =
|
|
90
|
+
const name = basename(args.path);
|
|
91
91
|
if (result.isError)
|
|
92
92
|
return `🛠 rm: ${name} • failed`;
|
|
93
93
|
return `🛠 rm: ${name}`;
|
package/dist/tools/diff-files.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
3
|
import { formatPatch, structuredPatch } from 'diff';
|
|
4
|
+
import { withAbort } from '../lib/abort.js';
|
|
4
5
|
import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
|
|
5
6
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
6
|
-
import { withAbort } from '../lib/fs-helpers.js';
|
|
7
7
|
import { validateExistingPath } from '../lib/paths.js';
|
|
8
8
|
import { DiffFilesInputSchema, DiffFilesOutputSchema } from '../schemas.js';
|
|
9
9
|
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
@@ -17,7 +17,6 @@ export const DIFF_FILES_TOOL = {
|
|
|
17
17
|
inputSchema: DiffFilesInputSchema,
|
|
18
18
|
outputSchema: DiffFilesOutputSchema,
|
|
19
19
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
20
|
-
gotchas: ['`isIdentical=true` means no hunks (`@@`) and empty diff.'],
|
|
21
20
|
taskSupport: 'forbidden',
|
|
22
21
|
};
|
|
23
22
|
function computeDiffStats(hunks) {
|
|
@@ -36,7 +35,7 @@ function computeDiffStats(hunks) {
|
|
|
36
35
|
function assertDiffFileSizeWithinLimit(filePath, size, maxFileSize) {
|
|
37
36
|
if (size <= maxFileSize)
|
|
38
37
|
return;
|
|
39
|
-
throw new McpError(ErrorCode.
|
|
38
|
+
throw new McpError(ErrorCode.TOO_LARGE, `File too large for diff (${size} bytes > ${maxFileSize} bytes).`, filePath, { size, maxFileSize });
|
|
40
39
|
}
|
|
41
40
|
async function handleDiffFiles(args, signal, resourceStore) {
|
|
42
41
|
const maxFileSize = MAX_TEXT_FILE_SIZE;
|
|
@@ -45,17 +44,17 @@ async function handleDiffFiles(args, signal, resourceStore) {
|
|
|
45
44
|
validateExistingPath(args.modified, signal),
|
|
46
45
|
]);
|
|
47
46
|
const [originalStats, modifiedStats] = await Promise.all([
|
|
48
|
-
withAbort(
|
|
49
|
-
withAbort(
|
|
47
|
+
withAbort(stat(originalPath), signal),
|
|
48
|
+
withAbort(stat(modifiedPath), signal),
|
|
50
49
|
]);
|
|
51
50
|
assertDiffFileSizeWithinLimit(originalPath, originalStats.size, maxFileSize);
|
|
52
51
|
assertDiffFileSizeWithinLimit(modifiedPath, modifiedStats.size, maxFileSize);
|
|
53
52
|
const [originalContent, modifiedContent] = await Promise.all([
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
readFile(originalPath, { encoding: 'utf-8', signal }),
|
|
54
|
+
readFile(modifiedPath, { encoding: 'utf-8', signal }),
|
|
56
55
|
]);
|
|
57
56
|
const patchObj = await new Promise((resolve) => {
|
|
58
|
-
structuredPatch(
|
|
57
|
+
structuredPatch(basename(originalPath), basename(modifiedPath), originalContent, modifiedContent, undefined, undefined, {
|
|
59
58
|
...(args.context !== undefined ? { context: args.context } : {}),
|
|
60
59
|
ignoreWhitespace: args.ignoreWhitespace,
|
|
61
60
|
stripTrailingCr: args.stripTrailingCr,
|
|
@@ -66,7 +65,7 @@ async function handleDiffFiles(args, signal, resourceStore) {
|
|
|
66
65
|
});
|
|
67
66
|
});
|
|
68
67
|
if (!patchObj) {
|
|
69
|
-
throw new McpError(ErrorCode.
|
|
68
|
+
throw new McpError(ErrorCode.TIMEOUT, `Diff timed out or too complex.`, originalPath);
|
|
70
69
|
}
|
|
71
70
|
const isIdentical = patchObj.hunks.length === 0;
|
|
72
71
|
const diffText = isIdentical ? '' : formatPatch(patchObj);
|
|
@@ -105,26 +104,25 @@ export function registerDiffFilesTool(server, options = {}) {
|
|
|
105
104
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
106
105
|
toolName: 'diff_files',
|
|
107
106
|
extra,
|
|
107
|
+
outputSchema: DiffFilesOutputSchema,
|
|
108
108
|
timedSignal: {},
|
|
109
109
|
context: { path: args.original },
|
|
110
110
|
run: (signal) => handleDiffFiles(args, signal, options.resourceStore),
|
|
111
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.
|
|
111
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.original),
|
|
112
112
|
});
|
|
113
113
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
114
114
|
guard: options.isInitialized,
|
|
115
115
|
progressMessage: (args) => {
|
|
116
|
-
const n1 =
|
|
117
|
-
const n2 =
|
|
116
|
+
const n1 = basename(args.original);
|
|
117
|
+
const n2 = basename(args.modified);
|
|
118
118
|
return `🕮 diff: ${n1} ⟷ ${n2}`;
|
|
119
119
|
},
|
|
120
120
|
completionMessage: (args, result) => {
|
|
121
|
-
const n1 =
|
|
122
|
-
const n2 =
|
|
121
|
+
const n1 = basename(args.original);
|
|
122
|
+
const n2 = basename(args.modified);
|
|
123
123
|
if (result.isError)
|
|
124
124
|
return `🕮 diff: ${n1} ⟷ ${n2} • failed`;
|
|
125
125
|
const sc = result.structuredContent;
|
|
126
|
-
if (!sc.ok)
|
|
127
|
-
return `🕮 diff: ${n1} ⟷ ${n2} • failed`;
|
|
128
126
|
if (sc.isIdentical)
|
|
129
127
|
return `🕮 diff: ${n1} ⟷ ${n2} • identical`;
|
|
130
128
|
const added = sc.linesAdded ?? 0;
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -2,9 +2,11 @@ import { readFile, stat } from 'node:fs/promises';
|
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
3
|
import { createTwoFilesPatch, diffLines } from 'diff';
|
|
4
4
|
import RE2 from 're2';
|
|
5
|
+
import { withAbort } from '../lib/abort.js';
|
|
5
6
|
import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
|
|
6
7
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
7
|
-
import { atomicWriteFile
|
|
8
|
+
import { atomicWriteFile } from '../lib/fs-helpers.js';
|
|
9
|
+
import { Logger } from '../lib/logger.js';
|
|
8
10
|
import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
|
|
9
11
|
import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
|
|
10
12
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
@@ -19,7 +21,6 @@ export const EDIT_FILE_TOOL = {
|
|
|
19
21
|
outputSchema: EditFileOutputSchema,
|
|
20
22
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
21
23
|
nuances: ['Each edit applies to the output of the previous edit.'],
|
|
22
|
-
gotchas: ['Unmatched `oldText` entries listed in `unmatchedEdits`.'],
|
|
23
24
|
taskSupport: 'forbidden',
|
|
24
25
|
};
|
|
25
26
|
function escapeRegExp(string) {
|
|
@@ -104,7 +105,7 @@ function mergeLineRange(currentRange, content, matchStartIndex, newText) {
|
|
|
104
105
|
}
|
|
105
106
|
function buildStructuredEditOutput(validPath, result) {
|
|
106
107
|
return {
|
|
107
|
-
ok:
|
|
108
|
+
ok: result.appliedEdits > 0 || result.unmatchedEdits.length === 0,
|
|
108
109
|
path: validPath,
|
|
109
110
|
appliedEdits: result.appliedEdits,
|
|
110
111
|
...(result.appliedEdits > 0
|
|
@@ -162,7 +163,7 @@ async function loadEditableFile(requestedPath, signal) {
|
|
|
162
163
|
assertAllowedFileAccess(requestedPath, validPath);
|
|
163
164
|
const stats = await withAbort(stat(validPath), signal);
|
|
164
165
|
if (stats.size > MAX_TEXT_FILE_SIZE) {
|
|
165
|
-
throw new McpError(ErrorCode.
|
|
166
|
+
throw new McpError(ErrorCode.TOO_LARGE, `File too large for edit (${stats.size} bytes > ${MAX_TEXT_FILE_SIZE} bytes)`, requestedPath, { size: stats.size, maxFileSize: MAX_TEXT_FILE_SIZE });
|
|
166
167
|
}
|
|
167
168
|
const content = await readFile(validPath, { encoding: 'utf-8', signal });
|
|
168
169
|
return { validPath, content };
|
|
@@ -215,11 +216,15 @@ async function handleEditFile(args, signal) {
|
|
|
215
216
|
}
|
|
216
217
|
return buildToolResponse(`Dry run complete. ${editResult.appliedEdits} edits would be applied.`, structured);
|
|
217
218
|
}
|
|
219
|
+
if (editResult.appliedEdits === 0 && editResult.unmatchedEdits.length > 0) {
|
|
220
|
+
throw new McpError(ErrorCode.INVALID_INPUT, `All ${editResult.unmatchedEdits.length} edits failed. Verify oldText matches exact file content.`, args.path);
|
|
221
|
+
}
|
|
218
222
|
if (editResult.appliedEdits > 0) {
|
|
219
223
|
await atomicWriteFile(validPath, editResult.content, {
|
|
220
224
|
encoding: 'utf-8',
|
|
221
225
|
signal,
|
|
222
226
|
});
|
|
227
|
+
Logger.info(`edit: ${args.path} (${editResult.appliedEdits} edits, +${editResult.linesAdded}/-${editResult.linesRemoved})`);
|
|
223
228
|
}
|
|
224
229
|
return buildToolResponse(buildEditMessage(args.path, editResult), structured);
|
|
225
230
|
}
|
|
@@ -227,10 +232,11 @@ export function registerEditFileTool(server, options = {}) {
|
|
|
227
232
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
228
233
|
toolName: 'edit',
|
|
229
234
|
extra,
|
|
235
|
+
outputSchema: EditFileOutputSchema,
|
|
230
236
|
timedSignal: {},
|
|
231
237
|
context: { path: args.path },
|
|
232
238
|
run: (signal) => handleEditFile(args, signal),
|
|
233
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.
|
|
239
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
|
|
234
240
|
});
|
|
235
241
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
236
242
|
guard: options.isInitialized,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import * as path from 'node:path';
|
|
2
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import { DEFAULT_EXCLUDE_PATTERNS, MAX_LIST_ENTRIES, } from '../lib/constants.js';
|
|
4
5
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
5
6
|
import { listDirectory } from '../lib/file-operations/metadata.js';
|
|
7
|
+
import { createBase64JsonCodec } from '../lib/zod-codecs.js';
|
|
6
8
|
import { formatOperationSummary, joinLines } from '../config.js';
|
|
7
9
|
import { ListDirectoryInputSchema, ListDirectoryOutputSchema, } from '../schemas.js';
|
|
8
10
|
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
@@ -23,6 +25,11 @@ const LIST_CURSOR_TTL_MS = parseInt(process.env['FS_CONTEXT_LIST_CURSOR_TTL_MS']
|
|
|
23
25
|
5 * 60 * 1000;
|
|
24
26
|
const listSnapshots = new Map();
|
|
25
27
|
const listSnapshotTimers = new Map();
|
|
28
|
+
const ListCursorPayloadSchema = z.strictObject({
|
|
29
|
+
snapshotId: z.string().min(1),
|
|
30
|
+
offset: z.int().min(0),
|
|
31
|
+
});
|
|
32
|
+
const ListCursorCodec = createBase64JsonCodec(ListCursorPayloadSchema);
|
|
26
33
|
function buildListFingerprint(args) {
|
|
27
34
|
return JSON.stringify({
|
|
28
35
|
path: resolvePathOrRoot(args.path),
|
|
@@ -55,27 +62,16 @@ function storeListSnapshot(snapshot) {
|
|
|
55
62
|
return snapshotId;
|
|
56
63
|
}
|
|
57
64
|
function encodeListCursor(payload) {
|
|
58
|
-
return
|
|
65
|
+
return z.encode(ListCursorCodec, payload);
|
|
59
66
|
}
|
|
60
67
|
function decodeListCursor(cursor) {
|
|
61
68
|
try {
|
|
62
|
-
|
|
63
|
-
if (typeof parsed === 'object' &&
|
|
64
|
-
parsed !== null &&
|
|
65
|
-
typeof parsed.snapshotId === 'string' &&
|
|
66
|
-
typeof parsed.offset === 'number') {
|
|
67
|
-
const payload = parsed;
|
|
68
|
-
if (payload.snapshotId.length > 0 &&
|
|
69
|
-
Number.isInteger(payload.offset) &&
|
|
70
|
-
payload.offset >= 0) {
|
|
71
|
-
return payload;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
69
|
+
return ListCursorCodec.parse(cursor);
|
|
74
70
|
}
|
|
75
71
|
catch {
|
|
76
72
|
// fall through to throw
|
|
77
73
|
}
|
|
78
|
-
throw new McpError(ErrorCode.
|
|
74
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Invalid or expired cursor.');
|
|
79
75
|
}
|
|
80
76
|
function resolveNextListCursor(snapshotId, offset, pageSize, totalEntries) {
|
|
81
77
|
if (!snapshotId)
|
|
@@ -170,7 +166,7 @@ async function handleListDirectory(args, signal) {
|
|
|
170
166
|
const cursor = decodeListCursor(args.cursor);
|
|
171
167
|
const snapshot = listSnapshots.get(cursor.snapshotId);
|
|
172
168
|
if (snapshot?.fingerprint !== fingerprint) {
|
|
173
|
-
throw new McpError(ErrorCode.
|
|
169
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Invalid or expired cursor.');
|
|
174
170
|
}
|
|
175
171
|
const { offset, snapshotId: storedSnapshotId } = cursor;
|
|
176
172
|
cursorOffset = offset;
|
|
@@ -201,20 +197,19 @@ export function registerListDirectoryTool(server, options = {}) {
|
|
|
201
197
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
202
198
|
toolName: 'ls',
|
|
203
199
|
extra,
|
|
200
|
+
outputSchema: ListDirectoryOutputSchema,
|
|
204
201
|
context: { path: args.path ?? '.' },
|
|
205
202
|
run: (signal) => handleListDirectory(args, signal),
|
|
206
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.
|
|
203
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.NOT_DIRECTORY, args.path ?? '.'),
|
|
207
204
|
});
|
|
208
205
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
209
206
|
guard: options.isInitialized,
|
|
210
|
-
progressMessage: (args) => `≣ ls: ${args.path ?
|
|
207
|
+
progressMessage: (args) => `≣ ls: ${args.path ? basename(args.path) : '.'}`,
|
|
211
208
|
completionMessage: (args, result) => {
|
|
212
|
-
const base = args.path ?
|
|
209
|
+
const base = args.path ? basename(args.path) : '.';
|
|
213
210
|
if (result.isError)
|
|
214
211
|
return `≣ ls: ${base} • failed`;
|
|
215
212
|
const sc = result.structuredContent;
|
|
216
|
-
if (!sc.ok)
|
|
217
|
-
return `≣ ls: ${base} • failed`;
|
|
218
213
|
const count = sc.totalEntries ?? 0;
|
|
219
214
|
return `≣ ls: ${base} • ${count} ${count === 1 ? 'entry' : 'entries'}`;
|
|
220
215
|
},
|