@j0hanz/filesystem-mcp 1.13.2 → 1.14.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 +117 -100
- package/dist/config.d.ts +0 -1
- package/dist/lib/errors.js +5 -2
- package/dist/lib/file-operations/metadata.js +9 -3
- package/dist/lib/file-operations/search.d.ts +0 -1
- package/dist/lib/file-operations/search.js +5 -12
- package/dist/lib/fs-helpers.js +10 -11
- package/dist/lib/globs.d.ts +2 -0
- package/dist/lib/globs.js +19 -0
- 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 +3 -12
- package/dist/resources/tool-catalog.js +10 -41
- package/dist/resources/tool-info.d.ts +0 -1
- package/dist/resources/tool-info.js +11 -39
- package/dist/resources/workflows.js +8 -1
- package/dist/schemas.d.ts +179 -459
- package/dist/schemas.js +156 -165
- package/dist/server/roots-manager.js +1 -1
- package/dist/tools/apply-patch.js +19 -8
- package/dist/tools/calculate-hash.js +3 -5
- package/dist/tools/create-directory.js +1 -1
- package/dist/tools/delete-file.js +2 -4
- package/dist/tools/diff-files.js +1 -3
- package/dist/tools/edit-file.js +5 -2
- package/dist/tools/list-directory.js +10 -15
- package/dist/tools/move-file.js +14 -26
- package/dist/tools/read-multiple.js +12 -7
- package/dist/tools/read.js +1 -2
- package/dist/tools/replace-in-files.js +58 -94
- package/dist/tools/roots.js +2 -6
- package/dist/tools/search-content.js +150 -186
- package/dist/tools/search-files.js +5 -9
- package/dist/tools/shared.d.ts +7 -0
- package/dist/tools/shared.js +38 -11
- package/dist/tools/stat-many.js +6 -4
- package/dist/tools/stat.js +2 -2
- package/dist/tools/tree.js +1 -1
- package/dist/tools/write-file.js +1 -5
- package/package.json +2 -1
|
@@ -2,11 +2,11 @@ import * as path from 'node:path';
|
|
|
2
2
|
import { readFile, stat } from 'node:fs/promises';
|
|
3
3
|
import { applyPatch, parsePatch } from 'diff';
|
|
4
4
|
import { MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY } from '../lib/constants.js';
|
|
5
|
-
import { ErrorCode,
|
|
5
|
+
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
6
6
|
import { atomicWriteFile, processInParallel, withAbort, } from '../lib/fs-helpers.js';
|
|
7
7
|
import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
|
|
8
8
|
import { ApplyPatchInputSchema, ApplyPatchOutputSchema } from '../schemas.js';
|
|
9
|
-
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
9
|
+
import { buildStructuredError, buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
10
10
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
11
11
|
export const APPLY_PATCH_TOOL = {
|
|
12
12
|
name: 'apply_patch',
|
|
@@ -21,7 +21,6 @@ export const APPLY_PATCH_TOOL = {
|
|
|
21
21
|
nuances: [
|
|
22
22
|
'Multi-file patches use `path` as base directory; per-file results in `results[]`.',
|
|
23
23
|
],
|
|
24
|
-
gotchas: ['Patch must include valid hunk headers; use `dryRun=true` first.'],
|
|
25
24
|
taskSupport: 'forbidden',
|
|
26
25
|
};
|
|
27
26
|
function assertPatchTargetSizeWithinLimit(filePath, size, maxFileSize) {
|
|
@@ -68,11 +67,15 @@ async function applyDiff(filePath, diff, options, signal) {
|
|
|
68
67
|
return {
|
|
69
68
|
path: validPath,
|
|
70
69
|
applied: false,
|
|
71
|
-
error: 'Patch application failed',
|
|
70
|
+
error: buildStructuredError(new McpError(ErrorCode.E_INVALID_INPUT, 'Patch application failed', validPath), ErrorCode.E_INVALID_INPUT, validPath),
|
|
72
71
|
};
|
|
73
72
|
}
|
|
74
73
|
if (patched === content) {
|
|
75
|
-
return {
|
|
74
|
+
return {
|
|
75
|
+
path: validPath,
|
|
76
|
+
applied: false,
|
|
77
|
+
error: buildStructuredError(new McpError(ErrorCode.E_INVALID_INPUT, 'Patch had no effect', validPath), ErrorCode.E_INVALID_INPUT, validPath),
|
|
78
|
+
};
|
|
76
79
|
}
|
|
77
80
|
const patchStats = countStructuredPatchStats(diff);
|
|
78
81
|
if (!options.dryRun) {
|
|
@@ -88,7 +91,7 @@ async function processMultiFilePatch(basePath, parsed, options, signal) {
|
|
|
88
91
|
return () => Promise.resolve({
|
|
89
92
|
path: '<unknown>',
|
|
90
93
|
applied: false,
|
|
91
|
-
error: 'Missing file name in patch header',
|
|
94
|
+
error: buildStructuredError(new McpError(ErrorCode.E_INVALID_INPUT, 'Missing file name in patch header'), ErrorCode.E_INVALID_INPUT),
|
|
92
95
|
});
|
|
93
96
|
}
|
|
94
97
|
const filePath = path.resolve(validBase, fileName);
|
|
@@ -101,7 +104,7 @@ async function processMultiFilePatch(basePath, parsed, options, signal) {
|
|
|
101
104
|
return {
|
|
102
105
|
path: fileName,
|
|
103
106
|
applied: false,
|
|
104
|
-
error:
|
|
107
|
+
error: buildStructuredError(error, ErrorCode.E_INVALID_INPUT, filePath),
|
|
105
108
|
};
|
|
106
109
|
}
|
|
107
110
|
};
|
|
@@ -117,6 +120,13 @@ async function processMultiFilePatch(basePath, parsed, options, signal) {
|
|
|
117
120
|
return acc;
|
|
118
121
|
}, { applied: 0, hunks: 0, added: 0, removed: 0 });
|
|
119
122
|
const label = options.dryRun ? ' (dry run)' : '';
|
|
123
|
+
if (totals.applied === 0) {
|
|
124
|
+
const failedPaths = results
|
|
125
|
+
.filter((r) => !r.applied)
|
|
126
|
+
.map((r) => r.path)
|
|
127
|
+
.join(', ');
|
|
128
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, `All ${parsed.length} file patches failed${label}. Files: ${failedPaths}. Generate fresh patches via diff_files and retry.`);
|
|
129
|
+
}
|
|
120
130
|
const text = `Applied ${totals.applied}/${parsed.length} file patches${label}`;
|
|
121
131
|
return buildToolResponse(text, {
|
|
122
132
|
ok: totals.applied === parsed.length,
|
|
@@ -152,7 +162,7 @@ async function handleApplyPatch(args, signal) {
|
|
|
152
162
|
}
|
|
153
163
|
const result = await applyDiff(args.path, diff, options, signal);
|
|
154
164
|
if (!result.applied) {
|
|
155
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT, result.error === 'Patch had no effect'
|
|
165
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, result.error?.message === 'Patch had no effect'
|
|
156
166
|
? 'Patch had no effect \u2014 the file content is unchanged after applying. The patch may not match the current file content. Generate a fresh patch via diff_files and retry.'
|
|
157
167
|
: 'Patch application failed. The file content may have changed or patch context is insufficient. Generate a fresh patch via diff_files against the current file, then retry. If differences are minor, enable fuzzy matching with the fuzzFactor parameter.');
|
|
158
168
|
}
|
|
@@ -172,6 +182,7 @@ export function registerApplyPatchTool(server, options = {}) {
|
|
|
172
182
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
173
183
|
toolName: 'apply_patch',
|
|
174
184
|
extra,
|
|
185
|
+
outputSchema: ApplyPatchOutputSchema,
|
|
175
186
|
timedSignal: {},
|
|
176
187
|
context: { path: args.path },
|
|
177
188
|
run: (signal) => handleApplyPatch(args, signal),
|
|
@@ -157,6 +157,7 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
157
157
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
158
158
|
toolName: 'calculate_hash',
|
|
159
159
|
extra,
|
|
160
|
+
outputSchema: CalculateHashOutputSchema,
|
|
160
161
|
timedSignal: {},
|
|
161
162
|
context: { path: args.path },
|
|
162
163
|
run: async (signal) => {
|
|
@@ -172,13 +173,10 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
172
173
|
try {
|
|
173
174
|
const result = await handleCalculateHash(args, signal, progressWithMessage);
|
|
174
175
|
const sc = result.structuredContent;
|
|
175
|
-
const totalFiles = sc.
|
|
176
|
+
const totalFiles = sc.fileCount ?? 1;
|
|
176
177
|
const finalCurrent = resolveFinalProgressCurrent(progress, totalFiles + 1);
|
|
177
178
|
let suffix;
|
|
178
|
-
if (
|
|
179
|
-
suffix = 'failed';
|
|
180
|
-
}
|
|
181
|
-
else if (sc.fileCount !== undefined && sc.fileCount > 1) {
|
|
179
|
+
if (sc.fileCount !== undefined && sc.fileCount > 1) {
|
|
182
180
|
suffix = `${sc.fileCount} files • ${(sc.hash ?? '').slice(0, 8)}…`;
|
|
183
181
|
}
|
|
184
182
|
else {
|
|
@@ -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) {
|
|
@@ -36,6 +35,7 @@ export function registerCreateDirectoryTool(server, options = {}) {
|
|
|
36
35
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
37
36
|
toolName: 'mkdir',
|
|
38
37
|
extra,
|
|
38
|
+
outputSchema: CreateDirectoryOutputSchema,
|
|
39
39
|
timedSignal: {},
|
|
40
40
|
context: { path: args.path ?? args.paths?.[0] },
|
|
41
41
|
run: (signal) => handleCreateDirectory(args, signal),
|
|
@@ -13,10 +13,7 @@ export const DELETE_FILE_TOOL = {
|
|
|
13
13
|
inputSchema: DeleteFileInputSchema,
|
|
14
14
|
outputSchema: DeleteFileOutputSchema,
|
|
15
15
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
16
|
-
gotchas: [
|
|
17
|
-
'No undo — deletion is permanent.',
|
|
18
|
-
'Non-empty directories require `recursive=true`.',
|
|
19
|
-
],
|
|
16
|
+
gotchas: ['Non-empty directories require `recursive=true`.'],
|
|
20
17
|
taskSupport: 'forbidden',
|
|
21
18
|
};
|
|
22
19
|
async function handleDeleteFile(args, signal) {
|
|
@@ -59,6 +56,7 @@ export function registerDeleteFileTool(server, options = {}) {
|
|
|
59
56
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
60
57
|
toolName: 'rm',
|
|
61
58
|
extra,
|
|
59
|
+
outputSchema: DeleteFileOutputSchema,
|
|
62
60
|
timedSignal: {},
|
|
63
61
|
context: { path: args.path },
|
|
64
62
|
run: (signal) => handleDeleteFile(args, signal),
|
package/dist/tools/diff-files.js
CHANGED
|
@@ -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) {
|
|
@@ -105,6 +104,7 @@ 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),
|
|
@@ -123,8 +123,6 @@ export function registerDiffFilesTool(server, options = {}) {
|
|
|
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
|
@@ -19,7 +19,6 @@ export const EDIT_FILE_TOOL = {
|
|
|
19
19
|
outputSchema: EditFileOutputSchema,
|
|
20
20
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
21
21
|
nuances: ['Each edit applies to the output of the previous edit.'],
|
|
22
|
-
gotchas: ['Unmatched `oldText` entries listed in `unmatchedEdits`.'],
|
|
23
22
|
taskSupport: 'forbidden',
|
|
24
23
|
};
|
|
25
24
|
function escapeRegExp(string) {
|
|
@@ -104,7 +103,7 @@ function mergeLineRange(currentRange, content, matchStartIndex, newText) {
|
|
|
104
103
|
}
|
|
105
104
|
function buildStructuredEditOutput(validPath, result) {
|
|
106
105
|
return {
|
|
107
|
-
ok:
|
|
106
|
+
ok: result.appliedEdits > 0 || result.unmatchedEdits.length === 0,
|
|
108
107
|
path: validPath,
|
|
109
108
|
appliedEdits: result.appliedEdits,
|
|
110
109
|
...(result.appliedEdits > 0
|
|
@@ -215,6 +214,9 @@ async function handleEditFile(args, signal) {
|
|
|
215
214
|
}
|
|
216
215
|
return buildToolResponse(`Dry run complete. ${editResult.appliedEdits} edits would be applied.`, structured);
|
|
217
216
|
}
|
|
217
|
+
if (editResult.appliedEdits === 0 && editResult.unmatchedEdits.length > 0) {
|
|
218
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, `All ${editResult.unmatchedEdits.length} edits failed to match. Verify oldText includes exact content with surrounding context.`, args.path);
|
|
219
|
+
}
|
|
218
220
|
if (editResult.appliedEdits > 0) {
|
|
219
221
|
await atomicWriteFile(validPath, editResult.content, {
|
|
220
222
|
encoding: 'utf-8',
|
|
@@ -227,6 +229,7 @@ export function registerEditFileTool(server, options = {}) {
|
|
|
227
229
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
228
230
|
toolName: 'edit',
|
|
229
231
|
extra,
|
|
232
|
+
outputSchema: EditFileOutputSchema,
|
|
230
233
|
timedSignal: {},
|
|
231
234
|
context: { path: args.path },
|
|
232
235
|
run: (signal) => handleEditFile(args, signal),
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
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,22 +62,11 @@ 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
|
|
@@ -201,6 +197,7 @@ 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
203
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, args.path ?? '.'),
|
|
@@ -213,8 +210,6 @@ export function registerListDirectoryTool(server, options = {}) {
|
|
|
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
|
},
|
package/dist/tools/move-file.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ErrorCode, formatUnknownErrorMessage, isNodeError, McpError, } from '..
|
|
|
4
4
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
5
5
|
import { assertAllowedFileAccess, validateExistingPath, validatePathForWrite, } from '../lib/paths.js';
|
|
6
6
|
import { MoveFileInputSchema, MoveFileOutputSchema } from '../schemas.js';
|
|
7
|
-
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { buildStructuredError, buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
export const MOVE_FILE_TOOL = {
|
|
10
10
|
name: 'mv',
|
|
@@ -13,12 +13,17 @@ export const MOVE_FILE_TOOL = {
|
|
|
13
13
|
inputSchema: MoveFileInputSchema,
|
|
14
14
|
outputSchema: MoveFileOutputSchema,
|
|
15
15
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
16
|
-
nuances: ['Cross-device moves fall back to copy+delete.'],
|
|
17
16
|
gotchas: [
|
|
18
17
|
'On POSIX, an existing destination is silently overwritten; on Windows, rename fails with EEXIST if destination exists.',
|
|
19
18
|
],
|
|
20
19
|
taskSupport: 'forbidden',
|
|
21
20
|
};
|
|
21
|
+
function toMoveFailure(source, error, defaultCode = ErrorCode.E_UNKNOWN) {
|
|
22
|
+
return {
|
|
23
|
+
source,
|
|
24
|
+
error: buildStructuredError(error, defaultCode, source),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
22
27
|
async function handleMoveFile(args, signal) {
|
|
23
28
|
const sources = args.sources ?? (args.source ? [args.source] : []);
|
|
24
29
|
if (sources.length === 0) {
|
|
@@ -52,10 +57,7 @@ async function handleMoveFile(args, signal) {
|
|
|
52
57
|
assertAllowedFileAccess(src, validSource);
|
|
53
58
|
}
|
|
54
59
|
catch (error) {
|
|
55
|
-
failed.push(
|
|
56
|
-
source: src,
|
|
57
|
-
error: formatUnknownErrorMessage(error),
|
|
58
|
-
});
|
|
60
|
+
failed.push(toMoveFailure(src, error, ErrorCode.E_ACCESS_DENIED));
|
|
59
61
|
continue;
|
|
60
62
|
}
|
|
61
63
|
const targetPath = destIsDirectory
|
|
@@ -68,10 +70,7 @@ async function handleMoveFile(args, signal) {
|
|
|
68
70
|
// Prevent moving a directory into its own subdirectory
|
|
69
71
|
// Fixes "Missing validation for moving directory into its own subdirectory" finding
|
|
70
72
|
if (path.resolve(targetPath).startsWith(path.resolve(validSource) + path.sep)) {
|
|
71
|
-
failed.push({
|
|
72
|
-
source: src,
|
|
73
|
-
error: `Cannot move directory '${src}' into its own subdirectory '${targetPath}'`,
|
|
74
|
-
});
|
|
73
|
+
failed.push(toMoveFailure(src, new McpError(ErrorCode.E_INVALID_INPUT, `Cannot move directory '${src}' into its own subdirectory '${targetPath}'`, src), ErrorCode.E_INVALID_INPUT));
|
|
75
74
|
continue;
|
|
76
75
|
}
|
|
77
76
|
try {
|
|
@@ -85,10 +84,7 @@ async function handleMoveFile(args, signal) {
|
|
|
85
84
|
await withAbort(fs.cp(validSource, targetPath, { recursive: true }), signal);
|
|
86
85
|
}
|
|
87
86
|
catch (copyError) {
|
|
88
|
-
failed.push(
|
|
89
|
-
source: src,
|
|
90
|
-
error: formatUnknownErrorMessage(copyError),
|
|
91
|
-
});
|
|
87
|
+
failed.push(toMoveFailure(src, copyError));
|
|
92
88
|
continue;
|
|
93
89
|
}
|
|
94
90
|
// Copy succeeded — now remove source
|
|
@@ -103,23 +99,14 @@ async function handleMoveFile(args, signal) {
|
|
|
103
99
|
}
|
|
104
100
|
catch {
|
|
105
101
|
// Rollback failed — data exists in both locations
|
|
106
|
-
failed.push({
|
|
107
|
-
source: src,
|
|
108
|
-
error: `Cross-device move partially failed: data exists at both '${validSource}' and '${targetPath}'. ${formatUnknownErrorMessage(deleteError)}`,
|
|
109
|
-
});
|
|
102
|
+
failed.push(toMoveFailure(src, new McpError(ErrorCode.E_UNKNOWN, `Cross-device move partially failed: data exists at both '${validSource}' and '${targetPath}'. ${formatUnknownErrorMessage(deleteError)}`, src)));
|
|
110
103
|
continue;
|
|
111
104
|
}
|
|
112
|
-
failed.push({
|
|
113
|
-
source: src,
|
|
114
|
-
error: `Cross-device move failed: could not remove source. ${formatUnknownErrorMessage(deleteError)}`,
|
|
115
|
-
});
|
|
105
|
+
failed.push(toMoveFailure(src, new McpError(ErrorCode.E_UNKNOWN, `Cross-device move failed: could not remove source. ${formatUnknownErrorMessage(deleteError)}`, src)));
|
|
116
106
|
}
|
|
117
107
|
}
|
|
118
108
|
else {
|
|
119
|
-
failed.push(
|
|
120
|
-
source: src,
|
|
121
|
-
error: formatUnknownErrorMessage(error),
|
|
122
|
-
});
|
|
109
|
+
failed.push(toMoveFailure(src, error));
|
|
123
110
|
}
|
|
124
111
|
}
|
|
125
112
|
}
|
|
@@ -137,6 +124,7 @@ export function registerMoveFileTool(server, options = {}) {
|
|
|
137
124
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
138
125
|
toolName: 'mv',
|
|
139
126
|
extra,
|
|
127
|
+
outputSchema: MoveFileOutputSchema,
|
|
140
128
|
timedSignal: {},
|
|
141
129
|
context: { path: args.source ?? args.sources?.[0] },
|
|
142
130
|
run: (signal) => handleMoveFile(args, signal),
|
|
@@ -3,7 +3,7 @@ import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { readMultipleFiles } from '../lib/file-operations/metadata.js';
|
|
5
5
|
import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
|
|
6
|
-
import { buildBatchCompletionSuffix, buildBatchPathContext, buildResourceLink, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
6
|
+
import { buildBatchCompletionSuffix, buildBatchPathContext, buildResourceLink, buildStructuredError, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
const READ_MANY_TOOL_NAME = 'read_many';
|
|
9
9
|
const READ_MANY_TOOL_LABEL = '🕮 read_many';
|
|
@@ -17,10 +17,6 @@ export const READ_MANY_TOOL = {
|
|
|
17
17
|
outputSchema: ReadMultipleFilesOutputSchema,
|
|
18
18
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
19
19
|
taskSupport: 'optional',
|
|
20
|
-
nuances: ['Total read budget is capped by `MAX_READ_MANY_TOTAL_SIZE`.'],
|
|
21
|
-
gotchas: [
|
|
22
|
-
'Per-file `truncationReason` can be `head`, `tail`, `range`, or `externalized`.',
|
|
23
|
-
],
|
|
24
20
|
};
|
|
25
21
|
function buildReadManyResourceName(filePath) {
|
|
26
22
|
return `read:${path.basename(filePath)}`;
|
|
@@ -72,8 +68,12 @@ function resolveReadManyTruncationReason(result) {
|
|
|
72
68
|
}
|
|
73
69
|
function maybeExternalizeReadManyResult(result, resourceStore) {
|
|
74
70
|
const truncationReason = resolveReadManyTruncationReason(result);
|
|
71
|
+
const { error, ...rest } = result;
|
|
75
72
|
const baseResult = {
|
|
76
|
-
...
|
|
73
|
+
...rest,
|
|
74
|
+
...(error
|
|
75
|
+
? { error: buildStructuredError(error, ErrorCode.E_UNKNOWN, result.path) }
|
|
76
|
+
: {}),
|
|
77
77
|
...(truncationReason ? { truncationReason } : {}),
|
|
78
78
|
};
|
|
79
79
|
if (!result.content) {
|
|
@@ -89,12 +89,13 @@ function maybeExternalizeReadManyResult(result, resourceStore) {
|
|
|
89
89
|
truncated: true,
|
|
90
90
|
resourceUri: externalized.entry.uri,
|
|
91
91
|
truncationReason: 'externalized',
|
|
92
|
+
expiresAt: externalized.entry.expiresAt,
|
|
92
93
|
};
|
|
93
94
|
}
|
|
94
95
|
function buildReadManyTextSection(result) {
|
|
95
96
|
const header = `=== ${result.path} ===`;
|
|
96
97
|
if (result.error) {
|
|
97
|
-
return `${header}\nError: ${result.error}`;
|
|
98
|
+
return `${header}\nError [${result.error.code}]: ${result.error.message}`;
|
|
98
99
|
}
|
|
99
100
|
return `${header}\n${result.content ?? ''}`;
|
|
100
101
|
}
|
|
@@ -128,6 +129,9 @@ function buildReadManyResponsePayload(results, resourceStore) {
|
|
|
128
129
|
uri: mappedResult.resourceUri,
|
|
129
130
|
name: buildReadManyResourceName(mappedResult.path),
|
|
130
131
|
description: FULL_FILE_CONTENTS_DESCRIPTION,
|
|
132
|
+
...(mappedResult.expiresAt
|
|
133
|
+
? { expiresAt: mappedResult.expiresAt }
|
|
134
|
+
: {}),
|
|
131
135
|
}));
|
|
132
136
|
}
|
|
133
137
|
if (mappedResult.error === undefined)
|
|
@@ -162,6 +166,7 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
162
166
|
return executeToolWithDiagnostics({
|
|
163
167
|
toolName: READ_MANY_TOOL_NAME,
|
|
164
168
|
extra,
|
|
169
|
+
outputSchema: ReadMultipleFilesOutputSchema,
|
|
165
170
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
166
171
|
context: { path: primaryPath },
|
|
167
172
|
run: async (signal) => {
|
package/dist/tools/read.js
CHANGED
|
@@ -115,8 +115,6 @@ function buildReadCompletionMessage(args, result) {
|
|
|
115
115
|
if (result.isError)
|
|
116
116
|
return `${READ_TOOL_LABEL}: ${name} • failed`;
|
|
117
117
|
const structured = result.structuredContent;
|
|
118
|
-
if (!structured.ok)
|
|
119
|
-
return `${READ_TOOL_LABEL}: ${name} • failed`;
|
|
120
118
|
const lines = structured.linesRead ?? structured.totalLines;
|
|
121
119
|
if (structured.startLine !== undefined) {
|
|
122
120
|
const end = structured.linesRead
|
|
@@ -158,6 +156,7 @@ export function registerReadFileTool(server, options = {}) {
|
|
|
158
156
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
159
157
|
toolName: READ_TOOL_NAME,
|
|
160
158
|
extra,
|
|
159
|
+
outputSchema: ReadFileOutputSchema,
|
|
161
160
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
162
161
|
context: { path: args.path },
|
|
163
162
|
run: (signal) => handleReadFile(args, signal, options.resourceStore),
|