@j0hanz/filesystem-mcp 1.2.2 → 1.2.3
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 +11 -0
- package/dist/cli.js +2 -4
- package/dist/completions.js +15 -6
- package/dist/lib/file-operations/glob-engine.js +3 -9
- package/dist/lib/file-operations/read-multiple-files.js +4 -23
- package/dist/lib/file-operations/search-content.js +11 -18
- package/dist/lib/observability.js +4 -4
- package/dist/lib/path-validation.js +22 -9
- package/dist/pkg-info.d.ts +6 -0
- package/dist/pkg-info.js +9 -0
- package/dist/schemas.d.ts +28 -28
- package/dist/schemas.js +26 -26
- package/dist/server/bootstrap.d.ts +4 -0
- package/dist/server/bootstrap.js +117 -0
- package/dist/server/capabilities.d.ts +10 -0
- package/dist/server/capabilities.js +40 -0
- package/dist/server/logging.d.ts +7 -0
- package/dist/server/logging.js +41 -0
- package/dist/server/roots-manager.d.ts +19 -0
- package/dist/server/roots-manager.js +173 -0
- package/dist/server/types.d.ts +4 -0
- package/dist/server/types.js +1 -0
- package/dist/server.d.ts +2 -8
- package/dist/server.js +1 -317
- package/dist/tools/apply-patch.js +3 -2
- package/dist/tools/calculate-hash.js +3 -2
- package/dist/tools/create-directory.js +3 -2
- package/dist/tools/delete-file.js +9 -2
- package/dist/tools/diff-files.js +3 -2
- package/dist/tools/edit-file.js +5 -4
- package/dist/tools/list-directory.js +13 -2
- package/dist/tools/move-file.js +11 -3
- package/dist/tools/read-multiple.js +6 -5
- package/dist/tools/read.js +3 -2
- package/dist/tools/replace-in-files.js +3 -2
- package/dist/tools/roots.js +12 -2
- package/dist/tools/search-content.js +10 -15
- package/dist/tools/search-files.js +13 -9
- package/dist/tools/shared.d.ts +3 -1
- package/dist/tools/shared.js +19 -1
- package/dist/tools/stat-many.js +6 -5
- package/dist/tools/stat.js +4 -3
- package/dist/tools/task-support.js +57 -10
- package/dist/tools/tree.js +15 -2
- package/dist/tools/write-file.js +3 -2
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
|
|
|
5
5
|
import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
6
6
|
import { searchContent } from '../lib/file-operations/search-content.js';
|
|
7
7
|
import { SearchContentInputSchema, SearchContentOutputSchema, } from '../schemas.js';
|
|
8
|
-
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
8
|
+
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
9
9
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
10
10
|
const MAX_INLINE_MATCHES = 50;
|
|
11
11
|
const SEARCH_CONTENT_TOOL = {
|
|
@@ -69,16 +69,14 @@ function formatSearchMatchLine(match) {
|
|
|
69
69
|
}
|
|
70
70
|
function buildStructuredSearchResult(result, normalizedMatches, options) {
|
|
71
71
|
const { summary } = result;
|
|
72
|
-
const matches =
|
|
73
|
-
for (const match of normalizedMatches) {
|
|
74
|
-
matches.push(buildSearchMatchPayload(match));
|
|
75
|
-
}
|
|
72
|
+
const matches = normalizedMatches.map((match) => buildSearchMatchPayload(match));
|
|
76
73
|
return {
|
|
77
74
|
ok: true,
|
|
78
75
|
patternType: options.patternType,
|
|
79
76
|
caseSensitive: options.caseSensitive,
|
|
80
77
|
matches,
|
|
81
78
|
totalMatches: summary.matches,
|
|
79
|
+
filesScanned: summary.filesScanned,
|
|
82
80
|
...(summary.truncated ? { truncated: summary.truncated } : {}),
|
|
83
81
|
...(summary.filesMatched ? { filesMatched: summary.filesMatched } : {}),
|
|
84
82
|
...(summary.skippedTooLarge
|
|
@@ -163,10 +161,7 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
163
161
|
return buildToolResponse(buildSearchTextResult(result, normalizedMatches), structuredFull);
|
|
164
162
|
}
|
|
165
163
|
const previewMatches = normalizedMatches.slice(0, MAX_INLINE_MATCHES);
|
|
166
|
-
const previewPayload =
|
|
167
|
-
for (const match of previewMatches) {
|
|
168
|
-
previewPayload.push(buildSearchMatchPayload(match));
|
|
169
|
-
}
|
|
164
|
+
const previewPayload = previewMatches.map((match) => buildSearchMatchPayload(match));
|
|
170
165
|
const previewStructured = {
|
|
171
166
|
...structuredFull,
|
|
172
167
|
matches: previewPayload,
|
|
@@ -201,9 +196,8 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
201
196
|
extra,
|
|
202
197
|
context: { path: args.path ?? '.' },
|
|
203
198
|
run: async (signal) => {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
const { pattern } = normalizedArgs;
|
|
199
|
+
const scope = args.filePattern;
|
|
200
|
+
const { pattern } = args;
|
|
207
201
|
let progressCursor = 0;
|
|
208
202
|
notifyProgress(extra, {
|
|
209
203
|
current: 0,
|
|
@@ -217,11 +211,11 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
217
211
|
baseReporter({
|
|
218
212
|
current,
|
|
219
213
|
...(total !== undefined ? { total } : {}),
|
|
220
|
-
message: `🔎︎ grep: ${pattern}
|
|
214
|
+
message: `🔎︎ grep: ${pattern} — ${current} ${fileWord} scanned`,
|
|
221
215
|
});
|
|
222
216
|
};
|
|
223
217
|
try {
|
|
224
|
-
const result = await handleSearchContent(
|
|
218
|
+
const result = await handleSearchContent(args, signal, options.resourceStore, progressWithMessage);
|
|
225
219
|
const sc = result.structuredContent;
|
|
226
220
|
const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
|
|
227
221
|
const filesMatched = sc.ok ? (sc.filesMatched ?? 0) : 0;
|
|
@@ -267,7 +261,8 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
267
261
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path ?? '.'),
|
|
268
262
|
});
|
|
269
263
|
const { isInitialized } = options;
|
|
270
|
-
const
|
|
264
|
+
const validatedHandler = withValidatedArgs(SearchContentInputSchema, handler);
|
|
265
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
271
266
|
guard: isInitialized,
|
|
272
267
|
});
|
|
273
268
|
if (registerToolTaskIfAvailable(server, 'grep', SEARCH_CONTENT_TOOL, wrappedHandler, options.iconInfo, isInitialized))
|
|
@@ -4,7 +4,7 @@ import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_TIMEOUT_MS, } from '../lib/con
|
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import { searchFiles } from '../lib/file-operations/search-files.js';
|
|
6
6
|
import { SearchFilesInputSchema, SearchFilesOutputSchema } from '../schemas.js';
|
|
7
|
-
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
const SEARCH_FILES_TOOL = {
|
|
10
10
|
title: 'Find Files',
|
|
@@ -43,6 +43,7 @@ async function handleSearchFiles(args, signal, onProgress) {
|
|
|
43
43
|
pattern: args.pattern,
|
|
44
44
|
results: relativeResults,
|
|
45
45
|
totalMatches: result.summary.matched,
|
|
46
|
+
filesScanned: result.summary.filesScanned,
|
|
46
47
|
...(result.summary.truncated
|
|
47
48
|
? { truncated: result.summary.truncated }
|
|
48
49
|
: {}),
|
|
@@ -89,12 +90,14 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
89
90
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
90
91
|
context: { path: args.path ?? '.' },
|
|
91
92
|
run: async (signal) => {
|
|
92
|
-
const
|
|
93
|
+
const rawScopeLabel = args.path ? path.basename(args.path) : '.';
|
|
94
|
+
const scopeLabel = rawScopeLabel || '.';
|
|
93
95
|
const { pattern } = args;
|
|
96
|
+
const context = `${pattern} in ${scopeLabel}`;
|
|
94
97
|
let progressCursor = 0;
|
|
95
98
|
notifyProgress(extra, {
|
|
96
99
|
current: 0,
|
|
97
|
-
message: `🔎︎ find: ${
|
|
100
|
+
message: `🔎︎ find: ${context}`,
|
|
98
101
|
});
|
|
99
102
|
const baseReporter = createProgressReporter(extra);
|
|
100
103
|
const progressWithMessage = ({ current, total, }) => {
|
|
@@ -104,7 +107,7 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
104
107
|
baseReporter({
|
|
105
108
|
current,
|
|
106
109
|
...(total !== undefined ? { total } : {}),
|
|
107
|
-
message: `🔎︎ find: ${pattern}
|
|
110
|
+
message: `🔎︎ find: ${pattern} [${current} ${fileWord} scanned]`,
|
|
108
111
|
});
|
|
109
112
|
};
|
|
110
113
|
try {
|
|
@@ -114,7 +117,7 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
114
117
|
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
115
118
|
let suffix;
|
|
116
119
|
if (count === 0) {
|
|
117
|
-
suffix = `No matches in ${
|
|
120
|
+
suffix = `No matches in ${scopeLabel}`;
|
|
118
121
|
}
|
|
119
122
|
else {
|
|
120
123
|
suffix = `${count} ${count === 1 ? 'match' : 'matches'}`;
|
|
@@ -132,7 +135,7 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
132
135
|
notifyProgress(extra, {
|
|
133
136
|
current: finalCurrent,
|
|
134
137
|
total: finalCurrent,
|
|
135
|
-
message: `🔎︎ find: ${
|
|
138
|
+
message: `🔎︎ find: ${context} • ${suffix}`,
|
|
136
139
|
});
|
|
137
140
|
return result;
|
|
138
141
|
}
|
|
@@ -141,15 +144,16 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
141
144
|
notifyProgress(extra, {
|
|
142
145
|
current: finalCurrent,
|
|
143
146
|
total: finalCurrent,
|
|
144
|
-
message: `🔎︎ find: ${
|
|
147
|
+
message: `🔎︎ find: ${context} • failed`,
|
|
145
148
|
});
|
|
146
149
|
throw error;
|
|
147
150
|
}
|
|
148
151
|
},
|
|
149
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.
|
|
152
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
|
|
150
153
|
});
|
|
151
154
|
const { isInitialized } = options;
|
|
152
|
-
const
|
|
155
|
+
const validatedHandler = withValidatedArgs(SearchFilesInputSchema, handler);
|
|
156
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
153
157
|
guard: isInitialized,
|
|
154
158
|
});
|
|
155
159
|
if (registerToolTaskIfAvailable(server, 'find', SEARCH_FILES_TOOL, wrappedHandler, options.iconInfo, isInitialized))
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContentBlock, Icon, ProgressNotificationParams } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import
|
|
2
|
+
import { z } from 'zod';
|
|
3
3
|
import type { FileInfo } from '../config.js';
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import type { ResourceStore } from '../lib/resource-store.js';
|
|
@@ -47,6 +47,8 @@ interface ToolErrorResponse extends Record<string, unknown> {
|
|
|
47
47
|
isError: true;
|
|
48
48
|
}
|
|
49
49
|
export type ToolResult<T> = ToolResponse<T> | ToolErrorResponse;
|
|
50
|
+
export declare function parseToolArgs<Schema extends z.ZodType>(schema: Schema, args: unknown): z.infer<Schema>;
|
|
51
|
+
export declare function withValidatedArgs<Args, Result>(schema: z.ZodType<Args>, handler: (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>): (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>;
|
|
50
52
|
type ProgressToken = string | number;
|
|
51
53
|
export interface ToolExtra {
|
|
52
54
|
signal?: AbortSignal;
|
package/dist/tools/shared.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { createDetailedError, ErrorCode, formatDetailedError, getSuggestion, McpError, } from '../lib/errors.js';
|
|
2
3
|
import { createTimedAbortSignal } from '../lib/fs-helpers.js';
|
|
3
4
|
import { withToolDiagnostics } from '../lib/observability.js';
|
|
@@ -91,6 +92,20 @@ function resolveDetailedError(error, defaultCode, path) {
|
|
|
91
92
|
export function buildToolResponse(text, structuredContent, extraContent = []) {
|
|
92
93
|
return buildContentBlock(text, structuredContent, extraContent);
|
|
93
94
|
}
|
|
95
|
+
export function parseToolArgs(schema, args) {
|
|
96
|
+
const candidate = args === undefined ? {} : args;
|
|
97
|
+
const parsed = schema.safeParse(candidate);
|
|
98
|
+
if (parsed.success) {
|
|
99
|
+
return parsed.data;
|
|
100
|
+
}
|
|
101
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid tool arguments: ${parsed.error.message}`, undefined, { errors: z.treeifyError(parsed.error) });
|
|
102
|
+
}
|
|
103
|
+
export function withValidatedArgs(schema, handler) {
|
|
104
|
+
return async (args, extra) => {
|
|
105
|
+
const normalizedArgs = parseToolArgs(schema, args);
|
|
106
|
+
return handler(normalizedArgs, extra);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
94
109
|
function canSendProgress(extra) {
|
|
95
110
|
return (extra._meta?.progressToken !== undefined &&
|
|
96
111
|
extra.sendNotification !== undefined);
|
|
@@ -211,9 +226,11 @@ export function createProgressReporter(extra) {
|
|
|
211
226
|
return (progress) => {
|
|
212
227
|
const { current, total, message } = progress;
|
|
213
228
|
// Enforce monotonic progress to prevent client confusion. Client behavior on
|
|
229
|
+
// out-of-order progress is undefined in the MCP spec.
|
|
214
230
|
if (current <= lastProgress)
|
|
215
231
|
return;
|
|
216
|
-
// Enforce rate-limiting to prevent client flooding. Progress updates
|
|
232
|
+
// Enforce rate-limiting to prevent client flooding. Progress updates faster
|
|
233
|
+
// than PROGRESS_RATE_LIMIT_MS are silently dropped.
|
|
217
234
|
const now = Date.now();
|
|
218
235
|
if (now - lastSentMs < PROGRESS_RATE_LIMIT_MS)
|
|
219
236
|
return;
|
|
@@ -266,6 +283,7 @@ async function withProgress(message, extra, run, getCompletionMessage) {
|
|
|
266
283
|
progressToken: token,
|
|
267
284
|
progress: total,
|
|
268
285
|
total,
|
|
286
|
+
message: `${message} • failed`,
|
|
269
287
|
});
|
|
270
288
|
throw error;
|
|
271
289
|
}
|
package/dist/tools/stat-many.js
CHANGED
|
@@ -4,7 +4,7 @@ import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import { getMultipleFileInfo } from '../lib/file-operations/file-info.js';
|
|
6
6
|
import { GetMultipleFileInfoInputSchema, GetMultipleFileInfoOutputSchema, } from '../schemas.js';
|
|
7
|
-
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
10
10
|
title: 'Get Multiple File Info',
|
|
@@ -73,19 +73,20 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
73
73
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, primaryPath),
|
|
74
74
|
});
|
|
75
75
|
};
|
|
76
|
-
const
|
|
76
|
+
const validatedHandler = withValidatedArgs(GetMultipleFileInfoInputSchema, handler);
|
|
77
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
77
78
|
guard: options.isInitialized,
|
|
78
79
|
progressMessage: (args) => {
|
|
79
80
|
const first = path.basename(args.paths[0] ?? '');
|
|
80
81
|
const extra = args.paths.length > 1 ? `, ${path.basename(args.paths[1] ?? '')}…` : '';
|
|
81
82
|
return `🕮 stat_many: ${args.paths.length} paths [${first}${extra}]`;
|
|
82
83
|
},
|
|
83
|
-
completionMessage: (
|
|
84
|
+
completionMessage: (args, result) => {
|
|
84
85
|
if (result.isError)
|
|
85
|
-
return `🕮 stat_many • failed`;
|
|
86
|
+
return `🕮 stat_many: ${args.paths.length} paths • failed`;
|
|
86
87
|
const sc = result.structuredContent;
|
|
87
88
|
if (!sc.ok)
|
|
88
|
-
return `🕮 stat_many • failed`;
|
|
89
|
+
return `🕮 stat_many: ${args.paths.length} paths • failed`;
|
|
89
90
|
const total = sc.summary?.total ?? 0;
|
|
90
91
|
const succeeded = sc.summary?.succeeded ?? 0;
|
|
91
92
|
const failed = sc.summary?.failed ?? 0;
|
package/dist/tools/stat.js
CHANGED
|
@@ -4,7 +4,7 @@ import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import { getFileInfo } from '../lib/file-operations/file-info.js';
|
|
6
6
|
import { GetFileInfoInputSchema, GetFileInfoOutputSchema } from '../schemas.js';
|
|
7
|
-
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
const GET_FILE_INFO_TOOL = {
|
|
9
9
|
title: 'Get File Info',
|
|
10
10
|
description: 'Get metadata (size, modified time, permissions, mime type) for a file or directory.',
|
|
@@ -45,7 +45,8 @@ export function registerGetFileInfoTool(server, options = {}) {
|
|
|
45
45
|
run: (signal) => handleGetFileInfo(args, signal),
|
|
46
46
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, args.path),
|
|
47
47
|
});
|
|
48
|
-
|
|
48
|
+
const validatedHandler = withValidatedArgs(GetFileInfoInputSchema, handler);
|
|
49
|
+
server.registerTool('stat', withDefaultIcons({ ...GET_FILE_INFO_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
|
|
49
50
|
guard: options.isInitialized,
|
|
50
51
|
progressMessage: (args) => `🕮 stat: ${path.basename(args.path)}`,
|
|
51
52
|
completionMessage: (args, result) => {
|
|
@@ -55,7 +56,7 @@ export function registerGetFileInfoTool(server, options = {}) {
|
|
|
55
56
|
const sc = result.structuredContent;
|
|
56
57
|
if (!sc.ok || !sc.info)
|
|
57
58
|
return `🕮 stat: ${name} • failed`;
|
|
58
|
-
return `🕮 stat: ${sc.info.name}
|
|
59
|
+
return `🕮 stat: ${sc.info.name} • ${sc.info.type}, ${formatBytes(sc.info.size)}`;
|
|
59
60
|
},
|
|
60
61
|
}));
|
|
61
62
|
}
|
|
@@ -18,6 +18,29 @@ function getExperimentalTaskRegistration(server) {
|
|
|
18
18
|
return undefined;
|
|
19
19
|
return tasks;
|
|
20
20
|
}
|
|
21
|
+
function hasTaskToolCapability(server) {
|
|
22
|
+
const maybeServer = server;
|
|
23
|
+
const serverRuntime = maybeServer.server;
|
|
24
|
+
const capabilityGetter = serverRuntime?.getCapabilities;
|
|
25
|
+
if (typeof capabilityGetter !== 'function') {
|
|
26
|
+
// Fallback for tests or custom wrappers that provide only registerTool/experimental.
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
const capabilities = capabilityGetter.call(serverRuntime);
|
|
30
|
+
if (!isRecord(capabilities))
|
|
31
|
+
return false;
|
|
32
|
+
const { tasks } = capabilities;
|
|
33
|
+
if (!isRecord(tasks))
|
|
34
|
+
return false;
|
|
35
|
+
const { requests } = tasks;
|
|
36
|
+
if (!isRecord(requests))
|
|
37
|
+
return false;
|
|
38
|
+
const { tools } = requests;
|
|
39
|
+
if (!isRecord(tools))
|
|
40
|
+
return false;
|
|
41
|
+
const { call } = tools;
|
|
42
|
+
return isRecord(call);
|
|
43
|
+
}
|
|
21
44
|
const RELATED_TASK_META_KEY = 'io.modelcontextprotocol/related-task';
|
|
22
45
|
const TASK_STATUS_NOTIFICATION_METHOD = 'notifications/tasks/status';
|
|
23
46
|
function isRequestTaskStore(value) {
|
|
@@ -101,6 +124,35 @@ function normalizeCallToolResult(value) {
|
|
|
101
124
|
return parsed.data;
|
|
102
125
|
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Stored task result is not a valid tool result.');
|
|
103
126
|
}
|
|
127
|
+
function getToolResultErrorCode(result) {
|
|
128
|
+
if (!isRecord(result) || result['isError'] !== true)
|
|
129
|
+
return undefined;
|
|
130
|
+
const structured = result['structuredContent'];
|
|
131
|
+
if (!isRecord(structured))
|
|
132
|
+
return undefined;
|
|
133
|
+
const { error } = structured;
|
|
134
|
+
if (!isRecord(error))
|
|
135
|
+
return undefined;
|
|
136
|
+
const { code } = error;
|
|
137
|
+
return typeof code === 'string' ? code : undefined;
|
|
138
|
+
}
|
|
139
|
+
function isCancelledToolResult(result) {
|
|
140
|
+
return getToolResultErrorCode(result) === ErrorCode.E_CANCELLED;
|
|
141
|
+
}
|
|
142
|
+
async function projectCancelledTaskStatus(taskStore, task) {
|
|
143
|
+
if (task.status !== 'failed')
|
|
144
|
+
return task;
|
|
145
|
+
try {
|
|
146
|
+
const result = await taskStore.getTaskResult(task.taskId);
|
|
147
|
+
if (isCancelledToolResult(result)) {
|
|
148
|
+
return { ...task, status: 'cancelled' };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Best effort only: task result may not be available yet.
|
|
153
|
+
}
|
|
154
|
+
return task;
|
|
155
|
+
}
|
|
104
156
|
function withRelatedTaskMeta(result, taskId) {
|
|
105
157
|
const existingMeta = isRecord(result['_meta']) ? result['_meta'] : {};
|
|
106
158
|
return {
|
|
@@ -129,7 +181,7 @@ async function notifyTaskStatusIfPossible(extra, taskStore, taskId) {
|
|
|
129
181
|
const notify = sendNotification;
|
|
130
182
|
try {
|
|
131
183
|
const task = await taskStore.getTask(taskId);
|
|
132
|
-
const normalized = normalizeGetTaskResult(task);
|
|
184
|
+
const normalized = await projectCancelledTaskStatus(taskStore, normalizeGetTaskResult(task));
|
|
133
185
|
await notify({
|
|
134
186
|
method: TASK_STATUS_NOTIFICATION_METHOD,
|
|
135
187
|
params: buildTaskStatusNotificationParams(normalized),
|
|
@@ -159,12 +211,6 @@ const TERMINAL_TASK_STATUSES = new Set([
|
|
|
159
211
|
'failed',
|
|
160
212
|
'cancelled',
|
|
161
213
|
]);
|
|
162
|
-
function isTerminalTaskStoreError(error) {
|
|
163
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
-
const normalized = message.toLowerCase();
|
|
165
|
-
return (normalized.includes('terminal status') ||
|
|
166
|
-
normalized.includes('task not found'));
|
|
167
|
-
}
|
|
168
214
|
async function isTaskAlreadyTerminal(taskStore, taskId) {
|
|
169
215
|
try {
|
|
170
216
|
const task = await taskStore.getTask(taskId);
|
|
@@ -183,8 +229,7 @@ async function tryStoreTaskResult(taskStore, taskId, status, result) {
|
|
|
183
229
|
await taskStore.storeTaskResult(taskId, status, resultWithTaskMeta);
|
|
184
230
|
}
|
|
185
231
|
catch (error) {
|
|
186
|
-
if (
|
|
187
|
-
(await isTaskAlreadyTerminal(taskStore, taskId)))
|
|
232
|
+
if (await isTaskAlreadyTerminal(taskStore, taskId))
|
|
188
233
|
return;
|
|
189
234
|
throw error;
|
|
190
235
|
}
|
|
@@ -213,6 +258,8 @@ async function runTaskInBackground(run, args, extra, taskStore, taskId) {
|
|
|
213
258
|
* `server.registerTool`.
|
|
214
259
|
*/
|
|
215
260
|
export function tryRegisterToolTask(server, toolName, toolDef, taskHandler, iconInfo) {
|
|
261
|
+
if (!hasTaskToolCapability(server))
|
|
262
|
+
return false;
|
|
216
263
|
const tasks = getExperimentalTaskRegistration(server);
|
|
217
264
|
if (!tasks?.registerToolTask)
|
|
218
265
|
return false;
|
|
@@ -248,7 +295,7 @@ export function createToolTaskHandler(run, options) {
|
|
|
248
295
|
const taskStore = getTaskStore(extra);
|
|
249
296
|
const taskId = getTaskId(extra);
|
|
250
297
|
const task = await taskStore.getTask(taskId);
|
|
251
|
-
return normalizeGetTaskResult(task);
|
|
298
|
+
return projectCancelledTaskStatus(taskStore, normalizeGetTaskResult(task));
|
|
252
299
|
});
|
|
253
300
|
const getTaskResult = (async (argsOrExtra, maybeExtra) => {
|
|
254
301
|
const extra = asTaskRequestExtra(maybeExtra ?? argsOrExtra);
|
package/dist/tools/tree.js
CHANGED
|
@@ -3,7 +3,7 @@ import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { formatTreeAscii, treeDirectory } from '../lib/file-operations/tree.js';
|
|
5
5
|
import { TreeInputSchema, TreeOutputSchema } from '../schemas.js';
|
|
6
|
-
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
6
|
+
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
const TREE_TOOL = {
|
|
9
9
|
title: 'Tree',
|
|
@@ -47,7 +47,8 @@ export function registerTreeTool(server, options = {}) {
|
|
|
47
47
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, targetPath),
|
|
48
48
|
});
|
|
49
49
|
};
|
|
50
|
-
const
|
|
50
|
+
const validatedHandler = withValidatedArgs(TreeInputSchema, handler);
|
|
51
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
51
52
|
guard: options.isInitialized,
|
|
52
53
|
progressMessage: (args) => {
|
|
53
54
|
if (args.path) {
|
|
@@ -55,6 +56,18 @@ export function registerTreeTool(server, options = {}) {
|
|
|
55
56
|
}
|
|
56
57
|
return '≣ tree';
|
|
57
58
|
},
|
|
59
|
+
completionMessage: (args, result) => {
|
|
60
|
+
const base = args.path ? path.basename(args.path) : '.';
|
|
61
|
+
if (result.isError)
|
|
62
|
+
return `≣ tree: ${base} • failed`;
|
|
63
|
+
const sc = result.structuredContent;
|
|
64
|
+
if (!sc.ok)
|
|
65
|
+
return `≣ tree: ${base} • failed`;
|
|
66
|
+
const count = sc.totalEntries ?? 0;
|
|
67
|
+
if (sc.truncated)
|
|
68
|
+
return `≣ tree: ${base} • ${count} entries [truncated]`;
|
|
69
|
+
return `≣ tree: ${base} • ${count} ${count === 1 ? 'entry' : 'entries'}`;
|
|
70
|
+
},
|
|
58
71
|
});
|
|
59
72
|
if (registerToolTaskIfAvailable(server, 'tree', TREE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
60
73
|
return;
|
package/dist/tools/write-file.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ErrorCode } from '../lib/errors.js';
|
|
|
4
4
|
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
5
5
|
import { validatePathForWrite } from '../lib/path-validation.js';
|
|
6
6
|
import { WriteFileInputSchema, WriteFileOutputSchema } from '../schemas.js';
|
|
7
|
-
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
const WRITE_FILE_TOOL = {
|
|
10
10
|
title: 'Write File',
|
|
@@ -34,7 +34,8 @@ export function registerWriteFileTool(server, options = {}) {
|
|
|
34
34
|
run: (signal) => handleWriteFile(args, signal),
|
|
35
35
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
|
|
36
36
|
});
|
|
37
|
-
const
|
|
37
|
+
const validatedHandler = withValidatedArgs(WriteFileInputSchema, handler);
|
|
38
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
38
39
|
guard: options.isInitialized,
|
|
39
40
|
progressMessage: (args) => `🛠 write: ${path.basename(args.path)} [${args.content.length} chars]`,
|
|
40
41
|
completionMessage: (args, result) => {
|