@j0hanz/filesystem-mcp 1.17.1 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import {} from '@modelcontextprotocol/server';
1
+ import { isTerminal, RELATED_TASK_META_KEY, } 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,8 +7,25 @@ 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, resolveToolTaskSupportLevel, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
10
+ import { buildToolErrorResponse, maybeStripStructuredContentFromResult, resolveToolTaskSupportLevel, toToolContext, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
11
11
  const taskContext = new AsyncLocalStorage();
12
+ /**
13
+ * Report an intermediate 'working' status update for the current task.
14
+ * No-op when called outside of a task context.
15
+ */
16
+ export async function reportTaskStatus(statusMessage) {
17
+ const store = taskContext.getStore();
18
+ if (!store)
19
+ return;
20
+ const { taskId, taskStore, ctx, toolName } = store;
21
+ try {
22
+ await taskStore.updateTaskStatus(taskId, 'working', statusMessage);
23
+ await notifyTaskStatusIfPossible(ctx, taskStore, taskId, toolName);
24
+ }
25
+ catch {
26
+ // Best-effort: never fail tool execution for status updates.
27
+ }
28
+ }
12
29
  const TASK_DIAGNOSTICS_CHANNEL = channel('filesystem-mcp:tasks');
13
30
  function publishTaskDiagnostics(event) {
14
31
  if (TASK_DIAGNOSTICS_CHANNEL.hasSubscribers) {
@@ -17,44 +34,33 @@ function publishTaskDiagnostics(event) {
17
34
  }
18
35
  // --- Type Guards & Helpers ---
19
36
  function hasTaskToolCapability(server) {
20
- const capabilities = server.server.getCapabilities();
21
- return capabilities.tasks?.requests?.tools?.call !== undefined;
37
+ try {
38
+ const capabilities = server.server.getCapabilities();
39
+ return capabilities.tasks?.requests?.tools?.call !== undefined;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
22
44
  }
23
45
  const TASK_STATUS_NOTIFICATION_METHOD = 'notifications/tasks/status';
24
46
  const TASK_CREATED_NOTIFICATION_METHOD = 'notifications/tasks/created';
25
- function isRequestTaskStore(value) {
26
- return (isRecord(value) &&
27
- typeof value.createTask === 'function' &&
28
- typeof value.getTask === 'function' &&
29
- typeof value.storeTaskResult === 'function' &&
30
- typeof value.getTaskResult === 'function');
31
- }
32
- function hasTaskStoreContext(value) {
33
- return isRecord(value.task) && isRequestTaskStore(value.task.store);
34
- }
35
- function asCreateTaskContext(value) {
36
- if (!hasTaskStoreContext(value)) {
47
+ function assertCreateTaskContext(value) {
48
+ if (!isRecord(value.task) || typeof value.task.store !== 'object') {
37
49
  throw new McpError(ErrorCode.INVALID_INPUT, 'Task store not configured.');
38
50
  }
39
- return value;
40
51
  }
41
- function asTaskRequestContext(value) {
42
- if (!hasTaskStoreContext(value) ||
43
- !isRecord(value.task) ||
52
+ function assertTaskRequestContext(value) {
53
+ if (!isRecord(value.task) ||
54
+ typeof value.task.store !== 'object' ||
44
55
  typeof value.task.id !== 'string' ||
45
56
  value.task.id.length === 0) {
46
57
  throw new McpError(ErrorCode.INVALID_INPUT, 'Task id or store missing.');
47
58
  }
48
- return value;
49
59
  }
50
60
  function toTaskToolContext(ctx) {
51
61
  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 } : {}),
62
+ ...toToolContext(ctx),
63
+ taskStore: ctx.task.store,
58
64
  ...(typeof ctx.task.id === 'string' && ctx.task.id.length > 0
59
65
  ? { taskId: ctx.task.id }
60
66
  : {}),
@@ -63,56 +69,11 @@ function toTaskToolContext(ctx) {
63
69
  : {}),
64
70
  };
65
71
  }
66
- const TASK_STATUSES = new Set([
67
- 'submitted',
68
- 'working',
69
- 'input_required',
70
- 'completed',
71
- 'failed',
72
- 'cancelled',
73
- 'unknown',
74
- ]);
75
- function isTaskStatus(value) {
76
- return typeof value === 'string' && TASK_STATUSES.has(value);
77
- }
78
- function normalizeGetTaskResult(value) {
79
- if (!isRecord(value) || typeof value.taskId !== 'string') {
80
- throw new McpError(ErrorCode.INVALID_INPUT, 'Invalid task object.');
81
- }
82
- const status = isTaskStatus(value.status) ? value.status : undefined;
83
- if (!status) {
84
- throw new McpError(ErrorCode.INVALID_INPUT, 'Invalid task status.');
85
- }
86
- const createdAt = typeof value.createdAt === 'string'
87
- ? value.createdAt
88
- : new Date().toISOString();
89
- const lastUpdatedAt = typeof value.lastUpdatedAt === 'string' ? value.lastUpdatedAt : createdAt;
90
- const ttl = typeof value.ttl === 'number' ? value.ttl : null;
91
- const normalized = {
92
- taskId: value.taskId,
93
- status,
94
- ttl,
95
- createdAt,
96
- lastUpdatedAt,
97
- };
98
- if (typeof value.pollInterval === 'number') {
99
- normalized.pollInterval = value.pollInterval;
100
- }
101
- if (typeof value.statusMessage === 'string') {
102
- normalized.statusMessage = value.statusMessage;
103
- }
104
- if (isRecord(value._meta)) {
105
- normalized._meta = value._meta;
106
- }
107
- return normalized;
72
+ function toGetTaskResult(task) {
73
+ return task;
108
74
  }
109
- function normalizeCallToolResult(value) {
110
- if (isRecord(value) &&
111
- Array.isArray(value.content) &&
112
- value.content.every((entry) => isRecord(entry) && typeof entry.type === 'string')) {
113
- return value;
114
- }
115
- throw new McpError(ErrorCode.INVALID_INPUT, 'Invalid stored task result.');
75
+ function toCallToolResult(value) {
76
+ return value;
116
77
  }
117
78
  function getToolResultErrorCode(result) {
118
79
  if (!isRecord(result) || result.isError !== true)
@@ -149,7 +110,7 @@ function attachRelatedTaskMeta(result, taskId) {
149
110
  ...result,
150
111
  _meta: {
151
112
  ...existingMeta,
152
- 'io.modelcontextprotocol/related-task': { taskId },
113
+ [RELATED_TASK_META_KEY]: { taskId },
153
114
  },
154
115
  };
155
116
  }
@@ -182,16 +143,14 @@ function buildTaskStatusNotificationParams(task) {
182
143
  return params;
183
144
  }
184
145
  async function notifyTaskCreatedIfPossible(ctx, taskId, toolName) {
185
- const { sendNotification } = ctx;
186
- if (typeof sendNotification !== 'function')
146
+ if (!ctx.sendNotification)
187
147
  return;
188
- const notify = sendNotification;
189
148
  try {
190
- await notify({
149
+ await ctx.sendNotification({
191
150
  method: TASK_CREATED_NOTIFICATION_METHOD,
192
151
  params: {
193
152
  _meta: {
194
- 'io.modelcontextprotocol/related-task': {
153
+ [RELATED_TASK_META_KEY]: {
195
154
  taskId,
196
155
  },
197
156
  },
@@ -207,14 +166,12 @@ async function notifyTaskCreatedIfPossible(ctx, taskId, toolName) {
207
166
  }
208
167
  }
209
168
  async function notifyTaskStatusIfPossible(ctx, taskStore, taskId, toolName) {
210
- const { sendNotification } = ctx;
211
- if (typeof sendNotification !== 'function')
169
+ if (!ctx.sendNotification)
212
170
  return;
213
- const notify = sendNotification;
214
171
  try {
215
172
  const task = await taskStore.getTask(taskId);
216
- const normalized = await projectCancelledTaskStatus(taskStore, normalizeGetTaskResult(task));
217
- await notify({
173
+ const normalized = await projectCancelledTaskStatus(taskStore, toGetTaskResult(task));
174
+ await ctx.sendNotification({
218
175
  method: TASK_STATUS_NOTIFICATION_METHOD,
219
176
  params: buildTaskStatusNotificationParams(normalized),
220
177
  });
@@ -257,12 +214,6 @@ function withoutStructuredContent(result) {
257
214
  delete stripped['structuredContent'];
258
215
  return stripped;
259
216
  }
260
- const TERMINAL_TASK_STATUSES = new Set([
261
- 'completed',
262
- 'failed',
263
- 'cancelled',
264
- 'unknown',
265
- ]);
266
217
  const taskCreationLocks = new WeakMap();
267
218
  async function acquireTaskCreationLock(taskStore) {
268
219
  const previous = taskCreationLocks.get(taskStore) ?? Promise.resolve();
@@ -282,7 +233,7 @@ async function isTaskAlreadyTerminal(taskStore, taskId) {
282
233
  if (!isRecord(task))
283
234
  return false;
284
235
  const { status } = task;
285
- return typeof status === 'string' && TERMINAL_TASK_STATUSES.has(status);
236
+ return typeof status === 'string' && isTerminal(status);
286
237
  }
287
238
  catch {
288
239
  return false;
@@ -301,7 +252,7 @@ async function countActiveTasks(taskStore) {
301
252
  for (const task of tasks) {
302
253
  if (!isRecord(task) || typeof task.status !== 'string')
303
254
  continue;
304
- if (!TERMINAL_TASK_STATUSES.has(task.status)) {
255
+ if (!isTerminal(task.status)) {
305
256
  active += 1;
306
257
  }
307
258
  }
@@ -357,7 +308,7 @@ async function runTaskInBackground(run, args, ctx, taskStore, taskId, toolName,
357
308
  let taskStatuses;
358
309
  let result;
359
310
  try {
360
- const rawResult = await taskContext.run({ taskId, toolName, startTime: start }, () => run(args, taskExtra));
311
+ const rawResult = await taskContext.run({ taskId, toolName, startTime: start, taskStore, ctx: taskExtra }, () => run(args, taskExtra));
361
312
  taskStatuses = resolveTaskResultStatuses(rawResult);
362
313
  result = isErrorResult(rawResult)
363
314
  ? withoutStructuredContent(rawResult)
@@ -392,10 +343,9 @@ async function runTaskInBackground(run, args, ctx, taskStore, taskId, toolName,
392
343
  lastUpdatedAt: new Date().toISOString(),
393
344
  statusMessage: 'Internal system error while storing result',
394
345
  };
395
- const { sendNotification } = ctx;
396
- if (typeof sendNotification === 'function') {
346
+ if (ctx.sendNotification) {
397
347
  try {
398
- await sendNotification({
348
+ await ctx.sendNotification({
399
349
  method: TASK_STATUS_NOTIFICATION_METHOD,
400
350
  params: buildTaskStatusNotificationParams(syntheticTask),
401
351
  });
@@ -451,7 +401,8 @@ export function createToolTaskHandler(run, options) {
451
401
  else {
452
402
  [args, serverCtx] = params;
453
403
  }
454
- const ctx = toTaskToolContext(asCreateTaskContext(serverCtx));
404
+ assertCreateTaskContext(serverCtx);
405
+ const ctx = toTaskToolContext(serverCtx);
455
406
  if (options?.guard && !options.guard()) {
456
407
  throw new McpError(ErrorCode.INVALID_INPUT, 'Client not initialized; wait for notifications/initialized');
457
408
  }
@@ -493,26 +444,25 @@ export function createToolTaskHandler(run, options) {
493
444
  void runTaskInBackground(run, args, taskExtra, taskStore, task.taskId, options?.toolName, options?.cancelPollMs);
494
445
  return {
495
446
  task,
496
- _meta: {
497
- 'io.modelcontextprotocol/model-immediate-response': `${toolLabel} task created — poll tasks/get for progress.`,
498
- },
499
447
  };
500
448
  });
501
449
  const getTask = (async (...params) => {
502
450
  const serverCtx = params.length === 1 ? params[0] : params[1];
503
- const ctx = toTaskToolContext(asTaskRequestContext(serverCtx));
451
+ assertTaskRequestContext(serverCtx);
452
+ const ctx = toTaskToolContext(serverCtx);
504
453
  const taskStore = getTaskStore(ctx);
505
454
  const taskId = getTaskId(ctx);
506
455
  const task = await taskStore.getTask(taskId);
507
- return projectCancelledTaskStatus(taskStore, normalizeGetTaskResult(task));
456
+ return projectCancelledTaskStatus(taskStore, toGetTaskResult(task));
508
457
  });
509
458
  const getTaskResult = (async (...params) => {
510
459
  const serverCtx = params.length === 1 ? params[0] : params[1];
511
- const ctx = toTaskToolContext(asTaskRequestContext(serverCtx));
460
+ assertTaskRequestContext(serverCtx);
461
+ const ctx = toTaskToolContext(serverCtx);
512
462
  const taskStore = getTaskStore(ctx);
513
463
  const taskId = getTaskId(ctx);
514
464
  const result = await taskStore.getTaskResult(taskId);
515
- return attachRelatedTaskMeta(normalizeCallToolResult(result), taskId);
465
+ return attachRelatedTaskMeta(toCallToolResult(result), taskId);
516
466
  });
517
467
  return {
518
468
  createTask,
@@ -1,6 +1,6 @@
1
1
  import { basename } from 'node:path';
2
2
  import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
3
- import { ErrorCode } from '../lib/errors.js';
3
+ import { classifyError, ErrorCode } from '../lib/errors.js';
4
4
  import { formatTreeAscii, treeDirectory, } from '../lib/file-operations/metadata.js';
5
5
  import { TreeInputSchema, TreeOutputSchema } from '../schemas.js';
6
6
  import { DIRECTORY_ICONS } from './icons.js';
@@ -56,7 +56,7 @@ export function registerTreeTool(server, options = {}) {
56
56
  notifyProgress(ctx, {
57
57
  current: 0,
58
58
  total: knownTotal,
59
- message: `≣ tree: ${context}`,
59
+ message: `${TREE_TOOL.title}: ${context}`,
60
60
  });
61
61
  const baseReporter = createProgressReporter(ctx);
62
62
  const onProgress = (progress) => {
@@ -66,7 +66,7 @@ export function registerTreeTool(server, options = {}) {
66
66
  baseReporter({
67
67
  current,
68
68
  total: knownTotal,
69
- message: `≣ tree: ${context} [${current} entries]`,
69
+ message: `${TREE_TOOL.title}: ${context} [${current} entries]`,
70
70
  });
71
71
  };
72
72
  try {
@@ -81,7 +81,7 @@ export function registerTreeTool(server, options = {}) {
81
81
  notifyProgress(ctx, {
82
82
  current: finalCurrent,
83
83
  total: finalCurrent,
84
- message: `≣ tree: ${context} • ${suffix}`,
84
+ message: `${TREE_TOOL.title}: ${context} • ${suffix}`,
85
85
  });
86
86
  return result;
87
87
  }
@@ -90,7 +90,7 @@ export function registerTreeTool(server, options = {}) {
90
90
  notifyProgress(ctx, {
91
91
  current: finalCurrent,
92
92
  total: finalCurrent,
93
- message: `≣ tree: ${context} • failed`,
93
+ message: `${TREE_TOOL.title}: ${context} • ${classifyError(error)}`,
94
94
  });
95
95
  throw error;
96
96
  }
@@ -48,13 +48,13 @@ export function registerWriteFileTool(server, options = {}) {
48
48
  onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
49
49
  });
50
50
  registerStandardTool(server, WRITE_FILE_TOOL, handler, options, {
51
- progressMessage: (args) => `🛠 write: ${basename(args.path)}`,
51
+ progressMessage: (args) => `${WRITE_FILE_TOOL.title}: ${basename(args.path)}`,
52
52
  completionMessage: (args, result) => {
53
53
  const name = basename(args.path);
54
54
  if (result.isError)
55
- return `🛠 write: ${name} • failed`;
55
+ return `${WRITE_FILE_TOOL.title}: ${name} • ${result.errorCode}`;
56
56
  const sc = result.structuredContent;
57
- return `🛠 write: ${name} • ${formatBytes(sc.bytesWritten ?? 0)}`;
57
+ return `${WRITE_FILE_TOOL.title}: ${name} • ${formatBytes(sc.bytesWritten ?? 0)}`;
58
58
  },
59
59
  });
60
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.17.1",
3
+ "version": "1.18.0",
4
4
  "mcpName": "io.github.j0hanz/filesystem-mcp",
5
5
  "description": "Secure filesystem MCP server for reading, writing, searching, diffing, and patching files.",
6
6
  "author": "j0hanz",