@j0hanz/filesystem-mcp 1.9.0 → 1.10.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 (48) hide show
  1. package/README.md +15 -15
  2. package/dist/completions.js +140 -115
  3. package/dist/lib/constants.d.ts +1 -0
  4. package/dist/lib/constants.js +2 -0
  5. package/dist/lib/file-operations/metadata.d.ts +5 -1
  6. package/dist/lib/file-operations/metadata.js +14 -1
  7. package/dist/lib/file-operations/search.d.ts +7 -5
  8. package/dist/lib/file-operations/search.js +64 -32
  9. package/dist/lib/fs-helpers.d.ts +3 -1
  10. package/dist/lib/fs-helpers.js +63 -0
  11. package/dist/lib/paths.d.ts +8 -0
  12. package/dist/lib/paths.js +119 -64
  13. package/dist/lib/resource-store.d.ts +2 -0
  14. package/dist/lib/resource-store.js +58 -17
  15. package/dist/prompts.d.ts +2 -0
  16. package/dist/prompts.js +51 -0
  17. package/dist/resources/generated-instructions.js +36 -9
  18. package/dist/resources/tool-catalog.js +30 -7
  19. package/dist/resources/tool-info.d.ts +4 -0
  20. package/dist/resources/tool-info.js +21 -3
  21. package/dist/resources/workflows.js +17 -5
  22. package/dist/schemas.d.ts +47 -12
  23. package/dist/schemas.js +75 -18
  24. package/dist/server/bootstrap.js +103 -91
  25. package/dist/server/roots-manager.d.ts +3 -0
  26. package/dist/server/roots-manager.js +15 -3
  27. package/dist/tools/apply-patch.js +135 -31
  28. package/dist/tools/calculate-hash.js +13 -8
  29. package/dist/tools/create-directory.js +14 -3
  30. package/dist/tools/delete-file.js +1 -0
  31. package/dist/tools/diff-files.js +26 -8
  32. package/dist/tools/edit-file.js +11 -8
  33. package/dist/tools/list-directory.js +1 -6
  34. package/dist/tools/move-file.js +39 -7
  35. package/dist/tools/read-multiple.js +9 -1
  36. package/dist/tools/read.js +38 -6
  37. package/dist/tools/replace-in-files.js +72 -25
  38. package/dist/tools/roots.js +1 -0
  39. package/dist/tools/search-content.js +76 -48
  40. package/dist/tools/search-files.js +6 -7
  41. package/dist/tools/shared.d.ts +2 -1
  42. package/dist/tools/shared.js +36 -20
  43. package/dist/tools/stat-many.js +1 -1
  44. package/dist/tools/stat.js +4 -0
  45. package/dist/tools/task-support.js +4 -12
  46. package/dist/tools/tree.js +4 -0
  47. package/dist/tools/write-file.js +4 -2
  48. package/package.json +17 -8
@@ -42,18 +42,16 @@ export function maybeStripStructuredContentFromResult(result) {
42
42
  return result;
43
43
  if (!Object.hasOwn(result, 'structuredContent'))
44
44
  return result;
45
- const rest = { ...result };
46
- delete rest['structuredContent'];
47
- return rest;
45
+ const stripped = Object.fromEntries(Object.entries(result).filter(([key]) => key !== 'structuredContent'));
46
+ return stripped;
48
47
  }
49
48
  function maybeStripOutputSchema(tool) {
50
49
  if (!shouldStripStructuredOutput())
51
50
  return tool;
52
51
  if (!Object.hasOwn(tool, 'outputSchema'))
53
52
  return tool;
54
- const mutable = { ...tool };
55
- delete mutable['outputSchema'];
56
- return mutable;
53
+ const stripped = Object.fromEntries(Object.entries(tool).filter(([key]) => key !== 'outputSchema'));
54
+ return stripped;
57
55
  }
58
56
  function buildTextPreview(text) {
59
57
  if (text.length <= MAX_INLINE_PREVIEW_CHARS)
@@ -83,11 +81,17 @@ export function maybeExternalizeTextContent(resourceStore, content, params) {
83
81
  };
84
82
  }
85
83
  export function buildResourceLink(params) {
84
+ const descParts = [];
85
+ if (params.description)
86
+ descParts.push(params.description);
87
+ if (params.expiresAt)
88
+ descParts.push(`Expires: ${params.expiresAt}`);
89
+ const description = descParts.length > 0 ? descParts.join(' · ') : undefined;
86
90
  return {
87
91
  type: 'resource_link',
88
92
  uri: params.uri,
89
93
  name: params.name,
90
- ...(params.description ? { description: params.description } : {}),
94
+ ...(description ? { description } : {}),
91
95
  ...(params.mimeType ? { mimeType: params.mimeType } : {}),
92
96
  };
93
97
  }
@@ -207,6 +211,18 @@ function buildNotInitializedResult() {
207
211
  return buildToolErrorResponse(NOT_INITIALIZED_ERROR, ErrorCode.E_INVALID_INPUT);
208
212
  }
209
213
  async function reportProgress(extra, progress) {
214
+ await updateTaskStoreProgress(extra, progress);
215
+ await sendMcpProgressNotification(extra, progress);
216
+ }
217
+ function formatTaskStatusMessage(progress) {
218
+ if (progress.total !== undefined) {
219
+ return progress.message
220
+ ? `${progress.message} (${progress.current}/${progress.total})`
221
+ : `${progress.current}/${progress.total}`;
222
+ }
223
+ return progress.message ?? `${progress.current}`;
224
+ }
225
+ async function updateTaskStoreProgress(extra, progress) {
210
226
  const taskExtra = extra;
211
227
  if (typeof taskExtra.taskId === 'string' &&
212
228
  taskExtra.taskStore !== undefined &&
@@ -214,22 +230,15 @@ async function reportProgress(extra, progress) {
214
230
  const store = taskExtra.taskStore;
215
231
  if (typeof store.updateTaskStatus === 'function') {
216
232
  try {
217
- let statusMessage = progress.message;
218
- if (progress.total !== undefined) {
219
- statusMessage = statusMessage
220
- ? `${statusMessage} (${progress.current}/${progress.total})`
221
- : `${progress.current}/${progress.total}`;
222
- }
223
- else {
224
- statusMessage ??= `${progress.current}`;
225
- }
226
- await store.updateTaskStatus(taskExtra.taskId, 'working', statusMessage);
233
+ await store.updateTaskStatus(taskExtra.taskId, 'working', formatTaskStatusMessage(progress));
227
234
  }
228
235
  catch (error) {
229
236
  console.error('Failed to update task status message:', error);
230
237
  }
231
238
  }
232
239
  }
240
+ }
241
+ async function sendMcpProgressNotification(extra, progress) {
233
242
  if (canSendProgress(extra)) {
234
243
  try {
235
244
  await extra.sendNotification({
@@ -282,9 +291,10 @@ export function notifyProgress(extra, progress) {
282
291
  return;
283
292
  void reportProgress(extra, progress);
284
293
  }
285
- export function createToolProgressSession(extra, startMessage) {
294
+ export function createToolProgressSession(extra, startMessage, initialTotal) {
286
295
  notifyProgress(extra, {
287
296
  current: 0,
297
+ ...(initialTotal !== undefined ? { total: initialTotal } : {}),
288
298
  message: startMessage,
289
299
  });
290
300
  let cursor = 0;
@@ -325,9 +335,15 @@ export function createToolProgressSession(extra, startMessage) {
325
335
  };
326
336
  }
327
337
  export function createBatchProgressCallbacks(extra, params) {
328
- const progress = createToolProgressSession(extra, `${params.toolLabel}: ${params.context}`);
338
+ const progress = createToolProgressSession(extra, `${params.toolLabel}: ${params.context}`, params.totalItems);
339
+ let itemsDone = 0;
329
340
  const onItemComplete = () => {
330
- progress.increment((current) => `${params.toolLabel}: ${params.context} [${current}/${params.totalItems} ${params.itemVerb}]`);
341
+ itemsDone++;
342
+ progress.update({
343
+ current: itemsDone,
344
+ total: params.totalItems,
345
+ message: `${params.toolLabel}: ${params.context} [${itemsDone}/${params.totalItems} ${params.itemVerb}]`,
346
+ });
331
347
  };
332
348
  return { progress, onItemComplete };
333
349
  }
@@ -79,7 +79,7 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
79
79
  toolLabel: '🕮 stat_many',
80
80
  context,
81
81
  totalItems: args.paths.length,
82
- itemVerb: 'scanned',
82
+ itemVerb: 'done',
83
83
  });
84
84
  try {
85
85
  const result = await handleGetMultipleFileInfo(args, signal, onItemComplete);
@@ -5,6 +5,7 @@ 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
7
  import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
+ import { registerToolTaskIfAvailable } from './task-support.js';
8
9
  export const GET_FILE_INFO_TOOL = {
9
10
  name: 'stat',
10
11
  title: 'Get File Info',
@@ -13,6 +14,7 @@ export const GET_FILE_INFO_TOOL = {
13
14
  inputSchema: GetFileInfoInputSchema,
14
15
  outputSchema: GetFileInfoOutputSchema,
15
16
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
17
+ taskSupport: 'optional',
16
18
  nuances: ['Use before read/search when file size/type uncertainty exists.'],
17
19
  };
18
20
  function formatFileInfoDetails(info) {
@@ -62,5 +64,7 @@ export function registerGetFileInfoTool(server, options = {}) {
62
64
  },
63
65
  });
64
66
  const validatedHandler = withValidatedArgs(GetFileInfoInputSchema, wrappedHandler);
67
+ if (registerToolTaskIfAvailable(server, 'stat', GET_FILE_INFO_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
68
+ return;
65
69
  server.registerTool('stat', withDefaultIcons({ ...GET_FILE_INFO_TOOL }, options.iconInfo), validatedHandler);
66
70
  }
@@ -1,5 +1,6 @@
1
1
  import { channel } from 'node:diagnostics_channel';
2
2
  import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
3
+ import { DEFAULT_TASK_TTL_MS } from '../lib/constants.js';
3
4
  import { ErrorCode, McpError } from '../lib/errors.js';
4
5
  import { isRecord } from '../lib/utils.js';
5
6
  import { buildToolErrorResponse, maybeStripStructuredContentFromResult, withDefaultIcons, } from './shared.js';
@@ -44,7 +45,6 @@ function hasTaskToolCapability(server) {
44
45
  const { call } = tools;
45
46
  return isRecord(call);
46
47
  }
47
- const RELATED_TASK_META_KEY = 'io.modelcontextprotocol/related-task';
48
48
  const TASK_STATUS_NOTIFICATION_METHOD = 'notifications/tasks/status';
49
49
  const TASK_DIAGNOSTICS_CHANNEL = channel('filesystem-mcp:tasks');
50
50
  function publishTaskDiagnostics(event) {
@@ -169,13 +169,6 @@ async function projectCancelledTaskStatus(taskStore, task) {
169
169
  }
170
170
  return task;
171
171
  }
172
- function withRelatedTaskMeta(result, taskId) {
173
- const existingMeta = isRecord(result['_meta']) ? result['_meta'] : {};
174
- return {
175
- ...result,
176
- _meta: { ...existingMeta, [RELATED_TASK_META_KEY]: { taskId } },
177
- };
178
- }
179
172
  function buildTaskStatusNotificationParams(task) {
180
173
  const params = {
181
174
  taskId: task.taskId,
@@ -259,9 +252,8 @@ async function isTaskAlreadyTerminal(taskStore, taskId) {
259
252
  }
260
253
  }
261
254
  async function tryStoreTaskResult(taskStore, taskId, status, result) {
262
- const resultWithTaskMeta = withRelatedTaskMeta(result, taskId);
263
255
  try {
264
- await taskStore.storeTaskResult(taskId, status, resultWithTaskMeta);
256
+ await taskStore.storeTaskResult(taskId, status, result);
265
257
  }
266
258
  catch (error) {
267
259
  if (await isTaskAlreadyTerminal(taskStore, taskId))
@@ -352,7 +344,7 @@ export function createToolTaskHandler(run, options) {
352
344
  }
353
345
  const taskStore = getTaskStore(extra);
354
346
  const task = await taskStore.createTask({
355
- ttl: extra.taskRequestedTtl ?? null,
347
+ ttl: extra.taskRequestedTtl ?? DEFAULT_TASK_TTL_MS,
356
348
  });
357
349
  publishTaskDiagnostics({
358
350
  phase: 'task_created',
@@ -383,7 +375,7 @@ export function createToolTaskHandler(run, options) {
383
375
  const taskStore = getTaskStore(extra);
384
376
  const taskId = getTaskId(extra);
385
377
  const result = await taskStore.getTaskResult(taskId);
386
- return normalizeCallToolResult(withRelatedTaskMeta(result, taskId));
378
+ return normalizeCallToolResult(result);
387
379
  });
388
380
  return {
389
381
  createTask,
@@ -23,6 +23,7 @@ async function handleTree(args, signal, onProgress) {
23
23
  maxEntries: args.maxEntries,
24
24
  includeHidden: args.includeHidden,
25
25
  includeIgnored: args.includeIgnored,
26
+ includeSizes: args.includeSizes,
26
27
  ...(signal ? { signal } : {}),
27
28
  ...(onProgress ? { onProgress } : {}),
28
29
  });
@@ -49,8 +50,10 @@ export function registerTreeTool(server, options = {}) {
49
50
  run: async (signal) => {
50
51
  const context = args.path ? path.basename(args.path) : '.';
51
52
  let progressCursor = 0;
53
+ const knownTotal = args.maxEntries;
52
54
  notifyProgress(extra, {
53
55
  current: 0,
56
+ total: knownTotal,
54
57
  message: `≣ tree: ${context}`,
55
58
  });
56
59
  const baseReporter = createProgressReporter(extra);
@@ -60,6 +63,7 @@ export function registerTreeTool(server, options = {}) {
60
63
  progressCursor = current;
61
64
  baseReporter({
62
65
  current,
66
+ total: knownTotal,
63
67
  message: `≣ tree: ${context} [${current} entries]`,
64
68
  });
65
69
  };
@@ -3,6 +3,7 @@ import * as path from 'node:path';
3
3
  import { ErrorCode } from '../lib/errors.js';
4
4
  import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
5
5
  import { validatePathForWrite } from '../lib/paths.js';
6
+ import { formatBytes } from '../config.js';
6
7
  import { WriteFileInputSchema, WriteFileOutputSchema } from '../schemas.js';
7
8
  import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
9
  import { registerToolTaskIfAvailable } from './task-support.js';
@@ -16,6 +17,7 @@ export const WRITE_FILE_TOOL = {
16
17
  gotchas: [
17
18
  '`write` replaces ALL existing content — use `edit` for partial updates.',
18
19
  ],
20
+ taskSupport: 'optional',
19
21
  };
20
22
  async function handleWriteFile(args, signal) {
21
23
  const validPath = await validatePathForWrite(args.path, signal);
@@ -40,7 +42,7 @@ export function registerWriteFileTool(server, options = {}) {
40
42
  });
41
43
  const wrappedHandler = wrapToolHandler(handler, {
42
44
  guard: options.isInitialized,
43
- progressMessage: (args) => `🛠 write: ${path.basename(args.path)} [${args.content.length} chars]`,
45
+ progressMessage: (args) => `🛠 write: ${path.basename(args.path)}`,
44
46
  completionMessage: (args, result) => {
45
47
  const name = path.basename(args.path);
46
48
  if (result.isError)
@@ -48,7 +50,7 @@ export function registerWriteFileTool(server, options = {}) {
48
50
  const sc = result.structuredContent;
49
51
  if (!sc.ok)
50
52
  return `🛠 write: ${name} • failed`;
51
- return `🛠 write: ${name} • ${sc.bytesWritten ?? 0} bytes`;
53
+ return `🛠 write: ${name} • ${formatBytes(sc.bytesWritten ?? 0)} written`;
52
54
  },
53
55
  });
54
56
  const validatedHandler = withValidatedArgs(WriteFileInputSchema, wrappedHandler);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "mcpName": "io.github.j0hanz/filesystem-mcp",
5
- "description": "MCP Server that enables LLMs to interact with the local filesystem.",
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.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
@@ -48,13 +48,23 @@
48
48
  "keywords": [
49
49
  "mcp",
50
50
  "model-context-protocol",
51
+ "mcp-server",
51
52
  "filesystem",
52
- "scanner",
53
+ "file-management",
54
+ "file-operations",
55
+ "grep",
56
+ "glob",
57
+ "diff",
58
+ "patch",
53
59
  "search",
54
- "analysis",
55
- "exploration",
56
- "manipulation",
57
- "server"
60
+ "security",
61
+ "server",
62
+ "stdio",
63
+ "sse",
64
+ "typescript",
65
+ "nodejs",
66
+ "llm",
67
+ "cli"
58
68
  ],
59
69
  "author": "j0hanz",
60
70
  "license": "MIT",
@@ -80,7 +90,6 @@
80
90
  "eslint-config-prettier": "^10.1.8",
81
91
  "eslint-plugin-de-morgan": "^2.1.1",
82
92
  "eslint-plugin-depend": "^1.5.0",
83
- "eslint-plugin-sonarjs": "^4.0.0",
84
93
  "eslint-plugin-unused-imports": "^4.4.1",
85
94
  "jscpd": "^4.0.8",
86
95
  "knip": "^5.85.0",