@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
package/dist/tools/shared.js
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
|
-
import * as path from 'node:path';
|
|
2
1
|
import { channel } from 'node:diagnostics_channel';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { createTimedAbortSignal } from '../lib/abort.js';
|
|
4
5
|
import { parseTrueEnvFlag } from '../lib/constants.js';
|
|
5
6
|
import { createDetailedError, ErrorCode, formatDetailedError, getSuggestion, McpError, } from '../lib/errors.js';
|
|
6
|
-
import {
|
|
7
|
-
import { withToolDiagnostics } from '../lib/observability.js';
|
|
7
|
+
import { Logger } from '../lib/logger.js';
|
|
8
|
+
import { withToolDiagnostics, } from '../lib/observability.js';
|
|
8
9
|
import { getAllowedDirectories } from '../lib/paths.js';
|
|
10
|
+
import { createBase64JsonCodec } from '../lib/zod-codecs.js';
|
|
9
11
|
export {} from './contract.js';
|
|
10
12
|
const MAX_INLINE_CONTENT_CHARS = parseInt(process.env['FS_CONTEXT_MAX_INLINE_CHARS'] ?? '', 10) || 20_000;
|
|
11
13
|
const MAX_INLINE_PREVIEW_CHARS = 4_000;
|
|
12
14
|
const PROGRESS_RATE_LIMIT_MS = 50;
|
|
13
15
|
const CONTEXT_DIAGNOSTICS_CHANNEL = channel('filesystem-mcp:context');
|
|
16
|
+
// W3C Trace Context: version-traceid-parentid-traceflags
|
|
17
|
+
const TRACEPARENT_RE = /^[\da-f]{2}-[\da-f]{32}-[\da-f]{16}-[\da-f]{2}$/i;
|
|
18
|
+
function extractTraceContext(meta) {
|
|
19
|
+
const tp = meta?.traceparent;
|
|
20
|
+
if (typeof tp !== 'string' || !TRACEPARENT_RE.test(tp))
|
|
21
|
+
return undefined;
|
|
22
|
+
return {
|
|
23
|
+
traceparent: tp,
|
|
24
|
+
...(typeof meta?.tracestate === 'string'
|
|
25
|
+
? { tracestate: meta.tracestate }
|
|
26
|
+
: {}),
|
|
27
|
+
...(typeof meta?.baggage === 'string' ? { baggage: meta.baggage } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
14
30
|
function publishContextDiagnostics(event) {
|
|
15
31
|
if (!CONTEXT_DIAGNOSTICS_CHANNEL.hasSubscribers)
|
|
16
32
|
return;
|
|
@@ -44,11 +60,13 @@ function normalizeToolExecution(tool) {
|
|
|
44
60
|
? candidate['execution']
|
|
45
61
|
: undefined;
|
|
46
62
|
const executionTaskSupport = existingExecution?.['taskSupport'];
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
let resolvedTaskSupport;
|
|
64
|
+
if (isTaskSupportLevel(topLevelTaskSupport)) {
|
|
65
|
+
resolvedTaskSupport = topLevelTaskSupport;
|
|
66
|
+
}
|
|
67
|
+
else if (isTaskSupportLevel(executionTaskSupport)) {
|
|
68
|
+
resolvedTaskSupport = executionTaskSupport;
|
|
69
|
+
}
|
|
52
70
|
if (resolvedTaskSupport === undefined && topLevelTaskSupport === undefined) {
|
|
53
71
|
return tool;
|
|
54
72
|
}
|
|
@@ -105,6 +123,7 @@ export function maybeExternalizeTextContent(resourceStore, content, params) {
|
|
|
105
123
|
chars: content.length,
|
|
106
124
|
uri: entry.uri,
|
|
107
125
|
});
|
|
126
|
+
Logger.debug(`Content externalized: ${params.name} (${content.length} chars) → ${entry.uri}`);
|
|
108
127
|
return {
|
|
109
128
|
entry,
|
|
110
129
|
preview: buildTextPreview(content),
|
|
@@ -127,30 +146,70 @@ export function buildResourceLink(params) {
|
|
|
127
146
|
}
|
|
128
147
|
function resolveDetailedError(error, defaultCode, path) {
|
|
129
148
|
const detailed = createDetailedError(error, path);
|
|
130
|
-
if (detailed.code === ErrorCode.
|
|
149
|
+
if (detailed.code === ErrorCode.UNKNOWN) {
|
|
131
150
|
detailed.code = defaultCode;
|
|
132
|
-
|
|
151
|
+
const suggestion = getSuggestion(defaultCode);
|
|
152
|
+
if (suggestion) {
|
|
153
|
+
detailed.suggestion = suggestion;
|
|
154
|
+
}
|
|
133
155
|
}
|
|
134
156
|
return detailed;
|
|
135
157
|
}
|
|
158
|
+
export function buildStructuredError(error, defaultCode, path) {
|
|
159
|
+
const detailed = resolveDetailedError(error, defaultCode, path);
|
|
160
|
+
return {
|
|
161
|
+
code: detailed.code,
|
|
162
|
+
message: detailed.message,
|
|
163
|
+
...(detailed.path !== undefined ? { path: detailed.path } : {}),
|
|
164
|
+
...(detailed.suggestion !== undefined
|
|
165
|
+
? { suggestion: detailed.suggestion }
|
|
166
|
+
: {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
136
169
|
export function buildToolResponse(text, structuredContent, extraContent = []) {
|
|
137
170
|
return {
|
|
138
171
|
content: [{ type: 'text', text }, ...extraContent],
|
|
139
172
|
structuredContent,
|
|
140
173
|
};
|
|
141
174
|
}
|
|
175
|
+
function validateStructuredContent(toolName, outputSchema, structuredContent) {
|
|
176
|
+
const parsed = outputSchema.safeParse(structuredContent);
|
|
177
|
+
if (parsed.success) {
|
|
178
|
+
return parsed.data;
|
|
179
|
+
}
|
|
180
|
+
throw new McpError(ErrorCode.UNKNOWN, `Tool "${toolName}" returned invalid structuredContent.`, undefined, { errors: z.treeifyError(parsed.error) });
|
|
181
|
+
}
|
|
182
|
+
function validateToolResponse(toolName, result, outputSchema) {
|
|
183
|
+
if (!outputSchema)
|
|
184
|
+
return result;
|
|
185
|
+
if (!Object.hasOwn(result, 'structuredContent')) {
|
|
186
|
+
throw new McpError(ErrorCode.UNKNOWN, `Tool "${toolName}" returned success without structuredContent.`);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
...result,
|
|
190
|
+
structuredContent: validateStructuredContent(toolName, outputSchema, result.structuredContent),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
142
193
|
function parseToolArgs(schema, args) {
|
|
143
194
|
const candidate = args === undefined ? {} : args;
|
|
144
195
|
const parsed = schema.safeParse(candidate);
|
|
145
196
|
if (parsed.success) {
|
|
146
197
|
return parsed.data;
|
|
147
198
|
}
|
|
148
|
-
throw new McpError(ErrorCode.
|
|
199
|
+
throw new McpError(ErrorCode.INVALID_INPUT, `Invalid tool arguments:\n${z.prettifyError(parsed.error)}`);
|
|
149
200
|
}
|
|
150
201
|
export function withValidatedArgs(schema, handler) {
|
|
151
202
|
return async (args, extra) => {
|
|
152
|
-
|
|
153
|
-
|
|
203
|
+
try {
|
|
204
|
+
const normalizedArgs = parseToolArgs(schema, args);
|
|
205
|
+
return await handler(normalizedArgs, extra);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error instanceof McpError && error.code === ErrorCode.INVALID_INPUT) {
|
|
209
|
+
return buildToolErrorResponse(error, ErrorCode.INVALID_INPUT);
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
154
213
|
};
|
|
155
214
|
}
|
|
156
215
|
function canSendProgress(extra) {
|
|
@@ -202,7 +261,7 @@ export function buildFileInfoPayload(info) {
|
|
|
202
261
|
: {}),
|
|
203
262
|
};
|
|
204
263
|
}
|
|
205
|
-
const NOT_INITIALIZED_ERROR = new McpError(ErrorCode.
|
|
264
|
+
const NOT_INITIALIZED_ERROR = new McpError(ErrorCode.INVALID_INPUT, 'Client not initialized; wait for notifications/initialized');
|
|
206
265
|
async function withToolErrorHandling(run, onError) {
|
|
207
266
|
try {
|
|
208
267
|
return await run();
|
|
@@ -219,15 +278,19 @@ function getToolSignal(extraSignal, timedSignal) {
|
|
|
219
278
|
return { signal, cleanup };
|
|
220
279
|
}
|
|
221
280
|
export async function executeToolWithDiagnostics(options) {
|
|
281
|
+
const traceContext = extractTraceContext(options.extra._meta);
|
|
222
282
|
return withToolDiagnostics(options.toolName, () => withToolErrorHandling(async () => {
|
|
223
283
|
const { signal, cleanup } = getToolSignal(options.extra.signal, options.timedSignal);
|
|
224
284
|
try {
|
|
225
|
-
return await options.run(signal);
|
|
285
|
+
return validateToolResponse(options.toolName, await options.run(signal), options.outputSchema);
|
|
226
286
|
}
|
|
227
287
|
finally {
|
|
228
288
|
cleanup();
|
|
229
289
|
}
|
|
230
|
-
}, options.onError),
|
|
290
|
+
}, options.onError), {
|
|
291
|
+
...options.context,
|
|
292
|
+
...(traceContext ? { traceContext } : {}),
|
|
293
|
+
});
|
|
231
294
|
}
|
|
232
295
|
export function buildToolErrorResponse(error, defaultCode, path) {
|
|
233
296
|
const detailed = resolveDetailedError(error, defaultCode, path);
|
|
@@ -239,7 +302,7 @@ export function buildToolErrorResponse(error, defaultCode, path) {
|
|
|
239
302
|
};
|
|
240
303
|
}
|
|
241
304
|
function buildNotInitializedResult() {
|
|
242
|
-
return buildToolErrorResponse(NOT_INITIALIZED_ERROR, ErrorCode.
|
|
305
|
+
return buildToolErrorResponse(NOT_INITIALIZED_ERROR, ErrorCode.INVALID_INPUT);
|
|
243
306
|
}
|
|
244
307
|
async function reportProgress(extra, progress) {
|
|
245
308
|
await updateTaskStoreProgress(extra, progress);
|
|
@@ -253,6 +316,10 @@ function formatTaskStatusMessage(progress) {
|
|
|
253
316
|
}
|
|
254
317
|
return progress.message ?? `${progress.current}`;
|
|
255
318
|
}
|
|
319
|
+
function isBenignTaskStatusUpdateError(error) {
|
|
320
|
+
return (error instanceof Error &&
|
|
321
|
+
/Task .*not found|terminal status/iu.test(error.message));
|
|
322
|
+
}
|
|
256
323
|
async function updateTaskStoreProgress(extra, progress) {
|
|
257
324
|
const taskExtra = extra;
|
|
258
325
|
if (typeof taskExtra.taskId === 'string' &&
|
|
@@ -264,7 +331,9 @@ async function updateTaskStoreProgress(extra, progress) {
|
|
|
264
331
|
await store.updateTaskStatus(taskExtra.taskId, 'working', formatTaskStatusMessage(progress));
|
|
265
332
|
}
|
|
266
333
|
catch (error) {
|
|
267
|
-
|
|
334
|
+
if (isBenignTaskStatusUpdateError(error))
|
|
335
|
+
return;
|
|
336
|
+
Logger.error('Failed to update task status message:', error);
|
|
268
337
|
}
|
|
269
338
|
}
|
|
270
339
|
}
|
|
@@ -285,7 +354,7 @@ async function sendMcpProgressNotification(extra, progress) {
|
|
|
285
354
|
});
|
|
286
355
|
}
|
|
287
356
|
catch (error) {
|
|
288
|
-
|
|
357
|
+
Logger.error('Failed to send progress notification:', error);
|
|
289
358
|
}
|
|
290
359
|
}
|
|
291
360
|
}
|
|
@@ -450,42 +519,42 @@ export function resolvePathOrRoot(pathValue) {
|
|
|
450
519
|
return pathValue;
|
|
451
520
|
const roots = getAllowedDirectories();
|
|
452
521
|
if (roots.length === 0) {
|
|
453
|
-
throw new McpError(ErrorCode.
|
|
522
|
+
throw new McpError(ErrorCode.ACCESS_DENIED, 'No roots configured. Use roots tool, --allow-cwd, or MCP Roots protocol.');
|
|
454
523
|
}
|
|
455
524
|
if (roots.length > 1) {
|
|
456
|
-
throw new McpError(ErrorCode.
|
|
525
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Multiple roots configured. Provide an explicit path.');
|
|
457
526
|
}
|
|
458
527
|
const root = roots[0];
|
|
459
528
|
if (!root) {
|
|
460
|
-
throw new McpError(ErrorCode.
|
|
529
|
+
throw new McpError(ErrorCode.ACCESS_DENIED, 'Workspace root is unexpectedly undefined');
|
|
461
530
|
}
|
|
462
531
|
return root;
|
|
463
532
|
}
|
|
533
|
+
const OffsetCursorSchema = z.strictObject({
|
|
534
|
+
offset: z.int().min(0),
|
|
535
|
+
});
|
|
536
|
+
const OffsetCursorCodec = createBase64JsonCodec(OffsetCursorSchema);
|
|
464
537
|
export function encodeOffsetCursor(offset) {
|
|
465
|
-
return
|
|
538
|
+
return z.encode(OffsetCursorCodec, { offset });
|
|
466
539
|
}
|
|
467
540
|
export function decodeOffsetCursor(cursor) {
|
|
468
541
|
try {
|
|
469
|
-
|
|
470
|
-
if (typeof parsed === 'object' &&
|
|
471
|
-
parsed !== null &&
|
|
472
|
-
typeof parsed.offset === 'number') {
|
|
473
|
-
const { offset } = parsed;
|
|
474
|
-
if (Number.isInteger(offset) && offset >= 0)
|
|
475
|
-
return offset;
|
|
476
|
-
}
|
|
542
|
+
return OffsetCursorCodec.parse(cursor).offset;
|
|
477
543
|
}
|
|
478
544
|
catch {
|
|
479
545
|
// fall through to throw
|
|
480
546
|
}
|
|
481
|
-
throw new McpError(ErrorCode.
|
|
547
|
+
throw new McpError(ErrorCode.INVALID_INPUT, `Invalid cursor. Request the first page without a cursor.`);
|
|
482
548
|
}
|
|
483
549
|
export function buildBatchPathContext(paths, unitLabel = 'paths') {
|
|
484
550
|
const normalizedLabel = paths.length === 1 ? unitLabel.replace(/s$/i, '') : unitLabel;
|
|
485
|
-
const first =
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
551
|
+
const first = basename(paths[0] ?? '');
|
|
552
|
+
let extraPaths = '';
|
|
553
|
+
if (paths.length > 1) {
|
|
554
|
+
const secondPath = basename(paths[1] ?? '');
|
|
555
|
+
const ellipsis = paths.length > 2 ? '…' : '';
|
|
556
|
+
extraPaths = `, ${secondPath}${ellipsis}`;
|
|
557
|
+
}
|
|
489
558
|
return `${paths.length} ${normalizedLabel} [${first}${extraPaths}]`;
|
|
490
559
|
}
|
|
491
560
|
export function truncateProgressPattern(pattern, maxLength = 40) {
|
package/dist/tools/stat-many.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ErrorCode } from '../lib/errors.js';
|
|
|
3
3
|
import { getMultipleFileInfo } from '../lib/file-operations/metadata.js';
|
|
4
4
|
import { formatBytes, joinLines } from '../config.js';
|
|
5
5
|
import { GetMultipleFileInfoInputSchema, GetMultipleFileInfoOutputSchema, } from '../schemas.js';
|
|
6
|
-
import { buildBatchCompletionSuffix, buildBatchPathContext, buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
6
|
+
import { buildBatchCompletionSuffix, buildBatchPathContext, buildFileInfoPayload, buildStructuredError, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
export const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
9
9
|
name: 'stat_many',
|
|
@@ -14,7 +14,6 @@ export const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
|
14
14
|
outputSchema: GetMultipleFileInfoOutputSchema,
|
|
15
15
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
16
16
|
taskSupport: 'optional',
|
|
17
|
-
nuances: ['Use before read/search when file size/type uncertainty exists.'],
|
|
18
17
|
};
|
|
19
18
|
function formatFileInfoDetail(info) {
|
|
20
19
|
const lines = [
|
|
@@ -38,14 +37,20 @@ async function handleGetMultipleFileInfo(args, signal, onProgress) {
|
|
|
38
37
|
const structuredResults = result.results.map((entry) => ({
|
|
39
38
|
path: entry.path,
|
|
40
39
|
info: entry.info ? buildFileInfoPayload(entry.info) : undefined,
|
|
41
|
-
error: entry.error
|
|
40
|
+
error: entry.error
|
|
41
|
+
? buildStructuredError(entry.error, ErrorCode.NOT_FOUND, entry.path)
|
|
42
|
+
: undefined,
|
|
42
43
|
}));
|
|
43
44
|
const text = result.results
|
|
44
|
-
.map((entry) =>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
.map((entry) => {
|
|
46
|
+
if (entry.error) {
|
|
47
|
+
return `${entry.path}: ${buildStructuredError(entry.error, ErrorCode.NOT_FOUND, entry.path).message}`;
|
|
48
|
+
}
|
|
49
|
+
if (entry.info) {
|
|
50
|
+
return formatFileInfoDetail(entry.info);
|
|
51
|
+
}
|
|
52
|
+
return entry.path;
|
|
53
|
+
})
|
|
49
54
|
.join('\n\n');
|
|
50
55
|
const structured = {
|
|
51
56
|
ok: true,
|
|
@@ -64,6 +69,7 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
64
69
|
return executeToolWithDiagnostics({
|
|
65
70
|
toolName: 'stat_many',
|
|
66
71
|
extra,
|
|
72
|
+
outputSchema: GetMultipleFileInfoOutputSchema,
|
|
67
73
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
68
74
|
context: { path: primaryPath },
|
|
69
75
|
run: async (signal) => {
|
|
@@ -88,7 +94,7 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
88
94
|
throw error;
|
|
89
95
|
}
|
|
90
96
|
},
|
|
91
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.
|
|
97
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.NOT_FOUND, primaryPath),
|
|
92
98
|
});
|
|
93
99
|
};
|
|
94
100
|
const wrappedHandler = wrapToolHandler(handler, {
|
package/dist/tools/stat.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
2
|
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { getFileInfo } from '../lib/file-operations/metadata.js';
|
|
@@ -15,7 +15,6 @@ export const GET_FILE_INFO_TOOL = {
|
|
|
15
15
|
outputSchema: GetFileInfoOutputSchema,
|
|
16
16
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
17
17
|
taskSupport: 'forbidden',
|
|
18
|
-
nuances: ['Use before read/search when file size/type uncertainty exists.'],
|
|
19
18
|
};
|
|
20
19
|
function formatFileInfoDetails(info) {
|
|
21
20
|
const lines = [
|
|
@@ -45,20 +44,21 @@ export function registerGetFileInfoTool(server, options = {}) {
|
|
|
45
44
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
46
45
|
toolName: 'stat',
|
|
47
46
|
extra,
|
|
47
|
+
outputSchema: GetFileInfoOutputSchema,
|
|
48
48
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
49
49
|
context: { path: args.path },
|
|
50
50
|
run: (signal) => handleGetFileInfo(args, signal),
|
|
51
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.
|
|
51
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.NOT_FOUND, args.path),
|
|
52
52
|
});
|
|
53
53
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
54
54
|
guard: options.isInitialized,
|
|
55
|
-
progressMessage: (args) => `🕮 stat: ${
|
|
55
|
+
progressMessage: (args) => `🕮 stat: ${basename(args.path)}`,
|
|
56
56
|
completionMessage: (args, result) => {
|
|
57
|
-
const name =
|
|
57
|
+
const name = basename(args.path);
|
|
58
58
|
if (result.isError)
|
|
59
59
|
return `🕮 stat: ${name} • failed`;
|
|
60
60
|
const sc = result.structuredContent;
|
|
61
|
-
if (!sc.
|
|
61
|
+
if (!sc.info)
|
|
62
62
|
return `🕮 stat: ${name} • failed`;
|
|
63
63
|
return `🕮 stat: ${sc.info.name} • ${sc.info.type}, ${formatBytes(sc.info.size)}`;
|
|
64
64
|
},
|
|
@@ -2,20 +2,21 @@ import type { ToolTaskHandler } from '@modelcontextprotocol/sdk/experimental/tas
|
|
|
2
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import type { AnySchema, SchemaOutput, ShapeOutput, ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js';
|
|
4
4
|
import type { RequestTaskStore } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
5
|
-
import type
|
|
5
|
+
import { type IconInfo, type ToolExtra, type ToolResult } from './shared.js';
|
|
6
6
|
type TaskToolExtra = ToolExtra & {
|
|
7
7
|
taskId?: string;
|
|
8
8
|
taskStore?: RequestTaskStore;
|
|
9
9
|
taskRequestedTtl?: number | null;
|
|
10
10
|
};
|
|
11
|
-
type
|
|
12
|
-
|
|
13
|
-
export declare function
|
|
11
|
+
export type ToolSchema = ZodRawShapeCompat | AnySchema | undefined;
|
|
12
|
+
type ToolArgs<Args extends ToolSchema> = Args extends ZodRawShapeCompat ? ShapeOutput<Args> : Args extends AnySchema ? SchemaOutput<Args> : undefined;
|
|
13
|
+
export declare function registerToolTaskIfAvailable<Args extends ToolSchema, Result>(server: McpServer, toolName: string, toolDef: object, run: (args: ToolArgs<Args>, extra: TaskToolExtra) => Promise<ToolResult<Result>>, iconInfo: IconInfo | undefined, guard?: () => boolean): boolean;
|
|
14
|
+
interface TaskHandlerOptions {
|
|
14
15
|
guard?: () => boolean;
|
|
15
16
|
toolName?: string;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
cancelPollMs?: number;
|
|
18
|
+
pollIntervalMs?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function createToolTaskHandler<Result>(run: (args: undefined, extra: TaskToolExtra) => Promise<ToolResult<Result>>, options?: TaskHandlerOptions): ToolTaskHandler;
|
|
21
|
+
export declare function createToolTaskHandler<Args extends ZodRawShapeCompat | AnySchema, Result>(run: (args: ToolArgs<Args>, extra: TaskToolExtra) => Promise<ToolResult<Result>>, options?: TaskHandlerOptions): ToolTaskHandler<Args>;
|
|
21
22
|
export {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { CallToolResultSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
1
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
3
|
import { channel } from 'node:diagnostics_channel';
|
|
3
4
|
import { performance } from 'node:perf_hooks';
|
|
4
5
|
import { format } from 'node:util';
|
|
5
|
-
import {
|
|
6
|
-
import { DEFAULT_TASK_TTL_MS, MAX_CONCURRENT_TASKS, MAX_TASK_TTL_MS, } from '../lib/constants.js';
|
|
6
|
+
import { DEFAULT_TASK_TTL_MS, MAX_CONCURRENT_TASKS, MAX_TASK_TTL_MS, TASK_CANCEL_POLL_MS, TASK_POLL_INTERVAL_MS, } from '../lib/constants.js';
|
|
7
7
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
8
|
+
import { Logger } from '../lib/logger.js';
|
|
8
9
|
import { isRecord } from '../lib/utils.js';
|
|
9
10
|
import { buildToolErrorResponse, maybeStripStructuredContentFromResult, withDefaultIcons, } from './shared.js';
|
|
10
11
|
const taskContext = new AsyncLocalStorage();
|
|
@@ -50,6 +51,7 @@ function hasTaskToolCapability(server) {
|
|
|
50
51
|
isRecord(tasks.requests.tools.call));
|
|
51
52
|
}
|
|
52
53
|
const TASK_STATUS_NOTIFICATION_METHOD = 'notifications/tasks/status';
|
|
54
|
+
const TASK_CREATED_NOTIFICATION_METHOD = 'notifications/tasks/created';
|
|
53
55
|
function isRequestTaskStore(value) {
|
|
54
56
|
return (isRecord(value) &&
|
|
55
57
|
typeof value.createTask === 'function' &&
|
|
@@ -67,13 +69,13 @@ function isTaskExtra(value) {
|
|
|
67
69
|
}
|
|
68
70
|
function asCreateTaskExtra(value) {
|
|
69
71
|
if (!isCreateTaskExtra(value)) {
|
|
70
|
-
throw new McpError(ErrorCode.
|
|
72
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Task store not configured.');
|
|
71
73
|
}
|
|
72
74
|
return value;
|
|
73
75
|
}
|
|
74
76
|
function asTaskRequestExtra(value) {
|
|
75
77
|
if (!isTaskExtra(value)) {
|
|
76
|
-
throw new McpError(ErrorCode.
|
|
78
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Task id or store missing.');
|
|
77
79
|
}
|
|
78
80
|
return value;
|
|
79
81
|
}
|
|
@@ -90,11 +92,11 @@ function isTaskStatus(value) {
|
|
|
90
92
|
}
|
|
91
93
|
function normalizeGetTaskResult(value) {
|
|
92
94
|
if (!isRecord(value) || typeof value.taskId !== 'string') {
|
|
93
|
-
throw new McpError(ErrorCode.
|
|
95
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Invalid task object.');
|
|
94
96
|
}
|
|
95
97
|
const status = isTaskStatus(value.status) ? value.status : undefined;
|
|
96
98
|
if (!status) {
|
|
97
|
-
throw new McpError(ErrorCode.
|
|
99
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Invalid task status.');
|
|
98
100
|
}
|
|
99
101
|
const createdAt = typeof value.createdAt === 'string'
|
|
100
102
|
? value.createdAt
|
|
@@ -123,7 +125,7 @@ function normalizeCallToolResult(value) {
|
|
|
123
125
|
const parsed = CallToolResultSchema.safeParse(value);
|
|
124
126
|
if (parsed.success)
|
|
125
127
|
return parsed.data;
|
|
126
|
-
throw new McpError(ErrorCode.
|
|
128
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Invalid stored task result.');
|
|
127
129
|
}
|
|
128
130
|
function getToolResultErrorCode(result) {
|
|
129
131
|
if (!isRecord(result) || result.isError !== true)
|
|
@@ -143,7 +145,7 @@ function getToolResultErrorCode(result) {
|
|
|
143
145
|
return match ? match[1] : undefined;
|
|
144
146
|
}
|
|
145
147
|
function isCancelledToolResult(result) {
|
|
146
|
-
return getToolResultErrorCode(result) === ErrorCode.
|
|
148
|
+
return getToolResultErrorCode(result) === ErrorCode.CANCELLED;
|
|
147
149
|
}
|
|
148
150
|
function resolveTaskResultStatuses(result) {
|
|
149
151
|
if (isCancelledToolResult(result)) {
|
|
@@ -192,6 +194,31 @@ function buildTaskStatusNotificationParams(task) {
|
|
|
192
194
|
params.statusMessage = task.statusMessage;
|
|
193
195
|
return params;
|
|
194
196
|
}
|
|
197
|
+
async function notifyTaskCreatedIfPossible(extra, taskId, toolName) {
|
|
198
|
+
const { sendNotification } = extra;
|
|
199
|
+
if (typeof sendNotification !== 'function')
|
|
200
|
+
return;
|
|
201
|
+
const notify = sendNotification;
|
|
202
|
+
try {
|
|
203
|
+
await notify({
|
|
204
|
+
method: TASK_CREATED_NOTIFICATION_METHOD,
|
|
205
|
+
params: {
|
|
206
|
+
_meta: {
|
|
207
|
+
'modelcontextprotocol.io/related-task': {
|
|
208
|
+
taskId,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
publishTaskDiagnostics({
|
|
216
|
+
phase: 'task_status_notify_failed',
|
|
217
|
+
taskId,
|
|
218
|
+
...(toolName ? { toolName } : {}),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
195
222
|
async function notifyTaskStatusIfPossible(extra, taskStore, taskId, toolName) {
|
|
196
223
|
const { sendNotification } = extra;
|
|
197
224
|
if (typeof sendNotification !== 'function')
|
|
@@ -222,13 +249,13 @@ async function notifyTaskStatusIfPossible(extra, taskStore, taskId, toolName) {
|
|
|
222
249
|
}
|
|
223
250
|
function getTaskStore(extra) {
|
|
224
251
|
if (!extra.taskStore) {
|
|
225
|
-
throw new McpError(ErrorCode.
|
|
252
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Task store not configured.');
|
|
226
253
|
}
|
|
227
254
|
return extra.taskStore;
|
|
228
255
|
}
|
|
229
256
|
function getTaskId(extra) {
|
|
230
257
|
if (!extra.taskId) {
|
|
231
|
-
throw new McpError(ErrorCode.
|
|
258
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Task id missing.');
|
|
232
259
|
}
|
|
233
260
|
return extra.taskId;
|
|
234
261
|
}
|
|
@@ -260,6 +287,10 @@ async function isTaskAlreadyTerminal(taskStore, taskId) {
|
|
|
260
287
|
return false;
|
|
261
288
|
}
|
|
262
289
|
}
|
|
290
|
+
function isMissingTaskStoreError(error) {
|
|
291
|
+
return (error instanceof Error &&
|
|
292
|
+
/Task .*not found|not found after cancellation/iu.test(error.message));
|
|
293
|
+
}
|
|
263
294
|
async function countActiveTasks(taskStore) {
|
|
264
295
|
if (typeof taskStore.listTasks !== 'function')
|
|
265
296
|
return 0;
|
|
@@ -281,11 +312,11 @@ function resolveRequestedTaskTtl(requestedTtl) {
|
|
|
281
312
|
if (requestedTtl == null)
|
|
282
313
|
return DEFAULT_TASK_TTL_MS;
|
|
283
314
|
if (!Number.isFinite(requestedTtl)) {
|
|
284
|
-
throw new McpError(ErrorCode.
|
|
315
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Task TTL must be finite.');
|
|
285
316
|
}
|
|
286
317
|
const normalized = Math.trunc(requestedTtl);
|
|
287
318
|
if (normalized <= 0) {
|
|
288
|
-
throw new McpError(ErrorCode.
|
|
319
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Task TTL must be > 0.');
|
|
289
320
|
}
|
|
290
321
|
return Math.min(normalized, MAX_TASK_TTL_MS);
|
|
291
322
|
}
|
|
@@ -294,18 +325,40 @@ async function safelyStoreTaskResult(taskStore, taskId, storedStatus, result) {
|
|
|
294
325
|
await taskStore.storeTaskResult(taskId, storedStatus, result);
|
|
295
326
|
}
|
|
296
327
|
catch (error) {
|
|
328
|
+
if (isMissingTaskStoreError(error))
|
|
329
|
+
return;
|
|
297
330
|
// If task was already cancelled/failed by another process, ignore the write error
|
|
298
331
|
if (await isTaskAlreadyTerminal(taskStore, taskId))
|
|
299
332
|
return;
|
|
300
333
|
throw error;
|
|
301
334
|
}
|
|
302
335
|
}
|
|
303
|
-
async function
|
|
336
|
+
async function isTaskCancelled(taskStore, taskId) {
|
|
337
|
+
try {
|
|
338
|
+
const task = await taskStore.getTask(taskId);
|
|
339
|
+
return isRecord(task) && task.status === 'cancelled';
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName, cancelPollMs) {
|
|
346
|
+
// Create a dedicated AbortController for background execution.
|
|
347
|
+
// The original request signal is stale once createTask returns.
|
|
348
|
+
const taskAbort = new AbortController();
|
|
349
|
+
const taskExtra = { ...extra, signal: taskAbort.signal };
|
|
350
|
+
// Poll the task store for client-initiated cancellation.
|
|
351
|
+
const cancelPoller = setInterval(() => {
|
|
352
|
+
void isTaskCancelled(taskStore, taskId).then((cancelled) => {
|
|
353
|
+
if (cancelled)
|
|
354
|
+
taskAbort.abort(new Error('Task cancelled by client'));
|
|
355
|
+
});
|
|
356
|
+
}, cancelPollMs ?? TASK_CANCEL_POLL_MS);
|
|
304
357
|
const start = performance.now();
|
|
305
358
|
let taskStatuses;
|
|
306
359
|
let result;
|
|
307
360
|
try {
|
|
308
|
-
const rawResult = await taskContext.run({ taskId, toolName, startTime: start }, () => run(args,
|
|
361
|
+
const rawResult = await taskContext.run({ taskId, toolName, startTime: start }, () => run(args, taskExtra));
|
|
309
362
|
taskStatuses = resolveTaskResultStatuses(rawResult);
|
|
310
363
|
result = isErrorResult(rawResult)
|
|
311
364
|
? withoutStructuredContent(rawResult)
|
|
@@ -313,7 +366,10 @@ async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName
|
|
|
313
366
|
}
|
|
314
367
|
catch (error) {
|
|
315
368
|
taskStatuses = { storedStatus: 'failed', reportedStatus: 'failed' };
|
|
316
|
-
result = maybeStripStructuredContentFromResult(buildToolErrorResponse(error, ErrorCode.
|
|
369
|
+
result = maybeStripStructuredContentFromResult(buildToolErrorResponse(error, ErrorCode.UNKNOWN));
|
|
370
|
+
}
|
|
371
|
+
finally {
|
|
372
|
+
clearInterval(cancelPoller);
|
|
317
373
|
}
|
|
318
374
|
const durationMs = performance.now() - start;
|
|
319
375
|
try {
|
|
@@ -328,7 +384,7 @@ async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName
|
|
|
328
384
|
await notifyTaskStatusIfPossible(extra, taskStore, taskId, toolName);
|
|
329
385
|
}
|
|
330
386
|
catch (innerError) {
|
|
331
|
-
|
|
387
|
+
Logger.error(format('Failed to store task result for task %s:', taskId), innerError);
|
|
332
388
|
const syntheticTask = {
|
|
333
389
|
taskId,
|
|
334
390
|
status: taskStatuses.reportedStatus === 'cancelled' ? 'cancelled' : 'failed',
|
|
@@ -339,10 +395,16 @@ async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName
|
|
|
339
395
|
};
|
|
340
396
|
const { sendNotification } = extra;
|
|
341
397
|
if (typeof sendNotification === 'function') {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
398
|
+
try {
|
|
399
|
+
await sendNotification({
|
|
400
|
+
method: TASK_STATUS_NOTIFICATION_METHOD,
|
|
401
|
+
params: buildTaskStatusNotificationParams(syntheticTask),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
// Best effort only: transport may already be closed while a cancelled
|
|
406
|
+
// task is still unwinding in the background.
|
|
407
|
+
}
|
|
346
408
|
}
|
|
347
409
|
}
|
|
348
410
|
}
|
|
@@ -379,15 +441,23 @@ export function createToolTaskHandler(run, options) {
|
|
|
379
441
|
const extra = asCreateTaskExtra(maybeExtra ?? argsOrExtra);
|
|
380
442
|
const args = (maybeExtra ? argsOrExtra : undefined);
|
|
381
443
|
if (options?.guard && !options.guard()) {
|
|
382
|
-
throw new McpError(ErrorCode.
|
|
444
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Client not initialized; wait for notifications/initialized');
|
|
383
445
|
}
|
|
384
446
|
const taskStore = getTaskStore(extra);
|
|
385
447
|
if ((await countActiveTasks(taskStore)) >= MAX_CONCURRENT_TASKS) {
|
|
386
|
-
throw new McpError(ErrorCode.
|
|
448
|
+
throw new McpError(ErrorCode.INVALID_INPUT, `Too many active tasks (limit: ${String(MAX_CONCURRENT_TASKS)}).`);
|
|
387
449
|
}
|
|
388
450
|
const task = await taskStore.createTask({
|
|
389
451
|
ttl: resolveRequestedTaskTtl(extra.taskRequestedTtl),
|
|
452
|
+
pollInterval: options?.pollIntervalMs ?? TASK_POLL_INTERVAL_MS,
|
|
390
453
|
});
|
|
454
|
+
const toolLabel = options?.toolName ?? 'tool';
|
|
455
|
+
try {
|
|
456
|
+
await taskStore.updateTaskStatus(task.taskId, 'working', `${toolLabel}: starting`);
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// Best effort — status message is informational.
|
|
460
|
+
}
|
|
391
461
|
publishTaskDiagnostics({
|
|
392
462
|
phase: 'task_created',
|
|
393
463
|
taskId: task.taskId,
|
|
@@ -399,8 +469,9 @@ export function createToolTaskHandler(run, options) {
|
|
|
399
469
|
taskStore,
|
|
400
470
|
taskId: task.taskId,
|
|
401
471
|
};
|
|
472
|
+
void notifyTaskCreatedIfPossible(taskExtra, task.taskId, options?.toolName);
|
|
402
473
|
void notifyTaskStatusIfPossible(taskExtra, taskStore, task.taskId, options?.toolName);
|
|
403
|
-
void runTaskInBackground(run, args, taskExtra, taskStore, task.taskId, options?.toolName);
|
|
474
|
+
void runTaskInBackground(run, args, taskExtra, taskStore, task.taskId, options?.toolName, options?.cancelPollMs);
|
|
404
475
|
return { task };
|
|
405
476
|
});
|
|
406
477
|
const getTask = (async (argsOrExtra, maybeExtra) => {
|