@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.
Files changed (46) hide show
  1. package/README.md +11 -0
  2. package/dist/cli.js +2 -4
  3. package/dist/completions.js +15 -6
  4. package/dist/lib/file-operations/glob-engine.js +3 -9
  5. package/dist/lib/file-operations/read-multiple-files.js +4 -23
  6. package/dist/lib/file-operations/search-content.js +11 -18
  7. package/dist/lib/observability.js +4 -4
  8. package/dist/lib/path-validation.js +22 -9
  9. package/dist/pkg-info.d.ts +6 -0
  10. package/dist/pkg-info.js +9 -0
  11. package/dist/schemas.d.ts +28 -28
  12. package/dist/schemas.js +26 -26
  13. package/dist/server/bootstrap.d.ts +4 -0
  14. package/dist/server/bootstrap.js +117 -0
  15. package/dist/server/capabilities.d.ts +10 -0
  16. package/dist/server/capabilities.js +40 -0
  17. package/dist/server/logging.d.ts +7 -0
  18. package/dist/server/logging.js +41 -0
  19. package/dist/server/roots-manager.d.ts +19 -0
  20. package/dist/server/roots-manager.js +173 -0
  21. package/dist/server/types.d.ts +4 -0
  22. package/dist/server/types.js +1 -0
  23. package/dist/server.d.ts +2 -8
  24. package/dist/server.js +1 -317
  25. package/dist/tools/apply-patch.js +3 -2
  26. package/dist/tools/calculate-hash.js +3 -2
  27. package/dist/tools/create-directory.js +3 -2
  28. package/dist/tools/delete-file.js +9 -2
  29. package/dist/tools/diff-files.js +3 -2
  30. package/dist/tools/edit-file.js +5 -4
  31. package/dist/tools/list-directory.js +13 -2
  32. package/dist/tools/move-file.js +11 -3
  33. package/dist/tools/read-multiple.js +6 -5
  34. package/dist/tools/read.js +3 -2
  35. package/dist/tools/replace-in-files.js +3 -2
  36. package/dist/tools/roots.js +12 -2
  37. package/dist/tools/search-content.js +10 -15
  38. package/dist/tools/search-files.js +13 -9
  39. package/dist/tools/shared.d.ts +3 -1
  40. package/dist/tools/shared.js +19 -1
  41. package/dist/tools/stat-many.js +6 -5
  42. package/dist/tools/stat.js +4 -3
  43. package/dist/tools/task-support.js +57 -10
  44. package/dist/tools/tree.js +15 -2
  45. package/dist/tools/write-file.js +3 -2
  46. 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 normalizedArgs = SearchContentInputSchema.parse(args);
205
- const scope = normalizedArgs.filePattern;
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} ${current} ${fileWord} scanned`,
214
+ message: `🔎︎ grep: ${pattern} ${current} ${fileWord} scanned`,
221
215
  });
222
216
  };
223
217
  try {
224
- const result = await handleSearchContent(normalizedArgs, signal, options.resourceStore, progressWithMessage);
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 wrappedHandler = wrapToolHandler(handler, {
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 scope = args.path ?? '.';
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: ${pattern} in ${scope}`,
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} ${current} ${fileWord} scanned`,
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 ${scope}`;
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: ${pattern} • ${suffix}`,
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: ${pattern} in ${scope} • failed`,
147
+ message: `🔎︎ find: ${context} • failed`,
145
148
  });
146
149
  throw error;
147
150
  }
148
151
  },
149
- onError: (error) => buildToolErrorResponse(error, ErrorCode.E_INVALID_PATTERN, args.path),
152
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
150
153
  });
151
154
  const { isInitialized } = options;
152
- const wrappedHandler = wrapToolHandler(handler, {
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))
@@ -1,5 +1,5 @@
1
1
  import type { ContentBlock, Icon, ProgressNotificationParams } from '@modelcontextprotocol/sdk/types.js';
2
- import type { z } from 'zod';
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;
@@ -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 that are
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
  }
@@ -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 wrappedHandler = wrapToolHandler(handler, {
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: (_args, result) => {
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;
@@ -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
- server.registerTool('stat', withDefaultIcons({ ...GET_FILE_INFO_TOOL }, options.iconInfo), wrapToolHandler(handler, {
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} [${sc.info.type}, ${formatBytes(sc.info.size)}]`;
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 (isTerminalTaskStoreError(error) ||
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);
@@ -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 wrappedHandler = wrapToolHandler(handler, {
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;
@@ -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 wrappedHandler = wrapToolHandler(handler, {
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "mcpName": "io.github.j0hanz/filesystem-mcp",
5
5
  "description": "MCP Server that enables LLMs to interact with the local filesystem.",
6
6
  "type": "module",