@j0hanz/filesystem-mcp 1.1.2 → 1.2.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.
Files changed (62) hide show
  1. package/README.md +514 -188
  2. package/dist/cli.js +29 -12
  3. package/dist/completions.js +50 -24
  4. package/dist/config.d.ts +3 -2
  5. package/dist/config.js +1 -1
  6. package/dist/index.js +14 -12
  7. package/dist/instructions.md +109 -97
  8. package/dist/lib/constants.js +25 -14
  9. package/dist/lib/errors.js +11 -6
  10. package/dist/lib/file-operations/common.d.ts +4 -0
  11. package/dist/lib/file-operations/common.js +9 -0
  12. package/dist/lib/file-operations/file-info.js +22 -10
  13. package/dist/lib/file-operations/gitignore.js +14 -11
  14. package/dist/lib/file-operations/glob-engine.d.ts +1 -0
  15. package/dist/lib/file-operations/glob-engine.js +46 -33
  16. package/dist/lib/file-operations/list-directory.js +31 -35
  17. package/dist/lib/file-operations/read-multiple-files.js +70 -62
  18. package/dist/lib/file-operations/search-content.js +83 -64
  19. package/dist/lib/file-operations/search-files.js +32 -30
  20. package/dist/lib/file-operations/search-worker.js +22 -12
  21. package/dist/lib/file-operations/tree.js +43 -34
  22. package/dist/lib/fs-helpers.js +61 -124
  23. package/dist/lib/observability.js +29 -28
  24. package/dist/lib/path-format.d.ts +1 -0
  25. package/dist/lib/path-format.js +7 -0
  26. package/dist/lib/path-policy.js +22 -20
  27. package/dist/lib/path-validation.js +13 -7
  28. package/dist/lib/resource-store.d.ts +2 -0
  29. package/dist/lib/resource-store.js +26 -5
  30. package/dist/lib/type-guards.d.ts +1 -0
  31. package/dist/lib/type-guards.js +3 -0
  32. package/dist/prompts.d.ts +1 -5
  33. package/dist/prompts.js +9 -16
  34. package/dist/resources.d.ts +1 -5
  35. package/dist/resources.js +12 -26
  36. package/dist/schemas.d.ts +213 -30
  37. package/dist/schemas.js +52 -90
  38. package/dist/server.js +85 -44
  39. package/dist/tools/apply-patch.js +24 -24
  40. package/dist/tools/calculate-hash.js +42 -45
  41. package/dist/tools/create-directory.js +18 -21
  42. package/dist/tools/delete-file.js +36 -39
  43. package/dist/tools/diff-files.js +16 -21
  44. package/dist/tools/edit-file.js +16 -20
  45. package/dist/tools/list-directory.js +25 -25
  46. package/dist/tools/move-file.js +18 -21
  47. package/dist/tools/read-multiple.js +56 -68
  48. package/dist/tools/read.js +27 -32
  49. package/dist/tools/replace-in-files.js +28 -35
  50. package/dist/tools/roots.js +9 -10
  51. package/dist/tools/search-content.js +74 -74
  52. package/dist/tools/search-files.js +45 -52
  53. package/dist/tools/shared.d.ts +44 -6
  54. package/dist/tools/shared.js +86 -64
  55. package/dist/tools/stat-many.js +45 -68
  56. package/dist/tools/stat.js +11 -39
  57. package/dist/tools/task-support.d.ts +9 -1
  58. package/dist/tools/task-support.js +86 -81
  59. package/dist/tools/tree.js +13 -30
  60. package/dist/tools/write-file.js +18 -21
  61. package/dist/tools.js +23 -18
  62. package/package.json +6 -7
@@ -3,41 +3,14 @@ import { formatBytes, joinLines } from '../config.js';
3
3
  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
- import { createTimedAbortSignal } from '../lib/fs-helpers.js';
7
- import { withToolDiagnostics } from '../lib/observability.js';
8
- import { GetFileInfoInputSchema, GetFileInfoOutputSchema } from '../schemas.js';
9
- import { buildToolErrorResponse, buildToolResponse, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.js';
6
+ import { GetFileInfoInputSchema, } from '../schemas.js';
7
+ import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
10
8
  const GET_FILE_INFO_TOOL = {
11
9
  title: 'Get File Info',
12
10
  description: 'Get metadata (size, modified time, permissions, mime type) for a file or directory.',
13
11
  inputSchema: GetFileInfoInputSchema,
14
- outputSchema: GetFileInfoOutputSchema,
15
- annotations: {
16
- readOnlyHint: true,
17
- idempotentHint: true,
18
- openWorldHint: false,
19
- },
12
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
20
13
  };
21
- function buildFileInfoPayload(info) {
22
- return {
23
- name: info.name,
24
- path: info.path,
25
- type: info.type,
26
- size: info.size,
27
- ...(info.tokenEstimate !== undefined
28
- ? { tokenEstimate: info.tokenEstimate }
29
- : {}),
30
- created: info.created.toISOString(),
31
- modified: info.modified.toISOString(),
32
- accessed: info.accessed.toISOString(),
33
- permissions: info.permissions,
34
- isHidden: info.isHidden,
35
- ...(info.mimeType !== undefined ? { mimeType: info.mimeType } : {}),
36
- ...(info.symlinkTarget !== undefined
37
- ? { symlinkTarget: info.symlinkTarget }
38
- : {}),
39
- };
40
- }
41
14
  function formatFileInfoDetails(info) {
42
15
  const lines = [
43
16
  `${info.name} (${info.type})`,
@@ -63,15 +36,14 @@ async function handleGetFileInfo(args, signal) {
63
36
  return buildToolResponse(formatFileInfoDetails(info), structured);
64
37
  }
65
38
  export function registerGetFileInfoTool(server, options = {}) {
66
- const handler = (args, extra) => withToolDiagnostics('stat', () => withToolErrorHandling(async () => {
67
- const { signal, cleanup } = createTimedAbortSignal(extra.signal, DEFAULT_SEARCH_TIMEOUT_MS);
68
- try {
69
- return await handleGetFileInfo(args, signal);
70
- }
71
- finally {
72
- cleanup();
73
- }
74
- }, (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, args.path)), { path: args.path });
39
+ const handler = (args, extra) => executeToolWithDiagnostics({
40
+ toolName: 'stat',
41
+ extra,
42
+ timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
43
+ context: { path: args.path },
44
+ run: (signal) => handleGetFileInfo(args, signal),
45
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, args.path),
46
+ });
75
47
  server.registerTool('stat', withDefaultIcons({ ...GET_FILE_INFO_TOOL }, options.iconInfo), wrapToolHandler(handler, {
76
48
  guard: options.isInitialized,
77
49
  progressMessage: (args) => `🕮 stat: ${path.basename(args.path)}`,
@@ -1,13 +1,21 @@
1
1
  import type { ToolTaskHandler } from '@modelcontextprotocol/sdk/experimental/tasks/interfaces.js';
2
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
3
  import type { AnySchema, SchemaOutput, ShapeOutput, ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js';
3
4
  import type { RequestTaskStore } from '@modelcontextprotocol/sdk/shared/protocol.js';
4
- import type { ToolExtra, ToolResult } from './shared.js';
5
+ import type { IconInfo, ToolExtra, ToolResult } from './shared.js';
5
6
  type TaskToolExtra = ToolExtra & {
6
7
  taskId?: string;
7
8
  taskStore?: RequestTaskStore;
8
9
  taskRequestedTtl?: number | null;
9
10
  };
10
11
  type ToolArgs<Args extends ZodRawShapeCompat | AnySchema | undefined> = Args extends ZodRawShapeCompat ? ShapeOutput<Args> : Args extends AnySchema ? SchemaOutput<Args> : undefined;
12
+ /**
13
+ * Registers a tool preferring task-capable registration when available, and
14
+ * returns `true`. Returns `false` so the caller can fall through to standard
15
+ * `server.registerTool`.
16
+ */
17
+ export declare function tryRegisterToolTask<Args extends ZodRawShapeCompat | AnySchema | undefined>(server: McpServer, toolName: string, toolDef: object, taskHandler: ToolTaskHandler<Args>, iconInfo: IconInfo | undefined): boolean;
18
+ export declare function registerToolTaskIfAvailable<Args extends ZodRawShapeCompat | AnySchema | undefined, Result>(server: McpServer, toolName: string, toolDef: object, run: (args: ToolArgs<Args>, extra: TaskToolExtra) => Promise<ToolResult<Result>>, iconInfo: IconInfo | undefined, guard?: () => boolean): boolean;
11
19
  export declare function createToolTaskHandler<Result>(run: (args: undefined, extra: TaskToolExtra) => Promise<ToolResult<Result>>, options?: {
12
20
  guard?: () => boolean;
13
21
  }): ToolTaskHandler;
@@ -1,11 +1,25 @@
1
1
  import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
2
2
  import { ErrorCode, McpError } from '../lib/errors.js';
3
- import { buildToolErrorResponse } from './shared.js';
3
+ import { isRecord } from '../lib/type-guards.js';
4
+ import { buildToolErrorResponse, withDefaultIcons } from './shared.js';
5
+ function isExperimentalTaskRegistration(value) {
6
+ if (!value || typeof value !== 'object')
7
+ return false;
8
+ const { registerToolTask } = value;
9
+ return (registerToolTask === undefined || typeof registerToolTask === 'function');
10
+ }
11
+ function getExperimentalTaskRegistration(server) {
12
+ const serverWithExperimental = server;
13
+ const { experimental } = serverWithExperimental;
14
+ if (!experimental || typeof experimental !== 'object')
15
+ return undefined;
16
+ const { tasks } = experimental;
17
+ if (!isExperimentalTaskRegistration(tasks))
18
+ return undefined;
19
+ return tasks;
20
+ }
4
21
  const RELATED_TASK_META_KEY = 'io.modelcontextprotocol/related-task';
5
22
  const TASK_STATUS_NOTIFICATION_METHOD = 'notifications/tasks/status';
6
- function isRecord(value) {
7
- return value !== null && typeof value === 'object';
8
- }
9
23
  function isRequestTaskStore(value) {
10
24
  if (!isRecord(value))
11
25
  return false;
@@ -41,26 +55,24 @@ const TASK_STATUSES = new Set([
41
55
  'failed',
42
56
  'cancelled',
43
57
  ]);
58
+ function isTaskStatus(value) {
59
+ return (typeof value === 'string' &&
60
+ TASK_STATUSES.has(value));
61
+ }
44
62
  function parseTaskStatus(value) {
45
- if (value === 'working' ||
46
- value === 'input_required' ||
47
- value === 'completed' ||
48
- value === 'failed' ||
49
- value === 'cancelled') {
50
- return value;
51
- }
52
- return undefined;
63
+ return isTaskStatus(value) ? value : undefined;
53
64
  }
54
65
  function normalizeGetTaskResult(value) {
55
66
  if (!isRecord(value) || typeof value['taskId'] !== 'string') {
56
67
  throw new McpError(ErrorCode.E_INVALID_INPUT, 'Invalid task object.');
57
68
  }
58
69
  const status = parseTaskStatus(value['status']);
59
- if (!status || !TASK_STATUSES.has(status)) {
70
+ if (!status) {
60
71
  throw new McpError(ErrorCode.E_INVALID_INPUT, 'Invalid task status.');
61
72
  }
62
- const now = new Date().toISOString();
63
- const createdAt = typeof value['createdAt'] === 'string' ? value['createdAt'] : now;
73
+ const createdAt = typeof value['createdAt'] === 'string'
74
+ ? value['createdAt']
75
+ : new Date().toISOString();
64
76
  const lastUpdatedAt = typeof value['lastUpdatedAt'] === 'string'
65
77
  ? value['lastUpdatedAt']
66
78
  : createdAt;
@@ -90,51 +102,35 @@ function normalizeCallToolResult(value) {
90
102
  throw new McpError(ErrorCode.E_INVALID_INPUT, 'Stored task result is not a valid tool result.');
91
103
  }
92
104
  function withRelatedTaskMeta(result, taskId) {
93
- if (!isRecord(result)) {
94
- return {
95
- _meta: {
96
- [RELATED_TASK_META_KEY]: { taskId },
97
- },
98
- };
99
- }
100
105
  const existingMeta = isRecord(result['_meta']) ? result['_meta'] : {};
101
106
  return {
102
107
  ...result,
103
- _meta: {
104
- ...existingMeta,
105
- [RELATED_TASK_META_KEY]: { taskId },
106
- },
108
+ _meta: { ...existingMeta, [RELATED_TASK_META_KEY]: { taskId } },
107
109
  };
108
110
  }
109
- function getTaskStatusNotificationSender(extra) {
110
- const candidate = extra.sendNotification;
111
- return typeof candidate === 'function'
112
- ? candidate
113
- : undefined;
114
- }
115
111
  function buildTaskStatusNotificationParams(task) {
116
- return {
112
+ const params = {
117
113
  taskId: task.taskId,
118
114
  status: task.status,
119
115
  ttl: task.ttl,
120
116
  createdAt: task.createdAt,
121
117
  lastUpdatedAt: task.lastUpdatedAt,
122
- ...(task.pollInterval !== undefined
123
- ? { pollInterval: task.pollInterval }
124
- : {}),
125
- ...(task.statusMessage !== undefined
126
- ? { statusMessage: task.statusMessage }
127
- : {}),
128
118
  };
119
+ if (task.pollInterval !== undefined)
120
+ params.pollInterval = task.pollInterval;
121
+ if (task.statusMessage !== undefined)
122
+ params.statusMessage = task.statusMessage;
123
+ return params;
129
124
  }
130
125
  async function notifyTaskStatusIfPossible(extra, taskStore, taskId) {
131
- const sendNotification = getTaskStatusNotificationSender(extra);
132
- if (!sendNotification)
126
+ const { sendNotification } = extra;
127
+ if (typeof sendNotification !== 'function')
133
128
  return;
129
+ const notify = sendNotification;
134
130
  try {
135
131
  const task = await taskStore.getTask(taskId);
136
132
  const normalized = normalizeGetTaskResult(task);
137
- await sendNotification({
133
+ await notify({
138
134
  method: TASK_STATUS_NOTIFICATION_METHOD,
139
135
  params: buildTaskStatusNotificationParams(normalized),
140
136
  });
@@ -158,49 +154,75 @@ function getTaskId(extra) {
158
154
  function isErrorResult(result) {
159
155
  return 'isError' in result && result.isError === true;
160
156
  }
161
- const TERMINAL_TASK_STATUSES = new Set(['completed', 'failed', 'cancelled']);
162
- function getTaskStatus(task) {
163
- if (!isRecord(task))
164
- return undefined;
165
- const { status } = task;
166
- return typeof status === 'string' ? status : undefined;
167
- }
168
- function isTerminalTaskStatus(status) {
169
- if (!status)
170
- return false;
171
- return TERMINAL_TASK_STATUSES.has(status);
172
- }
157
+ const TERMINAL_TASK_STATUSES = new Set([
158
+ 'completed',
159
+ 'failed',
160
+ 'cancelled',
161
+ ]);
173
162
  function isTerminalTaskStoreError(error) {
174
163
  const message = error instanceof Error ? error.message : String(error);
175
164
  const normalized = message.toLowerCase();
176
165
  return (normalized.includes('terminal status') ||
177
166
  normalized.includes('task not found'));
178
167
  }
179
- async function getCurrentTaskStatus(taskStore, taskId) {
168
+ async function isTaskAlreadyTerminal(taskStore, taskId) {
180
169
  try {
181
170
  const task = await taskStore.getTask(taskId);
182
- return getTaskStatus(task);
171
+ if (!isRecord(task))
172
+ return false;
173
+ const { status } = task;
174
+ return typeof status === 'string' && TERMINAL_TASK_STATUSES.has(status);
183
175
  }
184
176
  catch {
185
- return undefined;
177
+ return false;
186
178
  }
187
179
  }
188
180
  async function tryStoreTaskResult(taskStore, taskId, status, result) {
189
181
  const resultWithTaskMeta = withRelatedTaskMeta(result, taskId);
190
- const beforeStatus = await getCurrentTaskStatus(taskStore, taskId);
191
- if (isTerminalTaskStatus(beforeStatus))
192
- return;
193
182
  try {
194
183
  await taskStore.storeTaskResult(taskId, status, resultWithTaskMeta);
195
184
  }
196
185
  catch (error) {
197
- const afterStatus = await getCurrentTaskStatus(taskStore, taskId);
198
- if (isTerminalTaskStatus(afterStatus) || isTerminalTaskStoreError(error)) {
186
+ if (isTerminalTaskStoreError(error) ||
187
+ (await isTaskAlreadyTerminal(taskStore, taskId)))
199
188
  return;
200
- }
201
189
  throw error;
202
190
  }
203
191
  }
192
+ async function runTaskInBackground(run, args, extra, taskStore, taskId) {
193
+ try {
194
+ const result = await run(args, extra);
195
+ const status = isErrorResult(result) ? 'failed' : 'completed';
196
+ await tryStoreTaskResult(taskStore, taskId, status, result);
197
+ await notifyTaskStatusIfPossible(extra, taskStore, taskId);
198
+ }
199
+ catch (error) {
200
+ const fallback = buildToolErrorResponse(error, ErrorCode.E_UNKNOWN);
201
+ try {
202
+ await tryStoreTaskResult(taskStore, taskId, 'failed', fallback);
203
+ await notifyTaskStatusIfPossible(extra, taskStore, taskId);
204
+ }
205
+ catch (innerError) {
206
+ console.error(`Failed to store task failure result for task ${taskId}:`, innerError);
207
+ }
208
+ }
209
+ }
210
+ /**
211
+ * Registers a tool preferring task-capable registration when available, and
212
+ * returns `true`. Returns `false` so the caller can fall through to standard
213
+ * `server.registerTool`.
214
+ */
215
+ export function tryRegisterToolTask(server, toolName, toolDef, taskHandler, iconInfo) {
216
+ const tasks = getExperimentalTaskRegistration(server);
217
+ if (!tasks?.registerToolTask)
218
+ return false;
219
+ tasks.registerToolTask(toolName, withDefaultIcons({ ...toolDef, execution: { taskSupport: 'optional' } }, iconInfo), taskHandler);
220
+ return true;
221
+ }
222
+ export function registerToolTaskIfAvailable(server, toolName, toolDef, run, iconInfo, guard) {
223
+ const taskOptions = guard ? { guard } : undefined;
224
+ return tryRegisterToolTask(server, toolName, toolDef, createToolTaskHandler(run, taskOptions), iconInfo);
225
+ }
204
226
  export function createToolTaskHandler(run, options) {
205
227
  const createTask = (async (argsOrExtra, maybeExtra) => {
206
228
  const extra = asCreateTaskExtra(maybeExtra ?? argsOrExtra);
@@ -218,24 +240,7 @@ export function createToolTaskHandler(run, options) {
218
240
  taskId: task.taskId,
219
241
  };
220
242
  void notifyTaskStatusIfPossible(taskExtra, taskStore, task.taskId);
221
- void (async () => {
222
- try {
223
- const result = await run(args, taskExtra);
224
- const status = isErrorResult(result) ? 'failed' : 'completed';
225
- await tryStoreTaskResult(taskStore, task.taskId, status, result);
226
- await notifyTaskStatusIfPossible(taskExtra, taskStore, task.taskId);
227
- }
228
- catch (error) {
229
- const fallback = buildToolErrorResponse(error, ErrorCode.E_UNKNOWN);
230
- try {
231
- await tryStoreTaskResult(taskStore, task.taskId, 'failed', fallback);
232
- await notifyTaskStatusIfPossible(taskExtra, taskStore, task.taskId);
233
- }
234
- catch {
235
- // Swallow to avoid unhandled rejections from background task writes.
236
- }
237
- }
238
- })();
243
+ void runTaskInBackground(run, args, taskExtra, taskStore, task.taskId);
239
244
  return { task };
240
245
  });
241
246
  const getTask = (async (argsOrExtra, maybeExtra) => {
@@ -2,23 +2,16 @@ import * as path from 'node:path';
2
2
  import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
3
3
  import { ErrorCode } from '../lib/errors.js';
4
4
  import { formatTreeAscii, treeDirectory } from '../lib/file-operations/tree.js';
5
- import { createTimedAbortSignal } from '../lib/fs-helpers.js';
6
- import { withToolDiagnostics } from '../lib/observability.js';
7
- import { TreeInputSchema, TreeOutputSchema } from '../schemas.js';
8
- import { buildToolErrorResponse, buildToolResponse, getExperimentalTaskRegistration, resolvePathOrRoot, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.js';
9
- import { createToolTaskHandler } from './task-support.js';
5
+ import { TreeInputSchema } from '../schemas.js';
6
+ import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
7
+ import { registerToolTaskIfAvailable } from './task-support.js';
10
8
  const TREE_TOOL = {
11
9
  title: 'Tree',
12
10
  description: 'Render a directory tree (bounded recursion). ' +
13
11
  'Returns an ASCII tree for quick scanning and a structured JSON tree for programmatic use. ' +
14
12
  'Note: maxDepth=0 returns only the root node with empty children array.',
15
13
  inputSchema: TreeInputSchema,
16
- outputSchema: TreeOutputSchema,
17
- annotations: {
18
- readOnlyHint: true,
19
- idempotentHint: true,
20
- openWorldHint: false,
21
- },
14
+ annotations: READ_ONLY_TOOL_ANNOTATIONS,
22
15
  };
23
16
  async function handleTree(args, signal) {
24
17
  const basePath = resolvePathOrRoot(args.path);
@@ -44,15 +37,14 @@ async function handleTree(args, signal) {
44
37
  export function registerTreeTool(server, options = {}) {
45
38
  const handler = (args, extra) => {
46
39
  const targetPath = args.path ?? '.';
47
- return withToolDiagnostics('tree', () => withToolErrorHandling(async () => {
48
- const { signal, cleanup } = createTimedAbortSignal(extra.signal, DEFAULT_SEARCH_TIMEOUT_MS);
49
- try {
50
- return await handleTree(args, signal);
51
- }
52
- finally {
53
- cleanup();
54
- }
55
- }, (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, targetPath)), { path: targetPath });
40
+ return executeToolWithDiagnostics({
41
+ toolName: 'tree',
42
+ extra,
43
+ timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
44
+ context: { path: targetPath },
45
+ run: (signal) => handleTree(args, signal),
46
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, targetPath),
47
+ });
56
48
  };
57
49
  const wrappedHandler = wrapToolHandler(handler, {
58
50
  guard: options.isInitialized,
@@ -63,16 +55,7 @@ export function registerTreeTool(server, options = {}) {
63
55
  return '≣ tree';
64
56
  },
65
57
  });
66
- const taskOptions = options.isInitialized
67
- ? { guard: options.isInitialized }
68
- : undefined;
69
- const tasks = getExperimentalTaskRegistration(server);
70
- if (tasks?.registerToolTask) {
71
- tasks.registerToolTask('tree', withDefaultIcons({
72
- ...TREE_TOOL,
73
- execution: { taskSupport: 'optional' },
74
- }, options.iconInfo), createToolTaskHandler(wrappedHandler, taskOptions));
58
+ if (registerToolTaskIfAvailable(server, 'tree', TREE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
75
59
  return;
76
- }
77
60
  server.registerTool('tree', withDefaultIcons({ ...TREE_TOOL }, options.iconInfo), wrappedHandler);
78
61
  }
@@ -1,21 +1,16 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { ErrorCode } from '../lib/errors.js';
4
- import { atomicWriteFile, createTimedAbortSignal, withAbort, } from '../lib/fs-helpers.js';
5
- import { withToolDiagnostics } from '../lib/observability.js';
4
+ import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
6
5
  import { validatePathForWrite } from '../lib/path-validation.js';
7
- import { WriteFileInputSchema, WriteFileOutputSchema } from '../schemas.js';
8
- import { buildToolErrorResponse, buildToolResponse, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.js';
6
+ import { WriteFileInputSchema, } from '../schemas.js';
7
+ import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
8
+ import { registerToolTaskIfAvailable } from './task-support.js';
9
9
  const WRITE_FILE_TOOL = {
10
10
  title: 'Write File',
11
11
  description: 'Write content to a file. Creates the file if it does not exist.',
12
12
  inputSchema: WriteFileInputSchema,
13
- outputSchema: WriteFileOutputSchema,
14
- annotations: {
15
- readOnlyHint: false,
16
- destructiveHint: true,
17
- openWorldHint: false,
18
- },
13
+ annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
19
14
  };
20
15
  async function handleWriteFile(args, signal) {
21
16
  const validPath = await validatePathForWrite(args.path, signal);
@@ -30,17 +25,19 @@ async function handleWriteFile(args, signal) {
30
25
  });
31
26
  }
32
27
  export function registerWriteFileTool(server, options = {}) {
33
- const handler = (args, extra) => withToolDiagnostics('write', () => withToolErrorHandling(async () => {
34
- const { signal, cleanup } = createTimedAbortSignal(extra.signal);
35
- try {
36
- return await handleWriteFile(args, signal);
37
- }
38
- finally {
39
- cleanup();
40
- }
41
- }, (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path)), { path: args.path });
42
- server.registerTool('write', withDefaultIcons({ ...WRITE_FILE_TOOL }, options.iconInfo), wrapToolHandler(handler, {
28
+ const handler = (args, extra) => executeToolWithDiagnostics({
29
+ toolName: 'write',
30
+ extra,
31
+ timedSignal: {},
32
+ context: { path: args.path },
33
+ run: (signal) => handleWriteFile(args, signal),
34
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
35
+ });
36
+ const wrappedHandler = wrapToolHandler(handler, {
43
37
  guard: options.isInitialized,
44
38
  progressMessage: (args) => `🛠 write: ${path.basename(args.path)}`,
45
- }));
39
+ });
40
+ if (registerToolTaskIfAvailable(server, 'write', WRITE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
41
+ return;
42
+ server.registerTool('write', withDefaultIcons({ ...WRITE_FILE_TOOL }, options.iconInfo), wrappedHandler);
46
43
  }
package/dist/tools.js CHANGED
@@ -17,23 +17,28 @@ import { registerGetFileInfoTool } from './tools/stat.js';
17
17
  import { registerTreeTool } from './tools/tree.js';
18
18
  import { registerWriteFileTool } from './tools/write-file.js';
19
19
  export { buildToolErrorResponse, buildToolResponse } from './tools/shared.js';
20
+ const TOOL_REGISTRARS = [
21
+ registerListAllowedDirectoriesTool,
22
+ registerListDirectoryTool,
23
+ registerSearchFilesTool,
24
+ registerTreeTool,
25
+ registerReadFileTool,
26
+ registerReadMultipleFilesTool,
27
+ registerGetFileInfoTool,
28
+ registerGetMultipleFileInfoTool,
29
+ registerSearchContentTool,
30
+ registerCreateDirectoryTool,
31
+ registerWriteFileTool,
32
+ registerEditFileTool,
33
+ registerMoveFileTool,
34
+ registerDeleteFileTool,
35
+ registerCalculateHashTool,
36
+ registerDiffFilesTool,
37
+ registerApplyPatchTool,
38
+ registerSearchAndReplaceTool,
39
+ ];
20
40
  export function registerAllTools(server, options = {}) {
21
- registerListAllowedDirectoriesTool(server, options);
22
- registerListDirectoryTool(server, options);
23
- registerSearchFilesTool(server, options);
24
- registerTreeTool(server, options);
25
- registerReadFileTool(server, options);
26
- registerReadMultipleFilesTool(server, options);
27
- registerGetFileInfoTool(server, options);
28
- registerGetMultipleFileInfoTool(server, options);
29
- registerSearchContentTool(server, options);
30
- registerCreateDirectoryTool(server, options);
31
- registerWriteFileTool(server, options);
32
- registerEditFileTool(server, options);
33
- registerMoveFileTool(server, options);
34
- registerDeleteFileTool(server, options);
35
- registerCalculateHashTool(server, options);
36
- registerDiffFilesTool(server, options);
37
- registerApplyPatchTool(server, options);
38
- registerSearchAndReplaceTool(server, options);
41
+ for (const registerTool of TOOL_REGISTRARS) {
42
+ registerTool(server, options);
43
+ }
39
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
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",
@@ -71,21 +71,20 @@
71
71
  "zod": "^4.3.6"
72
72
  },
73
73
  "devDependencies": {
74
- "@eslint/js": "^9.39.2",
74
+ "@eslint/js": "^10.0.1",
75
+ "eslint": "^10.0.0",
75
76
  "@trivago/prettier-plugin-sort-imports": "^6.0.2",
76
77
  "@types/node": "^24",
77
- "eslint": "^9.39.2",
78
78
  "eslint-config-prettier": "^10.1.8",
79
79
  "eslint-plugin-de-morgan": "^2.0.0",
80
80
  "eslint-plugin-depend": "^1.4.0",
81
- "eslint-plugin-sonarjs": "^3.0.6",
82
- "eslint-plugin-unused-imports": "^4.3.0",
81
+ "eslint-plugin-unused-imports": "^4.4.1",
83
82
  "jscpd": "^4.0.8",
84
- "knip": "^5.83.1",
83
+ "knip": "^5.84.1",
85
84
  "prettier": "^3.8.1",
86
85
  "tsx": "^4.21.0",
87
86
  "typescript": "^5.9.3",
88
- "typescript-eslint": "^8.54.0"
87
+ "typescript-eslint": "^8.56.0"
89
88
  },
90
89
  "engines": {
91
90
  "node": ">=24"