@j0hanz/filesystem-mcp 1.2.1 → 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 (47) hide show
  1. package/README.md +11 -0
  2. package/dist/cli.js +14 -8
  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/lib/resource-store.js +1 -1
  10. package/dist/pkg-info.d.ts +6 -0
  11. package/dist/pkg-info.js +9 -0
  12. package/dist/schemas.d.ts +28 -28
  13. package/dist/schemas.js +26 -26
  14. package/dist/server/bootstrap.d.ts +4 -0
  15. package/dist/server/bootstrap.js +117 -0
  16. package/dist/server/capabilities.d.ts +10 -0
  17. package/dist/server/capabilities.js +40 -0
  18. package/dist/server/logging.d.ts +7 -0
  19. package/dist/server/logging.js +41 -0
  20. package/dist/server/roots-manager.d.ts +19 -0
  21. package/dist/server/roots-manager.js +173 -0
  22. package/dist/server/types.d.ts +4 -0
  23. package/dist/server/types.js +1 -0
  24. package/dist/server.d.ts +2 -8
  25. package/dist/server.js +1 -346
  26. package/dist/tools/apply-patch.js +17 -3
  27. package/dist/tools/calculate-hash.js +48 -13
  28. package/dist/tools/create-directory.js +13 -3
  29. package/dist/tools/delete-file.js +9 -2
  30. package/dist/tools/diff-files.js +16 -2
  31. package/dist/tools/edit-file.js +8 -7
  32. package/dist/tools/list-directory.js +13 -2
  33. package/dist/tools/move-file.js +11 -3
  34. package/dist/tools/read-multiple.js +21 -3
  35. package/dist/tools/read.js +16 -2
  36. package/dist/tools/replace-in-files.js +44 -11
  37. package/dist/tools/roots.js +12 -2
  38. package/dist/tools/search-content.js +62 -29
  39. package/dist/tools/search-files.js +60 -13
  40. package/dist/tools/shared.d.ts +5 -1
  41. package/dist/tools/shared.js +55 -8
  42. package/dist/tools/stat-many.js +22 -3
  43. package/dist/tools/stat.js +12 -2
  44. package/dist/tools/task-support.js +60 -13
  45. package/dist/tools/tree.js +15 -2
  46. package/dist/tools/write-file.js +13 -3
  47. package/package.json +4 -2
@@ -1,7 +1,7 @@
1
1
  import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
2
2
  import { ErrorCode, McpError } from '../lib/errors.js';
3
3
  import { isRecord } from '../lib/type-guards.js';
4
- import { buildToolErrorResponse, withDefaultIcons } from './shared.js';
4
+ import { buildToolErrorResponse, maybeStripStructuredContentFromResult, withDefaultIcons, } from './shared.js';
5
5
  function isExperimentalTaskRegistration(value) {
6
6
  if (!value || typeof value !== 'object')
7
7
  return false;
@@ -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,21 +229,20 @@ 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
  }
191
236
  }
192
237
  async function runTaskInBackground(run, args, extra, taskStore, taskId) {
193
238
  try {
194
- const result = await run(args, extra);
239
+ const result = maybeStripStructuredContentFromResult(await run(args, extra));
195
240
  const status = isErrorResult(result) ? 'failed' : 'completed';
196
241
  await tryStoreTaskResult(taskStore, taskId, status, result);
197
242
  await notifyTaskStatusIfPossible(extra, taskStore, taskId);
198
243
  }
199
244
  catch (error) {
200
- const fallback = buildToolErrorResponse(error, ErrorCode.E_UNKNOWN);
245
+ const fallback = maybeStripStructuredContentFromResult(buildToolErrorResponse(error, ErrorCode.E_UNKNOWN));
201
246
  try {
202
247
  await tryStoreTaskResult(taskStore, taskId, 'failed', fallback);
203
248
  await notifyTaskStatusIfPossible(extra, taskStore, taskId);
@@ -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,9 +34,19 @@ 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
- progressMessage: (args) => `🛠 write: ${path.basename(args.path)}`,
40
+ progressMessage: (args) => `🛠 write: ${path.basename(args.path)} [${args.content.length} chars]`,
41
+ completionMessage: (args, result) => {
42
+ const name = path.basename(args.path);
43
+ if (result.isError)
44
+ return `🛠 write: ${name} • failed`;
45
+ const sc = result.structuredContent;
46
+ if (!sc.ok)
47
+ return `🛠 write: ${name} • failed`;
48
+ return `🛠 write: ${name} • ${sc.bytesWritten ?? 0} bytes`;
49
+ },
40
50
  });
41
51
  if (registerToolTaskIfAvailable(server, 'write', WRITE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
42
52
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.2.1",
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",
@@ -31,12 +31,14 @@
31
31
  "start": "node dist/index.js",
32
32
  "format": "prettier --write .",
33
33
  "type-check": "node scripts/tasks.mjs type-check",
34
+ "type-check:src": "node node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
35
+ "type-check:tests": "node node_modules/typescript/bin/tsc -p tsconfig.test.json --noEmit",
34
36
  "type-check:diagnostics": "tsc --noEmit --extendedDiagnostics",
35
37
  "type-check:trace": "node -e \"require('fs').rmSync('.ts-trace',{recursive:true,force:true})\" && tsc --noEmit --generateTrace .ts-trace",
36
38
  "lint": "eslint .",
37
39
  "lint:fix": "eslint . --fix",
38
40
  "test": "node scripts/tasks.mjs test",
39
- "test:fast": "node --test --import tsx/esm src/__tests__/**/*.test.ts",
41
+ "test:fast": "node --test --import tsx/esm src/__tests__/**/*.test.ts node-tests/**/*.test.ts",
40
42
  "test:coverage": "node scripts/tasks.mjs test --coverage",
41
43
  "knip": "knip",
42
44
  "knip:fix": "knip --fix",