@j0hanz/filesystem-mcp 1.17.0 → 1.18.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/dist/completions.js +83 -16
- package/dist/lib/constants.d.ts +4 -2
- package/dist/lib/constants.js +6 -2
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.js +1 -1
- package/dist/lib/fs-helpers.d.ts +2 -1
- package/dist/lib/fs-helpers.js +39 -22
- package/dist/prompts.js +20 -0
- package/dist/server/bootstrap.js +24 -40
- package/dist/server/event-store.d.ts +18 -0
- package/dist/server/event-store.js +71 -0
- package/dist/server/roots-manager.js +5 -4
- package/dist/server/task-store.d.ts +1 -0
- package/dist/server/task-store.js +23 -5
- package/dist/tools/apply-patch.js +15 -8
- package/dist/tools/calculate-hash.js +5 -5
- package/dist/tools/create-directory.js +5 -6
- package/dist/tools/delete-file.js +3 -3
- package/dist/tools/diff-files.js +5 -5
- package/dist/tools/edit-file.js +13 -8
- package/dist/tools/list-directory.js +3 -3
- package/dist/tools/move-file.js +5 -6
- package/dist/tools/read-multiple.js +14 -7
- package/dist/tools/read.js +4 -7
- package/dist/tools/replace-in-files.js +5 -5
- package/dist/tools/roots.js +3 -3
- package/dist/tools/search-content.js +5 -4
- package/dist/tools/search-files.js +5 -5
- package/dist/tools/shared.d.ts +15 -18
- package/dist/tools/shared.js +5 -17
- package/dist/tools/stat-many.js +7 -6
- package/dist/tools/stat.js +4 -4
- package/dist/tools/task-support.d.ts +5 -0
- package/dist/tools/task-support.js +83 -113
- package/dist/tools/tree.js +5 -5
- package/dist/tools/write-file.js +3 -3
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InMemoryTaskStore, } from '@modelcontextprotocol/server';
|
|
2
|
+
import { CANCELLED_RESULT_TTL_MS } from '../lib/constants.js';
|
|
2
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
3
4
|
const DEFAULT_CANCELLED_STATUS_MESSAGE = 'Client cancelled task execution.';
|
|
4
5
|
function getTaskKey(taskId, sessionId) {
|
|
@@ -18,7 +19,16 @@ function buildCancelledTaskResult(statusMessage) {
|
|
|
18
19
|
}
|
|
19
20
|
export class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
|
|
20
21
|
cancelledResults = new Map();
|
|
22
|
+
evictExpired() {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
for (const [key, entry] of this.cancelledResults) {
|
|
25
|
+
if (now - entry.createdAt > CANCELLED_RESULT_TTL_MS) {
|
|
26
|
+
this.cancelledResults.delete(key);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
21
30
|
async getTaskResult(taskId, sessionId) {
|
|
31
|
+
this.evictExpired();
|
|
22
32
|
try {
|
|
23
33
|
return await super.getTaskResult(taskId, sessionId);
|
|
24
34
|
}
|
|
@@ -30,9 +40,9 @@ export class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
|
|
|
30
40
|
const key = getTaskKey(taskId, sessionId);
|
|
31
41
|
const existing = this.cancelledResults.get(key);
|
|
32
42
|
if (existing)
|
|
33
|
-
return existing;
|
|
43
|
+
return existing.result;
|
|
34
44
|
const result = buildCancelledTaskResult(task.statusMessage);
|
|
35
|
-
this.cancelledResults.set(key, result);
|
|
45
|
+
this.cancelledResults.set(key, { result, createdAt: Date.now() });
|
|
36
46
|
return result;
|
|
37
47
|
}
|
|
38
48
|
}
|
|
@@ -48,15 +58,23 @@ export class ResultAwareInMemoryTaskStore extends InMemoryTaskStore {
|
|
|
48
58
|
if (task?.status !== 'cancelled') {
|
|
49
59
|
throw error;
|
|
50
60
|
}
|
|
51
|
-
|
|
61
|
+
const key = getTaskKey(taskId, sessionId);
|
|
62
|
+
const existing = this.cancelledResults.get(key);
|
|
63
|
+
this.cancelledResults.set(key, {
|
|
64
|
+
result: existing?.result ?? result,
|
|
65
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
66
|
+
});
|
|
52
67
|
}
|
|
53
68
|
}
|
|
54
69
|
async updateTaskStatus(taskId, status, statusMessage, sessionId) {
|
|
55
70
|
await super.updateTaskStatus(taskId, status, statusMessage, sessionId);
|
|
56
71
|
const key = getTaskKey(taskId, sessionId);
|
|
57
72
|
if (status === 'cancelled') {
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
const existing = this.cancelledResults.get(key);
|
|
74
|
+
this.cancelledResults.set(key, {
|
|
75
|
+
result: existing?.result ?? buildCancelledTaskResult(statusMessage),
|
|
76
|
+
createdAt: existing?.createdAt ?? Date.now(),
|
|
77
|
+
});
|
|
60
78
|
return;
|
|
61
79
|
}
|
|
62
80
|
if (status === 'completed' || status === 'failed') {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
2
|
import { basename, resolve } from 'node:path';
|
|
3
3
|
import { applyPatch, parsePatch } from 'diff';
|
|
4
4
|
import { withAbort } from '../lib/abort.js';
|
|
5
5
|
import { MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY } from '../lib/constants.js';
|
|
6
6
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
7
|
-
import { atomicWriteFile, processInParallel } from '../lib/fs-helpers.js';
|
|
7
|
+
import { atomicWriteFile, processInParallel, readFileWithStats, } from '../lib/fs-helpers.js';
|
|
8
8
|
import { Logger } from '../lib/logger.js';
|
|
9
9
|
import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
|
|
10
10
|
import { ApplyPatchInputSchema, ApplyPatchOutputSchema } from '../schemas.js';
|
|
@@ -62,7 +62,12 @@ async function applyDiff(filePath, diff, options, signal) {
|
|
|
62
62
|
assertAllowedFileAccess(filePath, validPath);
|
|
63
63
|
const stats = await withAbort(stat(validPath), signal);
|
|
64
64
|
assertPatchTargetSizeWithinLimit(validPath, stats.size, MAX_TEXT_FILE_SIZE);
|
|
65
|
-
const content = await
|
|
65
|
+
const { content } = await readFileWithStats(filePath, validPath, stats, {
|
|
66
|
+
encoding: 'utf-8',
|
|
67
|
+
maxSize: MAX_TEXT_FILE_SIZE,
|
|
68
|
+
skipBinary: true,
|
|
69
|
+
...(signal ? { signal } : {}),
|
|
70
|
+
});
|
|
66
71
|
const patched = applyPatch(content, diff, {
|
|
67
72
|
fuzzFactor: options.fuzzFactor,
|
|
68
73
|
autoConvertLineEndings: options.autoConvertLineEndings,
|
|
@@ -208,21 +213,23 @@ export function registerApplyPatchTool(server, options = {}) {
|
|
|
208
213
|
registerStandardTool(server, APPLY_PATCH_TOOL, handler, options, {
|
|
209
214
|
progressMessage: (args) => {
|
|
210
215
|
const name = basename(args.path);
|
|
211
|
-
return args.dryRun
|
|
216
|
+
return args.dryRun
|
|
217
|
+
? `${APPLY_PATCH_TOOL.title}: ${name} [dry run]`
|
|
218
|
+
: `${APPLY_PATCH_TOOL.title}: ${name}`;
|
|
212
219
|
},
|
|
213
220
|
completionMessage: (args, result) => {
|
|
214
221
|
const name = basename(args.path);
|
|
215
222
|
if (result.isError)
|
|
216
|
-
return
|
|
223
|
+
return `${APPLY_PATCH_TOOL.title}: ${name} • ${result.errorCode}`;
|
|
217
224
|
const sc = result.structuredContent;
|
|
218
225
|
if (!sc.ok)
|
|
219
|
-
return
|
|
226
|
+
return `${APPLY_PATCH_TOOL.title}: ${name} • failed`;
|
|
220
227
|
const added = sc.linesAdded ?? 0;
|
|
221
228
|
const removed = sc.linesRemoved ?? 0;
|
|
222
229
|
const dry = args.dryRun ? 'dry run ' : '';
|
|
223
230
|
if (added > 0 || removed > 0)
|
|
224
|
-
return
|
|
225
|
-
return
|
|
231
|
+
return `${APPLY_PATCH_TOOL.title}: ${name} • ${dry} +${added} -${removed}`;
|
|
232
|
+
return `${APPLY_PATCH_TOOL.title}: ${name} • ${dry}no changes`;
|
|
226
233
|
},
|
|
227
234
|
});
|
|
228
235
|
}
|
|
@@ -5,7 +5,7 @@ import { basename, relative, win32 } from 'node:path';
|
|
|
5
5
|
import { pipeline } from 'node:stream/promises';
|
|
6
6
|
import { assertNotAborted, withAbort } from '../lib/abort.js';
|
|
7
7
|
import { PARALLEL_CONCURRENCY } from '../lib/constants.js';
|
|
8
|
-
import { ErrorCode } from '../lib/errors.js';
|
|
8
|
+
import { classifyError, ErrorCode } from '../lib/errors.js';
|
|
9
9
|
import { isIgnoredByGitignore, loadRootGitignore, } from '../lib/file-operations/core.js';
|
|
10
10
|
import { globEntries } from '../lib/file-operations/traversal.js';
|
|
11
11
|
import { validateExistingPath } from '../lib/paths.js';
|
|
@@ -153,12 +153,12 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
153
153
|
context: { path: args.path },
|
|
154
154
|
run: async (signal) => {
|
|
155
155
|
const baseName = basename(args.path);
|
|
156
|
-
const progress = createToolProgressSession(ctx,
|
|
156
|
+
const progress = createToolProgressSession(ctx, `${CALCULATE_HASH_TOOL.title}: ${baseName}`);
|
|
157
157
|
const progressWithMessage = ({ current, total, }) => {
|
|
158
158
|
progress.update({
|
|
159
159
|
current,
|
|
160
160
|
...(total !== undefined ? { total } : {}),
|
|
161
|
-
message:
|
|
161
|
+
message: `${CALCULATE_HASH_TOOL.title}: ${baseName} [${current} files]`,
|
|
162
162
|
});
|
|
163
163
|
};
|
|
164
164
|
try {
|
|
@@ -173,11 +173,11 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
173
173
|
else {
|
|
174
174
|
suffix = `${(sc.hash ?? '').slice(0, 8)}…`;
|
|
175
175
|
}
|
|
176
|
-
progress.complete(
|
|
176
|
+
progress.complete(`${CALCULATE_HASH_TOOL.title}: ${baseName} • ${suffix}`, finalCurrent);
|
|
177
177
|
return result;
|
|
178
178
|
}
|
|
179
179
|
catch (error) {
|
|
180
|
-
progress.fail(
|
|
180
|
+
progress.fail(`${CALCULATE_HASH_TOOL.title}: ${baseName} • ${classifyError(error)}`);
|
|
181
181
|
throw error;
|
|
182
182
|
}
|
|
183
183
|
},
|
|
@@ -48,22 +48,21 @@ export function registerCreateDirectoryTool(server, options = {}) {
|
|
|
48
48
|
registerStandardTool(server, CREATE_DIRECTORY_TOOL, handler, options, {
|
|
49
49
|
progressMessage: (args) => {
|
|
50
50
|
if (args.path && !args.paths?.length) {
|
|
51
|
-
return
|
|
51
|
+
return `${CREATE_DIRECTORY_TOOL.title}: ${basename(args.path)}`;
|
|
52
52
|
}
|
|
53
53
|
const count = (args.path ? 1 : 0) + (args.paths?.length ?? 0);
|
|
54
|
-
return
|
|
54
|
+
return `${CREATE_DIRECTORY_TOOL.title}: ${count} directories`;
|
|
55
55
|
},
|
|
56
56
|
completionMessage: (args, result) => {
|
|
57
57
|
if (args.path && !args.paths?.length) {
|
|
58
58
|
const name = basename(args.path);
|
|
59
59
|
if (result.isError)
|
|
60
|
-
return
|
|
61
|
-
return `🛠 mkdir: ${name}`;
|
|
60
|
+
return `${CREATE_DIRECTORY_TOOL.title}: ${name} • ${result.errorCode}`;
|
|
62
61
|
}
|
|
63
62
|
const count = (args.path ? 1 : 0) + (args.paths?.length ?? 0);
|
|
64
63
|
if (result.isError)
|
|
65
|
-
return
|
|
66
|
-
return
|
|
64
|
+
return `${CREATE_DIRECTORY_TOOL.title}: ${count} directories • ${result.errorCode}`;
|
|
65
|
+
return `${CREATE_DIRECTORY_TOOL.title}: ${count} directories`;
|
|
67
66
|
},
|
|
68
67
|
});
|
|
69
68
|
}
|
|
@@ -90,12 +90,12 @@ export function registerDeleteFileTool(server, options = {}) {
|
|
|
90
90
|
},
|
|
91
91
|
});
|
|
92
92
|
registerStandardTool(server, DELETE_FILE_TOOL, handler, options, {
|
|
93
|
-
progressMessage: (args) =>
|
|
93
|
+
progressMessage: (args) => `${DELETE_FILE_TOOL.title}: ${basename(args.path)}`,
|
|
94
94
|
completionMessage: (args, result) => {
|
|
95
95
|
const name = basename(args.path);
|
|
96
96
|
if (result.isError)
|
|
97
|
-
return
|
|
98
|
-
return
|
|
97
|
+
return `${DELETE_FILE_TOOL.title}: ${name} • ${result.errorCode}`;
|
|
98
|
+
return `${DELETE_FILE_TOOL.title}: ${name}`;
|
|
99
99
|
},
|
|
100
100
|
});
|
|
101
101
|
}
|
package/dist/tools/diff-files.js
CHANGED
|
@@ -116,21 +116,21 @@ export function registerDiffFilesTool(server, options = {}) {
|
|
|
116
116
|
progressMessage: (args) => {
|
|
117
117
|
const n1 = basename(args.original);
|
|
118
118
|
const n2 = basename(args.modified);
|
|
119
|
-
return
|
|
119
|
+
return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2}`;
|
|
120
120
|
},
|
|
121
121
|
completionMessage: (args, result) => {
|
|
122
122
|
const n1 = basename(args.original);
|
|
123
123
|
const n2 = basename(args.modified);
|
|
124
124
|
if (result.isError)
|
|
125
|
-
return
|
|
125
|
+
return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2} • ${result.errorCode}`;
|
|
126
126
|
const sc = result.structuredContent;
|
|
127
127
|
if (sc.isIdentical)
|
|
128
|
-
return
|
|
128
|
+
return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2} • identical`;
|
|
129
129
|
const added = sc.linesAdded ?? 0;
|
|
130
130
|
const removed = sc.linesRemoved ?? 0;
|
|
131
131
|
if (added > 0 || removed > 0)
|
|
132
|
-
return
|
|
133
|
-
return
|
|
132
|
+
return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2} • +${added} -${removed}`;
|
|
133
|
+
return `${DIFF_FILES_TOOL.title}: ${n1} ⟷ ${n2}`;
|
|
134
134
|
},
|
|
135
135
|
});
|
|
136
136
|
}
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { 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
5
|
import { withAbort } from '../lib/abort.js';
|
|
6
6
|
import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
|
|
7
7
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
8
|
-
import { atomicWriteFile } from '../lib/fs-helpers.js';
|
|
8
|
+
import { atomicWriteFile, readFileWithStats } from '../lib/fs-helpers.js';
|
|
9
9
|
import { Logger } from '../lib/logger.js';
|
|
10
10
|
import { assertAllowedFileAccess, validateExistingPath } from '../lib/paths.js';
|
|
11
11
|
import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
|
|
@@ -167,28 +167,33 @@ async function loadEditableFile(requestedPath, signal) {
|
|
|
167
167
|
if (stats.size > MAX_TEXT_FILE_SIZE) {
|
|
168
168
|
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 });
|
|
169
169
|
}
|
|
170
|
-
const content = await
|
|
170
|
+
const { content } = await readFileWithStats(requestedPath, validPath, stats, {
|
|
171
|
+
encoding: 'utf-8',
|
|
172
|
+
maxSize: MAX_TEXT_FILE_SIZE,
|
|
173
|
+
skipBinary: true,
|
|
174
|
+
...(signal ? { signal } : {}),
|
|
175
|
+
});
|
|
171
176
|
return { validPath, content };
|
|
172
177
|
}
|
|
173
178
|
function buildEditProgressMessage(args) {
|
|
174
179
|
const name = basename(args.path);
|
|
175
180
|
const tag = args.dryRun ? ' [dry run]' : '';
|
|
176
|
-
return
|
|
181
|
+
return `${EDIT_FILE_TOOL.title}: ${name}${tag}`;
|
|
177
182
|
}
|
|
178
183
|
function buildEditCompletionMessage(args, result) {
|
|
179
184
|
const name = basename(args.path);
|
|
180
185
|
if (result.isError)
|
|
181
|
-
return
|
|
186
|
+
return `${EDIT_FILE_TOOL.title}: ${name} • ${result.errorCode}`;
|
|
182
187
|
const { structuredContent } = result;
|
|
183
188
|
if (!structuredContent.ok)
|
|
184
|
-
return
|
|
189
|
+
return `${EDIT_FILE_TOOL.title}: ${name} • failed`;
|
|
185
190
|
const applied = structuredContent.appliedEdits ?? 0;
|
|
186
191
|
if (applied === 0)
|
|
187
|
-
return
|
|
192
|
+
return `${EDIT_FILE_TOOL.title}: ${name} • no changes`;
|
|
188
193
|
const added = structuredContent.linesAdded ?? 0;
|
|
189
194
|
const removed = structuredContent.linesRemoved ?? 0;
|
|
190
195
|
const dry = args.dryRun ? 'dry run ' : '';
|
|
191
|
-
return
|
|
196
|
+
return `${EDIT_FILE_TOOL.title}: ${name} • ${dry} +${added} -${removed}`;
|
|
192
197
|
}
|
|
193
198
|
async function applyEdits(content, edits, ignoreWhitespace) {
|
|
194
199
|
let newContent = content;
|
|
@@ -205,14 +205,14 @@ export function registerListDirectoryTool(server, options = {}) {
|
|
|
205
205
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.NOT_DIRECTORY, args.path ?? '.'),
|
|
206
206
|
});
|
|
207
207
|
registerStandardTool(server, LIST_DIRECTORY_TOOL, handler, options, {
|
|
208
|
-
progressMessage: (args) =>
|
|
208
|
+
progressMessage: (args) => `${LIST_DIRECTORY_TOOL.title}: ${args.path ? basename(args.path) : '.'}`,
|
|
209
209
|
completionMessage: (args, result) => {
|
|
210
210
|
const base = args.path ? basename(args.path) : '.';
|
|
211
211
|
if (result.isError)
|
|
212
|
-
return
|
|
212
|
+
return `${LIST_DIRECTORY_TOOL.title}: ${base} • ${result.errorCode}`;
|
|
213
213
|
const sc = result.structuredContent;
|
|
214
214
|
const count = sc.totalEntries ?? 0;
|
|
215
|
-
return
|
|
215
|
+
return `${LIST_DIRECTORY_TOOL.title}: ${base} • ${count} ${count === 1 ? 'entry' : 'entries'}`;
|
|
216
216
|
},
|
|
217
217
|
});
|
|
218
218
|
}
|
package/dist/tools/move-file.js
CHANGED
|
@@ -157,23 +157,22 @@ export function registerMoveFileTool(server, options = {}) {
|
|
|
157
157
|
progressMessage: (args) => {
|
|
158
158
|
const dest = basename(args.destination);
|
|
159
159
|
if (args.source && !args.sources?.length) {
|
|
160
|
-
return
|
|
160
|
+
return `${MOVE_FILE_TOOL.title}: ${basename(args.source)} → ${dest}`;
|
|
161
161
|
}
|
|
162
162
|
const count = (args.source ? 1 : 0) + (args.sources?.length ?? 0);
|
|
163
|
-
return
|
|
163
|
+
return `${MOVE_FILE_TOOL.title}: ${count} items → ${dest}`;
|
|
164
164
|
},
|
|
165
165
|
completionMessage: (args, result) => {
|
|
166
166
|
const dest = basename(args.destination);
|
|
167
167
|
if (args.source && !args.sources?.length) {
|
|
168
168
|
const src = basename(args.source);
|
|
169
169
|
if (result.isError)
|
|
170
|
-
return
|
|
171
|
-
return `🛠 mv: ${src} → ${dest}`;
|
|
170
|
+
return `${MOVE_FILE_TOOL.title}: ${src} → ${dest} • ${result.errorCode}`;
|
|
172
171
|
}
|
|
173
172
|
const count = (args.source ? 1 : 0) + (args.sources?.length ?? 0);
|
|
174
173
|
if (result.isError)
|
|
175
|
-
return
|
|
176
|
-
return
|
|
174
|
+
return `${MOVE_FILE_TOOL.title}: ${count} items → ${dest} • ${result.errorCode}`;
|
|
175
|
+
return `${MOVE_FILE_TOOL.title}: ${count} items → ${dest}`;
|
|
177
176
|
},
|
|
178
177
|
});
|
|
179
178
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
3
|
-
import { ErrorCode } from '../lib/errors.js';
|
|
3
|
+
import { classifyError, ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { readMultipleFiles } from '../lib/file-operations/metadata.js';
|
|
5
5
|
import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
|
|
6
6
|
import { FILE_READ_ICONS } from './icons.js';
|
|
7
|
-
import {
|
|
8
|
-
import { registerStandardTool } from './task-support.js';
|
|
7
|
+
import { buildBatchPathContext, buildResourceLink, buildStructuredError, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, } from './shared.js';
|
|
8
|
+
import { registerStandardTool, reportTaskStatus } from './task-support.js';
|
|
9
9
|
const READ_MANY_TOOL_NAME = 'read_many';
|
|
10
|
-
const READ_MANY_TOOL_LABEL = '🕮 read_many';
|
|
11
10
|
const FULL_FILE_CONTENTS_DESCRIPTION = 'Full file contents';
|
|
12
11
|
export const READ_MANY_TOOL = {
|
|
13
12
|
name: READ_MANY_TOOL_NAME,
|
|
@@ -20,6 +19,7 @@ export const READ_MANY_TOOL = {
|
|
|
20
19
|
icons: FILE_READ_ICONS,
|
|
21
20
|
taskSupport: 'optional',
|
|
22
21
|
};
|
|
22
|
+
const READ_MANY_TOOL_LABEL = READ_MANY_TOOL.title;
|
|
23
23
|
function buildReadManyResourceName(filePath) {
|
|
24
24
|
return `read:${basename(filePath)}`;
|
|
25
25
|
}
|
|
@@ -173,23 +173,30 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
173
173
|
context: { path: primaryPath },
|
|
174
174
|
run: async (signal) => {
|
|
175
175
|
const context = buildBatchPathContext(args.paths, 'files');
|
|
176
|
-
const { progress, onItemComplete } = createBatchProgressCallbacks(ctx, {
|
|
176
|
+
const { progress, onItemComplete: rawOnItemComplete } = createBatchProgressCallbacks(ctx, {
|
|
177
177
|
toolLabel: READ_MANY_TOOL_LABEL,
|
|
178
178
|
context,
|
|
179
179
|
totalItems: args.paths.length,
|
|
180
180
|
itemVerb: 'read',
|
|
181
181
|
});
|
|
182
|
+
let itemsDone = 0;
|
|
183
|
+
const onItemComplete = () => {
|
|
184
|
+
rawOnItemComplete();
|
|
185
|
+
itemsDone++;
|
|
186
|
+
void reportTaskStatus(`${READ_MANY_TOOL_LABEL}: ${context} [${itemsDone}/${args.paths.length} read]`);
|
|
187
|
+
};
|
|
182
188
|
try {
|
|
183
189
|
const result = await handleReadMultipleFiles(args, signal, options.resourceStore, onItemComplete);
|
|
184
190
|
const sc = result.structuredContent;
|
|
185
|
-
const suffix = buildBatchCompletionSuffix(sc.summary, 'files read', 'file read');
|
|
186
191
|
const total = sc.summary?.total ?? 0;
|
|
192
|
+
const failed = sc.summary?.failed ?? 0;
|
|
193
|
+
const suffix = failed ? `${failed} failed` : 'done';
|
|
187
194
|
const finalCurrent = resolveFinalProgressCurrent(progress, total);
|
|
188
195
|
progress.complete(`${READ_MANY_TOOL_LABEL}: ${context} • ${suffix}`, finalCurrent);
|
|
189
196
|
return result;
|
|
190
197
|
}
|
|
191
198
|
catch (error) {
|
|
192
|
-
progress.fail(`${READ_MANY_TOOL_LABEL}: ${context} •
|
|
199
|
+
progress.fail(`${READ_MANY_TOOL_LABEL}: ${context} • ${classifyError(error)}`);
|
|
193
200
|
throw error;
|
|
194
201
|
}
|
|
195
202
|
},
|
package/dist/tools/read.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { basename } from 'node:path';
|
|
3
2
|
import { DEFAULT_SEARCH_TIMEOUT_MS, MAX_TEXT_FILE_SIZE, } from '../lib/constants.js';
|
|
4
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
|
-
import { readFile } from '../lib/fs-helpers.js';
|
|
4
|
+
import { calculateFileContentHash, readFile } from '../lib/fs-helpers.js';
|
|
6
5
|
import { ReadFileInputSchema, ReadFileOutputSchema } from '../schemas.js';
|
|
7
6
|
import { FILE_READ_ICONS } from './icons.js';
|
|
8
7
|
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, } from './shared.js';
|
|
@@ -23,7 +22,7 @@ export const READ_FILE_TOOL = {
|
|
|
23
22
|
taskSupport: 'forbidden',
|
|
24
23
|
};
|
|
25
24
|
const READ_TOOL_NAME = 'read';
|
|
26
|
-
const READ_TOOL_LABEL =
|
|
25
|
+
const READ_TOOL_LABEL = READ_FILE_TOOL.title;
|
|
27
26
|
const FULL_FILE_CONTENTS_DESCRIPTION = 'Full file contents';
|
|
28
27
|
function buildReadResourceName(filePath) {
|
|
29
28
|
return `read:${basename(filePath)}`;
|
|
@@ -115,7 +114,7 @@ function buildReadProgressMessage(args) {
|
|
|
115
114
|
function buildReadCompletionMessage(args, result) {
|
|
116
115
|
const name = basename(args.path);
|
|
117
116
|
if (result.isError)
|
|
118
|
-
return `${READ_TOOL_LABEL}: ${name} •
|
|
117
|
+
return `${READ_TOOL_LABEL}: ${name} • ${result.errorCode}`;
|
|
119
118
|
const structured = result.structuredContent;
|
|
120
119
|
const lines = structured.linesRead ?? structured.totalLines;
|
|
121
120
|
if (structured.startLine !== undefined) {
|
|
@@ -144,9 +143,7 @@ async function handleReadFile(args, signal, resourceStore) {
|
|
|
144
143
|
const result = await readFile(args.path, options);
|
|
145
144
|
const structured = toStructuredReadFileResult(args, result);
|
|
146
145
|
if (args.includeHash) {
|
|
147
|
-
structured.contentHash =
|
|
148
|
-
.update(result.content, 'utf-8')
|
|
149
|
-
.digest('hex');
|
|
146
|
+
structured.contentHash = await calculateFileContentHash(result.path, signal);
|
|
150
147
|
}
|
|
151
148
|
const externalizedResponse = maybeBuildExternalizedReadResponse(args.path, result.content, structured, resourceStore);
|
|
152
149
|
if (externalizedResponse) {
|
|
@@ -4,7 +4,7 @@ import { basename, relative } from 'node:path';
|
|
|
4
4
|
import { createTwoFilesPatch } from 'diff';
|
|
5
5
|
import RE2 from 're2';
|
|
6
6
|
import { DEFAULT_EXCLUDE_PATTERNS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../lib/constants.js';
|
|
7
|
-
import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
7
|
+
import { classifyError, ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
8
8
|
import { globEntries } from '../lib/file-operations/traversal.js';
|
|
9
9
|
import { atomicWriteFile } from '../lib/fs-helpers.js';
|
|
10
10
|
import { Logger } from '../lib/logger.js';
|
|
@@ -340,12 +340,12 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
340
340
|
const dryLabel = args.dryRun ? ' [dry run]' : '';
|
|
341
341
|
const truncatedPattern = truncateProgressPattern(args.searchPattern);
|
|
342
342
|
const context = `"${truncatedPattern}" in ${args.filePattern}${dryLabel}`;
|
|
343
|
-
const progress = createToolProgressSession(ctx,
|
|
343
|
+
const progress = createToolProgressSession(ctx, `${SEARCH_AND_REPLACE_TOOL.title}: ${context}`);
|
|
344
344
|
const progressWithMessage = ({ current, total, }) => {
|
|
345
345
|
progress.update({
|
|
346
346
|
current,
|
|
347
347
|
...(total !== undefined ? { total } : {}),
|
|
348
|
-
message:
|
|
348
|
+
message: `${SEARCH_AND_REPLACE_TOOL.title}: ${truncatedPattern} [${current} files]`,
|
|
349
349
|
});
|
|
350
350
|
};
|
|
351
351
|
try {
|
|
@@ -357,14 +357,14 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
357
357
|
let endSuffix = `${sc.matches ?? 0} ${matchWord} in ${sc.filesChanged ?? 0} ${fileWord}`;
|
|
358
358
|
if (sc.failedFiles)
|
|
359
359
|
endSuffix += `, ${sc.failedFiles} failed`;
|
|
360
|
-
progress.complete(
|
|
360
|
+
progress.complete(`${SEARCH_AND_REPLACE_TOOL.title}: ${context} • ${endSuffix}`, finalCurrent);
|
|
361
361
|
if (!args.dryRun) {
|
|
362
362
|
void ctx.log?.('info', `search_and_replace: ${String(sc.matches ?? 0)} matches in ${String(sc.filesChanged ?? 0)} files`);
|
|
363
363
|
}
|
|
364
364
|
return result;
|
|
365
365
|
}
|
|
366
366
|
catch (error) {
|
|
367
|
-
progress.fail(
|
|
367
|
+
progress.fail(`${SEARCH_AND_REPLACE_TOOL.title}: ${context} • ${classifyError(error)}`);
|
|
368
368
|
throw error;
|
|
369
369
|
}
|
|
370
370
|
},
|
package/dist/tools/roots.js
CHANGED
|
@@ -41,13 +41,13 @@ export function registerListAllowedDirectoriesTool(server, options = {}) {
|
|
|
41
41
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN),
|
|
42
42
|
});
|
|
43
43
|
registerStandardTool(server, LIST_ALLOWED_DIRECTORIES_TOOL, handler, options, {
|
|
44
|
-
progressMessage: () =>
|
|
44
|
+
progressMessage: () => LIST_ALLOWED_DIRECTORIES_TOOL.title,
|
|
45
45
|
completionMessage: (_args, result) => {
|
|
46
46
|
if (result.isError)
|
|
47
|
-
return
|
|
47
|
+
return `${LIST_ALLOWED_DIRECTORIES_TOOL.title} • ${result.errorCode}`;
|
|
48
48
|
const sc = result.structuredContent;
|
|
49
49
|
const count = sc.directories?.length ?? 0;
|
|
50
|
-
return
|
|
50
|
+
return `${LIST_ALLOWED_DIRECTORIES_TOOL.title} • ${count} ${count === 1 ? 'root' : 'roots'}`;
|
|
51
51
|
},
|
|
52
52
|
});
|
|
53
53
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { relative } from 'node:path';
|
|
2
2
|
import RE2 from 're2';
|
|
3
3
|
import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
|
|
4
|
-
import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
4
|
+
import { classifyError, ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
5
5
|
import { searchContent, } from '../lib/file-operations/search.js';
|
|
6
6
|
import { formatOperationSummary } from '../config.js';
|
|
7
7
|
import { SearchContentInputSchema, SearchContentOutputSchema, } from '../schemas.js';
|
|
8
8
|
import { SEARCH_ICONS } from './icons.js';
|
|
9
9
|
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, resolvePathOrRoot, truncateProgressPattern, } from './shared.js';
|
|
10
|
-
import { registerStandardTool } from './task-support.js';
|
|
10
|
+
import { registerStandardTool, reportTaskStatus } from './task-support.js';
|
|
11
11
|
/**
|
|
12
12
|
* Configuration constants for the Search Content tool.
|
|
13
13
|
*/
|
|
@@ -268,7 +268,7 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
268
268
|
context: { path: args.path ?? '.' },
|
|
269
269
|
run: async (signal) => {
|
|
270
270
|
const { pattern, filePattern: scope } = args;
|
|
271
|
-
const progressLabel =
|
|
271
|
+
const progressLabel = `${SEARCH_CONTENT_TOOL.title}: ${truncateProgressPattern(pattern)}`;
|
|
272
272
|
const progress = createToolProgressSession(ctx, progressLabel);
|
|
273
273
|
const progressWithMessage = ({ current, total, }) => {
|
|
274
274
|
progress.update({
|
|
@@ -276,6 +276,7 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
276
276
|
...(total !== undefined ? { total } : {}),
|
|
277
277
|
message: `${progressLabel} [${current} files]`,
|
|
278
278
|
});
|
|
279
|
+
void reportTaskStatus(`${progressLabel} ${current} files`);
|
|
279
280
|
};
|
|
280
281
|
try {
|
|
281
282
|
const result = await handleSearchContent(args, signal, options.resourceStore, progressWithMessage);
|
|
@@ -287,7 +288,7 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
287
288
|
return result;
|
|
288
289
|
}
|
|
289
290
|
catch (error) {
|
|
290
|
-
progress.fail(`${progressLabel} •
|
|
291
|
+
progress.fail(`${progressLabel} • ${classifyError(error)}`);
|
|
291
292
|
throw error;
|
|
292
293
|
}
|
|
293
294
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { basename, relative } from 'node:path';
|
|
2
2
|
import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_TIMEOUT_MS, } from '../lib/constants.js';
|
|
3
|
-
import { ErrorCode } from '../lib/errors.js';
|
|
3
|
+
import { classifyError, ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { searchFiles } from '../lib/file-operations/search.js';
|
|
5
5
|
import { formatOperationSummary, joinLines } from '../config.js';
|
|
6
6
|
import { SearchFilesInputSchema, SearchFilesOutputSchema } from '../schemas.js';
|
|
@@ -126,7 +126,7 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
126
126
|
let progressCursor = 0;
|
|
127
127
|
notifyProgress(ctx, {
|
|
128
128
|
current: 0,
|
|
129
|
-
message:
|
|
129
|
+
message: `${SEARCH_FILES_TOOL.title}: ${truncatedPattern}`,
|
|
130
130
|
});
|
|
131
131
|
const baseReporter = createProgressReporter(ctx);
|
|
132
132
|
const progressWithMessage = ({ current, total, }) => {
|
|
@@ -135,7 +135,7 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
135
135
|
baseReporter({
|
|
136
136
|
current,
|
|
137
137
|
...(total !== undefined ? { total } : {}),
|
|
138
|
-
message:
|
|
138
|
+
message: `${SEARCH_FILES_TOOL.title}: ${truncatedPattern} [${current} files]`,
|
|
139
139
|
});
|
|
140
140
|
};
|
|
141
141
|
try {
|
|
@@ -162,7 +162,7 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
162
162
|
notifyProgress(ctx, {
|
|
163
163
|
current: finalCurrent,
|
|
164
164
|
total: finalCurrent,
|
|
165
|
-
message:
|
|
165
|
+
message: `${SEARCH_FILES_TOOL.title}: ${context} • ${suffix}`,
|
|
166
166
|
});
|
|
167
167
|
return result;
|
|
168
168
|
}
|
|
@@ -171,7 +171,7 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
171
171
|
notifyProgress(ctx, {
|
|
172
172
|
current: finalCurrent,
|
|
173
173
|
total: finalCurrent,
|
|
174
|
-
message:
|
|
174
|
+
message: `${SEARCH_FILES_TOOL.title}: ${context} • ${classifyError(error)}`,
|
|
175
175
|
});
|
|
176
176
|
throw error;
|
|
177
177
|
}
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ContentBlock, Icon, LoggingLevel,
|
|
1
|
+
import type { ContentBlock, Icon, LoggingLevel, Notification, RequestMeta, ServerContext } from '@modelcontextprotocol/server';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import type { ResourceStore } from '../lib/resource-store.js';
|
|
@@ -56,25 +56,27 @@ export type ToolResponse<T> = ReturnType<typeof buildToolResponse<T>> & {
|
|
|
56
56
|
interface ToolErrorResponse extends Record<string, unknown> {
|
|
57
57
|
content: ContentBlock[];
|
|
58
58
|
isError: true;
|
|
59
|
-
errorCode
|
|
59
|
+
errorCode: ErrorCode;
|
|
60
60
|
}
|
|
61
61
|
export type ToolResult<T> = ToolResponse<T> | ToolErrorResponse;
|
|
62
62
|
export declare function withValidatedArgs<Args, Result>(schema: z.ZodType<Args>, handler: (args: Args, ctx: ToolContext) => Promise<ToolResult<Result>>): (args: unknown, ctx: ToolContext | ServerContext) => Promise<ToolResult<Result>>;
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* App-level tracing metadata passed through {@linkcode RequestMeta}.
|
|
65
|
+
* These fields are preserved by the SDK's loose `RequestMeta` type.
|
|
66
|
+
*/
|
|
67
|
+
interface TracingMeta {
|
|
68
|
+
traceparent?: string | undefined;
|
|
69
|
+
tracestate?: string | undefined;
|
|
70
|
+
baggage?: string | undefined;
|
|
71
|
+
}
|
|
64
72
|
export interface ToolContext {
|
|
65
73
|
signal?: AbortSignal;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
tracestate?: string | undefined;
|
|
70
|
-
baggage?: string | undefined;
|
|
71
|
-
} | undefined;
|
|
72
|
-
sendNotification?: (notification: {
|
|
73
|
-
method: 'notifications/progress';
|
|
74
|
-
params: ProgressNotificationParams;
|
|
75
|
-
}) => Promise<void>;
|
|
74
|
+
sessionId?: string;
|
|
75
|
+
_meta?: (RequestMeta & TracingMeta) | undefined;
|
|
76
|
+
sendNotification?: (notification: Notification) => Promise<void>;
|
|
76
77
|
log?: (level: LoggingLevel, data: unknown, logger?: string) => Promise<void>;
|
|
77
78
|
}
|
|
79
|
+
export declare function toToolContext(ctx?: ToolContext | ServerContext): ToolContext;
|
|
78
80
|
export interface IconInfo {
|
|
79
81
|
src: string;
|
|
80
82
|
mimeType: string;
|
|
@@ -168,8 +170,3 @@ export declare function encodeOffsetCursor(offset: number): string;
|
|
|
168
170
|
export declare function decodeOffsetCursor(cursor: string): number;
|
|
169
171
|
export declare function buildBatchPathContext(paths: readonly string[], unitLabel?: string): string;
|
|
170
172
|
export declare function truncateProgressPattern(pattern: string, maxLength?: number): string;
|
|
171
|
-
export declare function buildBatchCompletionSuffix(summary: {
|
|
172
|
-
total?: number;
|
|
173
|
-
failed?: number;
|
|
174
|
-
succeeded?: number;
|
|
175
|
-
} | undefined, successWord: string, singularWord?: string): string;
|