@j0hanz/filesystem-mcp 1.16.2 → 1.16.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 (69) hide show
  1. package/README.md +18 -14
  2. package/dist/assets/logo.svg +26 -11
  3. package/dist/cli.js +1 -1
  4. package/dist/completions.d.ts +1 -1
  5. package/dist/completions.js +1 -2
  6. package/dist/lib/constants.d.ts +2 -0
  7. package/dist/lib/constants.js +2 -3
  8. package/dist/lib/fs-helpers.d.ts +1 -1
  9. package/dist/lib/fs-helpers.js +0 -1
  10. package/dist/lib/logger.d.ts +3 -4
  11. package/dist/lib/logger.js +1 -1
  12. package/dist/lib/paths.d.ts +1 -1
  13. package/dist/prompts.d.ts +1 -1
  14. package/dist/prompts.js +11 -11
  15. package/dist/resources/workflows.js +12 -0
  16. package/dist/resources.d.ts +1 -1
  17. package/dist/resources.js +1 -1
  18. package/dist/schemas.d.ts +4 -4
  19. package/dist/server/bootstrap.d.ts +1 -10
  20. package/dist/server/bootstrap.js +165 -95
  21. package/dist/server/roots-manager.d.ts +3 -2
  22. package/dist/server/roots-manager.js +30 -4
  23. package/dist/server/task-store.d.ts +2 -3
  24. package/dist/server/task-store.js +1 -1
  25. package/dist/tools/apply-patch.d.ts +1 -1
  26. package/dist/tools/apply-patch.js +16 -12
  27. package/dist/tools/calculate-hash.d.ts +1 -1
  28. package/dist/tools/calculate-hash.js +8 -12
  29. package/dist/tools/contract.d.ts +5 -0
  30. package/dist/tools/create-directory.d.ts +1 -1
  31. package/dist/tools/create-directory.js +7 -10
  32. package/dist/tools/delete-file.d.ts +1 -1
  33. package/dist/tools/delete-file.js +12 -11
  34. package/dist/tools/diff-files.d.ts +1 -1
  35. package/dist/tools/diff-files.js +8 -11
  36. package/dist/tools/edit-file.d.ts +1 -1
  37. package/dist/tools/edit-file.js +12 -11
  38. package/dist/tools/icons.d.ts +15 -0
  39. package/dist/tools/icons.js +24 -0
  40. package/dist/tools/list-directory.d.ts +1 -1
  41. package/dist/tools/list-directory.js +7 -10
  42. package/dist/tools/move-file.d.ts +1 -1
  43. package/dist/tools/move-file.js +12 -11
  44. package/dist/tools/read-multiple.d.ts +1 -1
  45. package/dist/tools/read-multiple.js +8 -12
  46. package/dist/tools/read.d.ts +1 -1
  47. package/dist/tools/read.js +7 -10
  48. package/dist/tools/replace-in-files.d.ts +1 -1
  49. package/dist/tools/replace-in-files.js +11 -13
  50. package/dist/tools/roots.d.ts +1 -1
  51. package/dist/tools/roots.js +7 -10
  52. package/dist/tools/search-content.d.ts +1 -1
  53. package/dist/tools/search-content.js +8 -13
  54. package/dist/tools/search-files.d.ts +1 -1
  55. package/dist/tools/search-files.js +11 -16
  56. package/dist/tools/shared.d.ts +15 -12
  57. package/dist/tools/shared.js +84 -56
  58. package/dist/tools/stat-many.d.ts +1 -1
  59. package/dist/tools/stat-many.js +8 -12
  60. package/dist/tools/stat.d.ts +1 -1
  61. package/dist/tools/stat.js +7 -10
  62. package/dist/tools/task-support.d.ts +14 -12
  63. package/dist/tools/task-support.js +98 -92
  64. package/dist/tools/tree.d.ts +1 -1
  65. package/dist/tools/tree.js +11 -15
  66. package/dist/tools/write-file.d.ts +1 -1
  67. package/dist/tools/write-file.js +12 -11
  68. package/dist/tools.d.ts +1 -1
  69. package/package.json +6 -4
@@ -4,8 +4,9 @@ import { ErrorCode } from '../lib/errors.js';
4
4
  import { getFileInfo } from '../lib/file-operations/metadata.js';
5
5
  import { formatBytes, joinLines } from '../config.js';
6
6
  import { GetFileInfoInputSchema, GetFileInfoOutputSchema } from '../schemas.js';
7
- import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
- import { registerToolTaskIfAvailable } from './task-support.js';
7
+ import { FILE_READ_ICONS } from './icons.js';
8
+ import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, } from './shared.js';
9
+ import { registerStandardTool } from './task-support.js';
9
10
  export const GET_FILE_INFO_TOOL = {
10
11
  name: 'stat',
11
12
  title: 'Get File Info',
@@ -14,6 +15,7 @@ export const GET_FILE_INFO_TOOL = {
14
15
  inputSchema: GetFileInfoInputSchema,
15
16
  outputSchema: GetFileInfoOutputSchema,
16
17
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
18
+ icons: FILE_READ_ICONS,
17
19
  taskSupport: 'forbidden',
18
20
  };
19
21
  function formatFileInfoDetails(info) {
@@ -41,17 +43,16 @@ async function handleGetFileInfo(args, signal) {
41
43
  return buildToolResponse(formatFileInfoDetails(info), structured);
42
44
  }
43
45
  export function registerGetFileInfoTool(server, options = {}) {
44
- const handler = (args, extra) => executeToolWithDiagnostics({
46
+ const handler = (args, ctx) => executeToolWithDiagnostics({
45
47
  toolName: 'stat',
46
- extra,
48
+ ctx,
47
49
  outputSchema: GetFileInfoOutputSchema,
48
50
  timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
49
51
  context: { path: args.path },
50
52
  run: (signal) => handleGetFileInfo(args, signal),
51
53
  onError: (error) => buildToolErrorResponse(error, ErrorCode.NOT_FOUND, args.path),
52
54
  });
53
- const wrappedHandler = wrapToolHandler(handler, {
54
- guard: options.isInitialized,
55
+ registerStandardTool(server, GET_FILE_INFO_TOOL, handler, options, {
55
56
  progressMessage: (args) => `🕮 stat: ${basename(args.path)}`,
56
57
  completionMessage: (args, result) => {
57
58
  const name = basename(args.path);
@@ -63,8 +64,4 @@ export function registerGetFileInfoTool(server, options = {}) {
63
64
  return `🕮 stat: ${sc.info.name} • ${sc.info.type}, ${formatBytes(sc.info.size)}`;
64
65
  },
65
66
  });
66
- const validatedHandler = withValidatedArgs(GetFileInfoInputSchema, wrappedHandler);
67
- if (registerToolTaskIfAvailable(server, 'stat', GET_FILE_INFO_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
68
- return;
69
- server.registerTool('stat', withDefaultIcons({ ...GET_FILE_INFO_TOOL }, options.iconInfo), validatedHandler);
70
67
  }
@@ -1,22 +1,24 @@
1
- import type { ToolTaskHandler } from '@modelcontextprotocol/sdk/experimental/tasks/interfaces.js';
2
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import type { AnySchema, SchemaOutput, ShapeOutput, ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js';
4
- import type { RequestTaskStore } from '@modelcontextprotocol/sdk/shared/protocol.js';
5
- import { type IconInfo, type ToolExtra, type ToolResult } from './shared.js';
6
- type TaskToolExtra = ToolExtra & {
1
+ import { type McpServer, type RequestTaskStore, type StandardSchemaWithJSON, type ToolTaskHandler } from '@modelcontextprotocol/server';
2
+ import { type IconInfo, type ToolContext, type ToolContract, type ToolRegistrationOptions, type ToolResult } from './shared.js';
3
+ type TaskToolContext = ToolContext & {
7
4
  taskId?: string;
8
5
  taskStore?: RequestTaskStore;
9
- taskRequestedTtl?: number | null;
6
+ taskRequestedTtl?: number;
10
7
  };
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;
8
+ type ToolSchema = StandardSchemaWithJSON | undefined;
9
+ type ToolArgs<Args extends ToolSchema> = Args extends StandardSchemaWithJSON ? StandardSchemaWithJSON.InferOutput<Args> : undefined;
10
+ export declare function registerToolTaskIfAvailable<Args extends ToolSchema, Result>(server: McpServer, toolName: string, toolDef: object, run: (args: ToolArgs<Args>, ctx: TaskToolContext) => Promise<ToolResult<Result>>, iconInfo: IconInfo | undefined, guard?: () => boolean): boolean;
11
+ export declare function registerStandardTool<Args, Result extends Record<string, unknown>>(server: McpServer, toolDef: ToolContract, handler: (args: Args, ctx: ToolContext) => Promise<ToolResult<Result>>, options?: ToolRegistrationOptions, wrapOptions?: {
12
+ guard?: (() => boolean) | undefined;
13
+ progressMessage?: (args: Args) => string;
14
+ completionMessage?: (args: Args, result: ToolResult<Result>) => string | undefined;
15
+ }): void;
14
16
  interface TaskHandlerOptions {
15
17
  guard?: () => boolean;
16
18
  toolName?: string;
17
19
  cancelPollMs?: number;
18
20
  pollIntervalMs?: number;
19
21
  }
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>;
22
+ export declare function createToolTaskHandler<Result>(run: (args: undefined, ctx: TaskToolContext) => Promise<ToolResult<Result>>, options?: TaskHandlerOptions): ToolTaskHandler;
23
+ export declare function createToolTaskHandler<Args extends StandardSchemaWithJSON, Result>(run: (args: ToolArgs<Args>, ctx: TaskToolContext) => Promise<ToolResult<Result>>, options?: TaskHandlerOptions): ToolTaskHandler<Args>;
22
24
  export {};
@@ -1,4 +1,4 @@
1
- import { CallToolResultSchema, } from '@modelcontextprotocol/sdk/types.js';
1
+ import {} from '@modelcontextprotocol/server';
2
2
  import { AsyncLocalStorage } from 'node:async_hooks';
3
3
  import { channel } from 'node:diagnostics_channel';
4
4
  import { performance } from 'node:perf_hooks';
@@ -7,7 +7,7 @@ import { DEFAULT_TASK_TTL_MS, MAX_CONCURRENT_TASKS, MAX_TASK_TTL_MS, TASK_CANCEL
7
7
  import { ErrorCode, McpError } from '../lib/errors.js';
8
8
  import { Logger } from '../lib/logger.js';
9
9
  import { isRecord } from '../lib/utils.js';
10
- import { buildToolErrorResponse, maybeStripStructuredContentFromResult, withDefaultIcons, } from './shared.js';
10
+ import { buildToolErrorResponse, maybeStripStructuredContentFromResult, resolveToolTaskSupportLevel, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
11
11
  const taskContext = new AsyncLocalStorage();
12
12
  const TASK_DIAGNOSTICS_CHANNEL = channel('filesystem-mcp:tasks');
13
13
  function publishTaskDiagnostics(event) {
@@ -15,40 +15,10 @@ function publishTaskDiagnostics(event) {
15
15
  TASK_DIAGNOSTICS_CHANNEL.publish(event);
16
16
  }
17
17
  }
18
- function getDynamicProperty(target, key) {
19
- return Reflect.get(target, key);
20
- }
21
18
  // --- Type Guards & Helpers ---
22
- function isExperimentalTaskRegistration(value) {
23
- if (!isRecord(value))
24
- return false;
25
- const { registerToolTask } = value;
26
- return (registerToolTask === undefined || typeof registerToolTask === 'function');
27
- }
28
- function getExperimentalTaskRegistration(server) {
29
- const experimental = getDynamicProperty(server, 'experimental');
30
- if (!isRecord(experimental))
31
- return undefined;
32
- const { tasks } = experimental;
33
- if (!isExperimentalTaskRegistration(tasks))
34
- return undefined;
35
- return tasks;
36
- }
37
19
  function hasTaskToolCapability(server) {
38
- const serverRuntime = getDynamicProperty(server, 'server');
39
- if (!isRecord(serverRuntime))
40
- return true; // Assume capability if runtime structure is opaque
41
- const getCapabilities = getDynamicProperty(serverRuntime, 'getCapabilities');
42
- if (typeof getCapabilities !== 'function')
43
- return true;
44
- const capabilities = getCapabilities.call(serverRuntime);
45
- if (!isRecord(capabilities))
46
- return false;
47
- const { tasks } = capabilities;
48
- return (isRecord(tasks) &&
49
- isRecord(tasks.requests) &&
50
- isRecord(tasks.requests.tools) &&
51
- isRecord(tasks.requests.tools.call));
20
+ const capabilities = server.server.getCapabilities();
21
+ return capabilities.tasks?.requests?.tools?.call !== undefined;
52
22
  }
53
23
  const TASK_STATUS_NOTIFICATION_METHOD = 'notifications/tasks/status';
54
24
  const TASK_CREATED_NOTIFICATION_METHOD = 'notifications/tasks/created';
@@ -59,36 +29,51 @@ function isRequestTaskStore(value) {
59
29
  typeof value.storeTaskResult === 'function' &&
60
30
  typeof value.getTaskResult === 'function');
61
31
  }
62
- function isCreateTaskExtra(value) {
63
- return isRecord(value) && isRequestTaskStore(value.taskStore);
64
- }
65
- function isTaskExtra(value) {
66
- return (isCreateTaskExtra(value) &&
67
- typeof value.taskId === 'string' &&
68
- value.taskId.length > 0);
32
+ function hasTaskStoreContext(value) {
33
+ return isRecord(value.task) && isRequestTaskStore(value.task.store);
69
34
  }
70
- function asCreateTaskExtra(value) {
71
- if (!isCreateTaskExtra(value)) {
35
+ function asCreateTaskContext(value) {
36
+ if (!hasTaskStoreContext(value)) {
72
37
  throw new McpError(ErrorCode.INVALID_INPUT, 'Task store not configured.');
73
38
  }
74
39
  return value;
75
40
  }
76
- function asTaskRequestExtra(value) {
77
- if (!isTaskExtra(value)) {
41
+ function asTaskRequestContext(value) {
42
+ if (!hasTaskStoreContext(value) ||
43
+ !isRecord(value.task) ||
44
+ typeof value.task.id !== 'string' ||
45
+ value.task.id.length === 0) {
78
46
  throw new McpError(ErrorCode.INVALID_INPUT, 'Task id or store missing.');
79
47
  }
80
48
  return value;
81
49
  }
50
+ function toTaskToolContext(ctx) {
51
+ return {
52
+ signal: ctx.mcpReq.signal,
53
+ ...(ctx.mcpReq._meta
54
+ ? { _meta: ctx.mcpReq._meta }
55
+ : {}),
56
+ sendNotification: async (notification) => ctx.mcpReq.notify(notification),
57
+ ...(hasTaskStoreContext(ctx) ? { taskStore: ctx.task.store } : {}),
58
+ ...(typeof ctx.task.id === 'string' && ctx.task.id.length > 0
59
+ ? { taskId: ctx.task.id }
60
+ : {}),
61
+ ...(ctx.task.requestedTtl !== undefined
62
+ ? { taskRequestedTtl: ctx.task.requestedTtl }
63
+ : {}),
64
+ };
65
+ }
82
66
  const TASK_STATUSES = new Set([
67
+ 'submitted',
83
68
  'working',
84
69
  'input_required',
85
70
  'completed',
86
71
  'failed',
87
72
  'cancelled',
73
+ 'unknown',
88
74
  ]);
89
75
  function isTaskStatus(value) {
90
- return (typeof value === 'string' &&
91
- TASK_STATUSES.has(value));
76
+ return typeof value === 'string' && TASK_STATUSES.has(value);
92
77
  }
93
78
  function normalizeGetTaskResult(value) {
94
79
  if (!isRecord(value) || typeof value.taskId !== 'string') {
@@ -122,9 +107,11 @@ function normalizeGetTaskResult(value) {
122
107
  return normalized;
123
108
  }
124
109
  function normalizeCallToolResult(value) {
125
- const parsed = CallToolResultSchema.safeParse(value);
126
- if (parsed.success)
127
- return parsed.data;
110
+ if (isRecord(value) &&
111
+ Array.isArray(value.content) &&
112
+ value.content.every((entry) => isRecord(entry) && typeof entry.type === 'string')) {
113
+ return value;
114
+ }
128
115
  throw new McpError(ErrorCode.INVALID_INPUT, 'Invalid stored task result.');
129
116
  }
130
117
  function getToolResultErrorCode(result) {
@@ -194,8 +181,8 @@ function buildTaskStatusNotificationParams(task) {
194
181
  params.statusMessage = task.statusMessage;
195
182
  return params;
196
183
  }
197
- async function notifyTaskCreatedIfPossible(extra, taskId, toolName) {
198
- const { sendNotification } = extra;
184
+ async function notifyTaskCreatedIfPossible(ctx, taskId, toolName) {
185
+ const { sendNotification } = ctx;
199
186
  if (typeof sendNotification !== 'function')
200
187
  return;
201
188
  const notify = sendNotification;
@@ -204,7 +191,7 @@ async function notifyTaskCreatedIfPossible(extra, taskId, toolName) {
204
191
  method: TASK_CREATED_NOTIFICATION_METHOD,
205
192
  params: {
206
193
  _meta: {
207
- 'modelcontextprotocol.io/related-task': {
194
+ 'io.modelcontextprotocol/related-task': {
208
195
  taskId,
209
196
  },
210
197
  },
@@ -219,8 +206,8 @@ async function notifyTaskCreatedIfPossible(extra, taskId, toolName) {
219
206
  });
220
207
  }
221
208
  }
222
- async function notifyTaskStatusIfPossible(extra, taskStore, taskId, toolName) {
223
- const { sendNotification } = extra;
209
+ async function notifyTaskStatusIfPossible(ctx, taskStore, taskId, toolName) {
210
+ const { sendNotification } = ctx;
224
211
  if (typeof sendNotification !== 'function')
225
212
  return;
226
213
  const notify = sendNotification;
@@ -247,17 +234,17 @@ async function notifyTaskStatusIfPossible(extra, taskStore, taskId, toolName) {
247
234
  // Never fail task execution because status notifications are optional.
248
235
  }
249
236
  }
250
- function getTaskStore(extra) {
251
- if (!extra.taskStore) {
237
+ function getTaskStore(ctx) {
238
+ if (!ctx.taskStore) {
252
239
  throw new McpError(ErrorCode.INVALID_INPUT, 'Task store not configured.');
253
240
  }
254
- return extra.taskStore;
241
+ return ctx.taskStore;
255
242
  }
256
- function getTaskId(extra) {
257
- if (!extra.taskId) {
243
+ function getTaskId(ctx) {
244
+ if (!ctx.taskId) {
258
245
  throw new McpError(ErrorCode.INVALID_INPUT, 'Task id missing.');
259
246
  }
260
- return extra.taskId;
247
+ return ctx.taskId;
261
248
  }
262
249
  function isErrorResult(result) {
263
250
  return 'isError' in result && result.isError;
@@ -274,6 +261,7 @@ const TERMINAL_TASK_STATUSES = new Set([
274
261
  'completed',
275
262
  'failed',
276
263
  'cancelled',
264
+ 'unknown',
277
265
  ]);
278
266
  async function isTaskAlreadyTerminal(taskStore, taskId) {
279
267
  try {
@@ -300,16 +288,14 @@ async function countActiveTasks(taskStore) {
300
288
  for (const task of tasks) {
301
289
  if (!isRecord(task) || typeof task.status !== 'string')
302
290
  continue;
303
- if (task.status !== 'completed' &&
304
- task.status !== 'failed' &&
305
- task.status !== 'cancelled') {
291
+ if (!TERMINAL_TASK_STATUSES.has(task.status)) {
306
292
  active += 1;
307
293
  }
308
294
  }
309
295
  return active;
310
296
  }
311
297
  function resolveRequestedTaskTtl(requestedTtl) {
312
- if (requestedTtl == null)
298
+ if (requestedTtl === undefined)
313
299
  return DEFAULT_TASK_TTL_MS;
314
300
  if (!Number.isFinite(requestedTtl)) {
315
301
  throw new McpError(ErrorCode.INVALID_INPUT, 'Task TTL must be finite.');
@@ -342,11 +328,11 @@ async function isTaskCancelled(taskStore, taskId) {
342
328
  return false;
343
329
  }
344
330
  }
345
- async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName, cancelPollMs) {
331
+ async function runTaskInBackground(run, args, ctx, taskStore, taskId, toolName, cancelPollMs) {
346
332
  // Create a dedicated AbortController for background execution.
347
333
  // The original request signal is stale once createTask returns.
348
334
  const taskAbort = new AbortController();
349
- const taskExtra = { ...extra, signal: taskAbort.signal };
335
+ const taskExtra = { ...ctx, signal: taskAbort.signal };
350
336
  // Poll the task store for client-initiated cancellation.
351
337
  const cancelPoller = setInterval(() => {
352
338
  void isTaskCancelled(taskStore, taskId).then((cancelled) => {
@@ -381,7 +367,7 @@ async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName
381
367
  toolName,
382
368
  durationMs,
383
369
  });
384
- await notifyTaskStatusIfPossible(extra, taskStore, taskId, toolName);
370
+ await notifyTaskStatusIfPossible(ctx, taskStore, taskId, toolName);
385
371
  }
386
372
  catch (innerError) {
387
373
  Logger.error(format('Failed to store task result for task %s:', taskId), innerError);
@@ -393,7 +379,7 @@ async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName
393
379
  lastUpdatedAt: new Date().toISOString(),
394
380
  statusMessage: 'Internal system error while storing result',
395
381
  };
396
- const { sendNotification } = extra;
382
+ const { sendNotification } = ctx;
397
383
  if (typeof sendNotification === 'function') {
398
384
  try {
399
385
  await sendNotification({
@@ -416,17 +402,12 @@ async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName
416
402
  function tryRegisterToolTask(server, toolName, toolDef, taskHandler, iconInfo) {
417
403
  if (!hasTaskToolCapability(server))
418
404
  return false;
419
- const tasks = getExperimentalTaskRegistration(server);
420
- if (!tasks?.registerToolTask)
421
- return false;
422
405
  const def = toolDef;
423
406
  const existingExecution = def.execution ?? {};
424
- const taskSupport = def.taskSupport ??
425
- existingExecution.taskSupport ??
426
- 'forbidden';
427
- if (taskSupport === 'forbidden')
407
+ const taskSupport = resolveToolTaskSupportLevel(def.taskSupport, existingExecution.taskSupport);
408
+ if (!taskSupport || taskSupport === 'forbidden')
428
409
  return false;
429
- tasks.registerToolTask(toolName, withDefaultIcons({ ...toolDef, execution: { ...existingExecution, taskSupport } }, iconInfo), taskHandler);
410
+ server.experimental.tasks.registerToolTask(toolName, withDefaultIcons({ ...toolDef, execution: { ...existingExecution, taskSupport } }, iconInfo), taskHandler);
430
411
  return true;
431
412
  }
432
413
  export function registerToolTaskIfAvailable(server, toolName, toolDef, run, iconInfo, guard) {
@@ -436,19 +417,37 @@ export function registerToolTaskIfAvailable(server, toolName, toolDef, run, icon
436
417
  };
437
418
  return tryRegisterToolTask(server, toolName, toolDef, createToolTaskHandler(run, taskOptions), iconInfo);
438
419
  }
420
+ export function registerStandardTool(server, toolDef, handler, options = {}, wrapOptions = {}) {
421
+ const wrappedHandler = wrapToolHandler(handler, {
422
+ guard: options.isInitialized,
423
+ ...wrapOptions,
424
+ });
425
+ const validatedHandler = withValidatedArgs(toolDef.inputSchema, wrappedHandler);
426
+ if (registerToolTaskIfAvailable(server, toolDef.name, toolDef, validatedHandler, options.iconInfo, options.isInitialized)) {
427
+ return;
428
+ }
429
+ server.registerTool(toolDef.name, withDefaultIcons({ ...toolDef }, options.iconInfo), validatedHandler);
430
+ }
439
431
  export function createToolTaskHandler(run, options) {
440
- const createTask = (async (argsOrExtra, maybeExtra) => {
441
- const extra = asCreateTaskExtra(maybeExtra ?? argsOrExtra);
442
- const args = (maybeExtra ? argsOrExtra : undefined);
432
+ const createTask = (async (...params) => {
433
+ let args;
434
+ let serverCtx;
435
+ if (params.length === 1) {
436
+ serverCtx = params[0];
437
+ }
438
+ else {
439
+ [args, serverCtx] = params;
440
+ }
441
+ const ctx = toTaskToolContext(asCreateTaskContext(serverCtx));
443
442
  if (options?.guard && !options.guard()) {
444
443
  throw new McpError(ErrorCode.INVALID_INPUT, 'Client not initialized; wait for notifications/initialized');
445
444
  }
446
- const taskStore = getTaskStore(extra);
445
+ const taskStore = getTaskStore(ctx);
447
446
  if ((await countActiveTasks(taskStore)) >= MAX_CONCURRENT_TASKS) {
448
447
  throw new McpError(ErrorCode.INVALID_INPUT, `Too many active tasks (limit: ${String(MAX_CONCURRENT_TASKS)}).`);
449
448
  }
450
449
  const task = await taskStore.createTask({
451
- ttl: resolveRequestedTaskTtl(extra.taskRequestedTtl),
450
+ ttl: resolveRequestedTaskTtl(ctx.taskRequestedTtl),
452
451
  pollInterval: options?.pollIntervalMs ?? TASK_POLL_INTERVAL_MS,
453
452
  });
454
453
  const toolLabel = options?.toolName ?? 'tool';
@@ -465,26 +464,33 @@ export function createToolTaskHandler(run, options) {
465
464
  ...(options?.toolName ? { toolName: options.toolName } : {}),
466
465
  });
467
466
  const taskExtra = {
468
- ...extra,
467
+ ...ctx,
469
468
  taskStore,
470
469
  taskId: task.taskId,
471
470
  };
472
471
  void notifyTaskCreatedIfPossible(taskExtra, task.taskId, options?.toolName);
473
472
  void notifyTaskStatusIfPossible(taskExtra, taskStore, task.taskId, options?.toolName);
474
473
  void runTaskInBackground(run, args, taskExtra, taskStore, task.taskId, options?.toolName, options?.cancelPollMs);
475
- return { task };
474
+ return {
475
+ task,
476
+ _meta: {
477
+ 'io.modelcontextprotocol/model-immediate-response': `${toolLabel} task created — poll tasks/get for progress.`,
478
+ },
479
+ };
476
480
  });
477
- const getTask = (async (argsOrExtra, maybeExtra) => {
478
- const extra = asTaskRequestExtra(maybeExtra ?? argsOrExtra);
479
- const taskStore = getTaskStore(extra);
480
- const taskId = getTaskId(extra);
481
+ const getTask = (async (...params) => {
482
+ const serverCtx = params.length === 1 ? params[0] : params[1];
483
+ const ctx = toTaskToolContext(asTaskRequestContext(serverCtx));
484
+ const taskStore = getTaskStore(ctx);
485
+ const taskId = getTaskId(ctx);
481
486
  const task = await taskStore.getTask(taskId);
482
487
  return projectCancelledTaskStatus(taskStore, normalizeGetTaskResult(task));
483
488
  });
484
- const getTaskResult = (async (argsOrExtra, maybeExtra) => {
485
- const extra = asTaskRequestExtra(maybeExtra ?? argsOrExtra);
486
- const taskStore = getTaskStore(extra);
487
- const taskId = getTaskId(extra);
489
+ const getTaskResult = (async (...params) => {
490
+ const serverCtx = params.length === 1 ? params[0] : params[1];
491
+ const ctx = toTaskToolContext(asTaskRequestContext(serverCtx));
492
+ const taskStore = getTaskStore(ctx);
493
+ const taskId = getTaskId(ctx);
488
494
  const result = await taskStore.getTaskResult(taskId);
489
495
  return attachRelatedTaskMeta(normalizeCallToolResult(result), taskId);
490
496
  });
@@ -1,4 +1,4 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import type { McpServer } from '@modelcontextprotocol/server';
2
2
  import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
3
  export declare const TREE_TOOL: ToolContract;
4
4
  export declare function registerTreeTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -3,8 +3,9 @@ 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/metadata.js';
5
5
  import { TreeInputSchema, TreeOutputSchema } from '../schemas.js';
6
- import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
7
- import { registerToolTaskIfAvailable } from './task-support.js';
6
+ import { DIRECTORY_ICONS } from './icons.js';
7
+ import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, } from './shared.js';
8
+ import { registerStandardTool } from './task-support.js';
8
9
  export const TREE_TOOL = {
9
10
  name: 'tree',
10
11
  title: 'Tree',
@@ -13,6 +14,7 @@ export const TREE_TOOL = {
13
14
  inputSchema: TreeInputSchema,
14
15
  outputSchema: TreeOutputSchema,
15
16
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
17
+ icons: DIRECTORY_ICONS,
16
18
  taskSupport: 'optional',
17
19
  };
18
20
  async function handleTree(args, signal, onProgress) {
@@ -39,11 +41,11 @@ async function handleTree(args, signal, onProgress) {
39
41
  return buildToolResponse(text, structured);
40
42
  }
41
43
  export function registerTreeTool(server, options = {}) {
42
- const handler = (args, extra) => {
44
+ const handler = (args, ctx) => {
43
45
  const targetPath = args.path ?? '.';
44
46
  return executeToolWithDiagnostics({
45
47
  toolName: 'tree',
46
- extra,
48
+ ctx,
47
49
  outputSchema: TreeOutputSchema,
48
50
  timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
49
51
  context: { path: targetPath },
@@ -51,12 +53,12 @@ export function registerTreeTool(server, options = {}) {
51
53
  const context = args.path ? basename(args.path) : '.';
52
54
  let progressCursor = 0;
53
55
  const knownTotal = args.maxEntries;
54
- notifyProgress(extra, {
56
+ notifyProgress(ctx, {
55
57
  current: 0,
56
58
  total: knownTotal,
57
59
  message: `≣ tree: ${context}`,
58
60
  });
59
- const baseReporter = createProgressReporter(extra);
61
+ const baseReporter = createProgressReporter(ctx);
60
62
  const onProgress = (progress) => {
61
63
  const { current } = progress;
62
64
  if (current > progressCursor)
@@ -76,7 +78,7 @@ export function registerTreeTool(server, options = {}) {
76
78
  if (truncated)
77
79
  suffix += ' [truncated]';
78
80
  const finalCurrent = Math.max(count, progressCursor + 1);
79
- notifyProgress(extra, {
81
+ notifyProgress(ctx, {
80
82
  current: finalCurrent,
81
83
  total: finalCurrent,
82
84
  message: `≣ tree: ${context} • ${suffix}`,
@@ -85,7 +87,7 @@ export function registerTreeTool(server, options = {}) {
85
87
  }
86
88
  catch (error) {
87
89
  const finalCurrent = Math.max(progressCursor + 1, 1);
88
- notifyProgress(extra, {
90
+ notifyProgress(ctx, {
89
91
  current: finalCurrent,
90
92
  total: finalCurrent,
91
93
  message: `≣ tree: ${context} • failed`,
@@ -96,11 +98,5 @@ export function registerTreeTool(server, options = {}) {
96
98
  onError: (error) => buildToolErrorResponse(error, ErrorCode.NOT_DIRECTORY, targetPath),
97
99
  });
98
100
  };
99
- const wrappedHandler = wrapToolHandler(handler, {
100
- guard: options.isInitialized,
101
- });
102
- const validatedHandler = withValidatedArgs(TreeInputSchema, wrappedHandler);
103
- if (registerToolTaskIfAvailable(server, 'tree', TREE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
104
- return;
105
- server.registerTool('tree', withDefaultIcons({ ...TREE_TOOL }, options.iconInfo), validatedHandler);
101
+ registerStandardTool(server, TREE_TOOL, handler, options);
106
102
  }
@@ -1,4 +1,4 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import type { McpServer } from '@modelcontextprotocol/server';
2
2
  import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
3
  export declare const WRITE_FILE_TOOL: ToolContract;
4
4
  export declare function registerWriteFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -7,8 +7,9 @@ import { Logger } from '../lib/logger.js';
7
7
  import { validatePathForWrite } from '../lib/paths.js';
8
8
  import { formatBytes } from '../config.js';
9
9
  import { WriteFileInputSchema, WriteFileOutputSchema } from '../schemas.js';
10
- import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
11
- import { registerToolTaskIfAvailable } from './task-support.js';
10
+ import { FILE_EDIT_ICONS } from './icons.js';
11
+ import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, } from './shared.js';
12
+ import { registerStandardTool } from './task-support.js';
12
13
  export const WRITE_FILE_TOOL = {
13
14
  name: 'write',
14
15
  title: 'Write File',
@@ -16,6 +17,7 @@ export const WRITE_FILE_TOOL = {
16
17
  inputSchema: WriteFileInputSchema,
17
18
  outputSchema: WriteFileOutputSchema,
18
19
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
20
+ icons: FILE_EDIT_ICONS,
19
21
  taskSupport: 'forbidden',
20
22
  };
21
23
  async function handleWriteFile(args, signal) {
@@ -32,17 +34,20 @@ async function handleWriteFile(args, signal) {
32
34
  });
33
35
  }
34
36
  export function registerWriteFileTool(server, options = {}) {
35
- const handler = (args, extra) => executeToolWithDiagnostics({
37
+ const handler = (args, ctx) => executeToolWithDiagnostics({
36
38
  toolName: 'write',
37
- extra,
39
+ ctx,
38
40
  outputSchema: WriteFileOutputSchema,
39
41
  timedSignal: {},
40
42
  context: { path: args.path },
41
- run: (signal) => handleWriteFile(args, signal),
43
+ run: async (signal) => {
44
+ const result = await handleWriteFile(args, signal);
45
+ void ctx.log?.('info', `write: ${args.path} (${String(result.structuredContent.bytesWritten ?? 0)} bytes)`);
46
+ return result;
47
+ },
42
48
  onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
43
49
  });
44
- const wrappedHandler = wrapToolHandler(handler, {
45
- guard: options.isInitialized,
50
+ registerStandardTool(server, WRITE_FILE_TOOL, handler, options, {
46
51
  progressMessage: (args) => `🛠 write: ${basename(args.path)}`,
47
52
  completionMessage: (args, result) => {
48
53
  const name = basename(args.path);
@@ -52,8 +57,4 @@ export function registerWriteFileTool(server, options = {}) {
52
57
  return `🛠 write: ${name} • ${formatBytes(sc.bytesWritten ?? 0)}`;
53
58
  },
54
59
  });
55
- const validatedHandler = withValidatedArgs(WriteFileInputSchema, wrappedHandler);
56
- if (registerToolTaskIfAvailable(server, 'write', WRITE_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
57
- return;
58
- server.registerTool('write', withDefaultIcons({ ...WRITE_FILE_TOOL }, options.iconInfo), validatedHandler);
59
60
  }
package/dist/tools.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import type { McpServer } from '@modelcontextprotocol/server';
2
2
  import { type ToolContract } from './tools/contract.js';
3
3
  import type { ToolRegistrationOptions } from './tools/shared.js';
4
4
  export declare const ALL_TOOLS: ToolContract[];
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.16.2",
3
+ "version": "1.16.3",
4
4
  "mcpName": "io.github.j0hanz/filesystem-mcp",
5
- "description": "A local filesystem MCP server that lets LLMs and AI agents read, write, search, diff, patch, and manage files safely and efficiently. Built for reliable, structured, and controlled filesystem interaction.",
5
+ "description": "Secure filesystem MCP server for reading, writing, searching, diffing, and patching files.",
6
6
  "author": "j0hanz",
7
7
  "license": "MIT",
8
8
  "repository": {
@@ -68,14 +68,16 @@
68
68
  "security",
69
69
  "server",
70
70
  "stdio",
71
- "sse",
72
71
  "typescript",
73
72
  "nodejs",
74
73
  "llm",
75
74
  "cli"
76
75
  ],
77
76
  "dependencies": {
78
- "@modelcontextprotocol/sdk": "^1.29.0",
77
+ "@cfworker/json-schema": "^4.1.1",
78
+ "@modelcontextprotocol/client": "^2.0.0-alpha.2",
79
+ "@modelcontextprotocol/node": "^2.0.0-alpha.2",
80
+ "@modelcontextprotocol/server": "^2.0.0-alpha.2",
79
81
  "commander": "^14.0.3",
80
82
  "diff": "^8.0.4",
81
83
  "ignore": "^7.0.5",