@j0hanz/filesystem-mcp 1.13.1 → 1.14.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 (65) hide show
  1. package/README.md +117 -100
  2. package/dist/completions.d.ts +0 -11
  3. package/dist/completions.js +1 -1
  4. package/dist/config.d.ts +0 -1
  5. package/dist/lib/constants.d.ts +0 -1
  6. package/dist/lib/constants.js +0 -1
  7. package/dist/lib/errors.d.ts +0 -1
  8. package/dist/lib/errors.js +5 -9
  9. package/dist/lib/file-operations/core.d.ts +3 -2
  10. package/dist/lib/file-operations/metadata.js +9 -3
  11. package/dist/lib/file-operations/search.d.ts +1 -59
  12. package/dist/lib/file-operations/search.js +9 -22
  13. package/dist/lib/file-operations/traversal.d.ts +2 -2
  14. package/dist/lib/fs-helpers.d.ts +1 -2
  15. package/dist/lib/fs-helpers.js +10 -12
  16. package/dist/lib/globs.d.ts +2 -0
  17. package/dist/lib/globs.js +19 -0
  18. package/dist/lib/paths.d.ts +0 -3
  19. package/dist/lib/paths.js +1 -4
  20. package/dist/lib/utils.d.ts +4 -4
  21. package/dist/lib/utils.js +1 -10
  22. package/dist/lib/zod-codecs.d.ts +2 -0
  23. package/dist/lib/zod-codecs.js +18 -0
  24. package/dist/pkg-info.d.ts +1 -0
  25. package/dist/pkg-info.js +2 -2
  26. package/dist/prompts.js +3 -3
  27. package/dist/resources/generated-instructions.js +3 -12
  28. package/dist/resources/tool-catalog.js +10 -41
  29. package/dist/resources/tool-info.d.ts +0 -1
  30. package/dist/resources/tool-info.js +24 -39
  31. package/dist/resources/workflows.js +8 -1
  32. package/dist/schemas.d.ts +179 -481
  33. package/dist/schemas.js +156 -169
  34. package/dist/server/bootstrap.d.ts +0 -10
  35. package/dist/server/bootstrap.js +3 -3
  36. package/dist/server/roots-manager.js +1 -1
  37. package/dist/tools/apply-patch.js +19 -8
  38. package/dist/tools/calculate-hash.js +3 -5
  39. package/dist/tools/create-directory.d.ts +1 -4
  40. package/dist/tools/create-directory.js +2 -2
  41. package/dist/tools/delete-file.js +2 -4
  42. package/dist/tools/diff-files.js +1 -3
  43. package/dist/tools/edit-file.d.ts +1 -7
  44. package/dist/tools/edit-file.js +6 -3
  45. package/dist/tools/list-directory.js +10 -15
  46. package/dist/tools/move-file.d.ts +1 -4
  47. package/dist/tools/move-file.js +15 -27
  48. package/dist/tools/read-multiple.js +12 -7
  49. package/dist/tools/read.js +1 -2
  50. package/dist/tools/replace-in-files.d.ts +1 -10
  51. package/dist/tools/replace-in-files.js +62 -97
  52. package/dist/tools/roots.js +2 -6
  53. package/dist/tools/search-content.js +152 -188
  54. package/dist/tools/search-files.js +10 -13
  55. package/dist/tools/shared.d.ts +10 -3
  56. package/dist/tools/shared.js +53 -12
  57. package/dist/tools/stat-many.js +6 -4
  58. package/dist/tools/stat.js +2 -2
  59. package/dist/tools/task-support.d.ts +0 -7
  60. package/dist/tools/task-support.js +7 -6
  61. package/dist/tools/tree.js +1 -1
  62. package/dist/tools/write-file.js +1 -5
  63. package/dist/tools.d.ts +0 -1
  64. package/dist/tools.js +0 -1
  65. package/package.json +5 -5
@@ -6,6 +6,7 @@ import { createDetailedError, ErrorCode, formatDetailedError, getSuggestion, Mcp
6
6
  import { createTimedAbortSignal } from '../lib/fs-helpers.js';
7
7
  import { withToolDiagnostics } from '../lib/observability.js';
8
8
  import { getAllowedDirectories } from '../lib/paths.js';
9
+ import { createBase64JsonCodec } from '../lib/zod-codecs.js';
9
10
  export {} from './contract.js';
10
11
  const MAX_INLINE_CONTENT_CHARS = parseInt(process.env['FS_CONTEXT_MAX_INLINE_CHARS'] ?? '', 10) || 20_000;
11
12
  const MAX_INLINE_PREVIEW_CHARS = 4_000;
@@ -64,7 +65,7 @@ function normalizeToolExecution(tool) {
64
65
  }
65
66
  return normalized;
66
67
  }
67
- export function shouldStripStructuredOutput() {
68
+ function shouldStripStructuredOutput() {
68
69
  return parseTrueEnvFlag(process.env['FS_CONTEXT_STRIP_STRUCTURED']);
69
70
  }
70
71
  export function maybeStripStructuredContentFromResult(result) {
@@ -133,19 +134,48 @@ function resolveDetailedError(error, defaultCode, path) {
133
134
  }
134
135
  return detailed;
135
136
  }
137
+ export function buildStructuredError(error, defaultCode, path) {
138
+ const detailed = resolveDetailedError(error, defaultCode, path);
139
+ return {
140
+ code: detailed.code,
141
+ message: detailed.message,
142
+ ...(detailed.path !== undefined ? { path: detailed.path } : {}),
143
+ ...(detailed.suggestion !== undefined
144
+ ? { suggestion: detailed.suggestion }
145
+ : {}),
146
+ };
147
+ }
136
148
  export function buildToolResponse(text, structuredContent, extraContent = []) {
137
149
  return {
138
150
  content: [{ type: 'text', text }, ...extraContent],
139
151
  structuredContent,
140
152
  };
141
153
  }
154
+ function validateStructuredContent(toolName, outputSchema, structuredContent) {
155
+ const parsed = outputSchema.safeParse(structuredContent);
156
+ if (parsed.success) {
157
+ return parsed.data;
158
+ }
159
+ throw new McpError(ErrorCode.E_UNKNOWN, `Tool "${toolName}" returned invalid structuredContent.`, undefined, { errors: z.treeifyError(parsed.error) });
160
+ }
161
+ function validateToolResponse(toolName, result, outputSchema) {
162
+ if (!outputSchema)
163
+ return result;
164
+ if (!Object.hasOwn(result, 'structuredContent')) {
165
+ throw new McpError(ErrorCode.E_UNKNOWN, `Tool "${toolName}" returned success without structuredContent.`);
166
+ }
167
+ return {
168
+ ...result,
169
+ structuredContent: validateStructuredContent(toolName, outputSchema, result.structuredContent),
170
+ };
171
+ }
142
172
  function parseToolArgs(schema, args) {
143
173
  const candidate = args === undefined ? {} : args;
144
174
  const parsed = schema.safeParse(candidate);
145
175
  if (parsed.success) {
146
176
  return parsed.data;
147
177
  }
148
- throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid tool arguments: ${parsed.error.message}`, undefined, { errors: z.treeifyError(parsed.error) });
178
+ throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid tool arguments:\n${z.prettifyError(parsed.error)}`, undefined, { errors: z.treeifyError(parsed.error) });
149
179
  }
150
180
  export function withValidatedArgs(schema, handler) {
151
181
  return async (args, extra) => {
@@ -222,7 +252,7 @@ export async function executeToolWithDiagnostics(options) {
222
252
  return withToolDiagnostics(options.toolName, () => withToolErrorHandling(async () => {
223
253
  const { signal, cleanup } = getToolSignal(options.extra.signal, options.timedSignal);
224
254
  try {
225
- return await options.run(signal);
255
+ return validateToolResponse(options.toolName, await options.run(signal), options.outputSchema);
226
256
  }
227
257
  finally {
228
258
  cleanup();
@@ -461,19 +491,16 @@ export function resolvePathOrRoot(pathValue) {
461
491
  }
462
492
  return root;
463
493
  }
494
+ const OffsetCursorSchema = z.strictObject({
495
+ offset: z.int().min(0),
496
+ });
497
+ const OffsetCursorCodec = createBase64JsonCodec(OffsetCursorSchema);
464
498
  export function encodeOffsetCursor(offset) {
465
- return Buffer.from(JSON.stringify({ offset })).toString('base64url');
499
+ return z.encode(OffsetCursorCodec, { offset });
466
500
  }
467
501
  export function decodeOffsetCursor(cursor) {
468
502
  try {
469
- const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
470
- if (typeof parsed === 'object' &&
471
- parsed !== null &&
472
- typeof parsed.offset === 'number') {
473
- const { offset } = parsed;
474
- if (Number.isInteger(offset) && offset >= 0)
475
- return offset;
476
- }
503
+ return OffsetCursorCodec.parse(cursor).offset;
477
504
  }
478
505
  catch {
479
506
  // fall through to throw
@@ -488,6 +515,20 @@ export function buildBatchPathContext(paths, unitLabel = 'paths') {
488
515
  : '';
489
516
  return `${paths.length} ${normalizedLabel} [${first}${extraPaths}]`;
490
517
  }
518
+ export function truncateProgressPattern(pattern, maxLength = 40) {
519
+ if (pattern.length <= maxLength)
520
+ return pattern;
521
+ if (pattern.includes('|')) {
522
+ const segments = pattern.split('|');
523
+ const first = segments[0] ?? '';
524
+ const second = segments[1];
525
+ const preview = second !== undefined ? `${first}|${second}` : first;
526
+ return preview.length <= maxLength
527
+ ? `${preview}…`
528
+ : `${preview.slice(0, maxLength)}…`;
529
+ }
530
+ return `${pattern.slice(0, maxLength)}…`;
531
+ }
491
532
  export function buildBatchCompletionSuffix(summary, successWord, singularWord) {
492
533
  const total = summary?.total ?? 0;
493
534
  const failed = summary?.failed ?? 0;
@@ -3,7 +3,7 @@ import { ErrorCode } from '../lib/errors.js';
3
3
  import { getMultipleFileInfo } from '../lib/file-operations/metadata.js';
4
4
  import { formatBytes, joinLines } from '../config.js';
5
5
  import { GetMultipleFileInfoInputSchema, GetMultipleFileInfoOutputSchema, } from '../schemas.js';
6
- import { buildBatchCompletionSuffix, buildBatchPathContext, buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
6
+ import { buildBatchCompletionSuffix, buildBatchPathContext, buildFileInfoPayload, buildStructuredError, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
7
7
  import { registerToolTaskIfAvailable } from './task-support.js';
8
8
  export const GET_MULTIPLE_FILE_INFO_TOOL = {
9
9
  name: 'stat_many',
@@ -14,7 +14,6 @@ export const GET_MULTIPLE_FILE_INFO_TOOL = {
14
14
  outputSchema: GetMultipleFileInfoOutputSchema,
15
15
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
16
16
  taskSupport: 'optional',
17
- nuances: ['Use before read/search when file size/type uncertainty exists.'],
18
17
  };
19
18
  function formatFileInfoDetail(info) {
20
19
  const lines = [
@@ -38,11 +37,13 @@ async function handleGetMultipleFileInfo(args, signal, onProgress) {
38
37
  const structuredResults = result.results.map((entry) => ({
39
38
  path: entry.path,
40
39
  info: entry.info ? buildFileInfoPayload(entry.info) : undefined,
41
- error: entry.error,
40
+ error: entry.error
41
+ ? buildStructuredError(entry.error, ErrorCode.E_NOT_FOUND, entry.path)
42
+ : undefined,
42
43
  }));
43
44
  const text = result.results
44
45
  .map((entry) => entry.error
45
- ? `${entry.path}: ${entry.error}`
46
+ ? `${entry.path}: ${buildStructuredError(entry.error, ErrorCode.E_NOT_FOUND, entry.path).message}`
46
47
  : entry.info
47
48
  ? formatFileInfoDetail(entry.info)
48
49
  : entry.path)
@@ -64,6 +65,7 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
64
65
  return executeToolWithDiagnostics({
65
66
  toolName: 'stat_many',
66
67
  extra,
68
+ outputSchema: GetMultipleFileInfoOutputSchema,
67
69
  timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
68
70
  context: { path: primaryPath },
69
71
  run: async (signal) => {
@@ -15,7 +15,6 @@ export const GET_FILE_INFO_TOOL = {
15
15
  outputSchema: GetFileInfoOutputSchema,
16
16
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
17
17
  taskSupport: 'forbidden',
18
- nuances: ['Use before read/search when file size/type uncertainty exists.'],
19
18
  };
20
19
  function formatFileInfoDetails(info) {
21
20
  const lines = [
@@ -45,6 +44,7 @@ export function registerGetFileInfoTool(server, options = {}) {
45
44
  const handler = (args, extra) => executeToolWithDiagnostics({
46
45
  toolName: 'stat',
47
46
  extra,
47
+ outputSchema: GetFileInfoOutputSchema,
48
48
  timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
49
49
  context: { path: args.path },
50
50
  run: (signal) => handleGetFileInfo(args, signal),
@@ -58,7 +58,7 @@ export function registerGetFileInfoTool(server, options = {}) {
58
58
  if (result.isError)
59
59
  return `🕮 stat: ${name} • failed`;
60
60
  const sc = result.structuredContent;
61
- if (!sc.ok || !sc.info)
61
+ if (!sc.info)
62
62
  return `🕮 stat: ${name} • failed`;
63
63
  return `🕮 stat: ${sc.info.name} • ${sc.info.type}, ${formatBytes(sc.info.size)}`;
64
64
  },
@@ -1,15 +1,8 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks';
2
1
  import type { ToolTaskHandler } from '@modelcontextprotocol/sdk/experimental/tasks/interfaces.js';
3
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
3
  import type { AnySchema, SchemaOutput, ShapeOutput, ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js';
5
4
  import type { RequestTaskStore } from '@modelcontextprotocol/sdk/shared/protocol.js';
6
5
  import type { IconInfo, ToolExtra, ToolResult } from './shared.js';
7
- export interface TaskContext {
8
- taskId: string;
9
- toolName?: string | undefined;
10
- startTime: number;
11
- }
12
- export declare const taskContext: AsyncLocalStorage<TaskContext>;
13
6
  type TaskToolExtra = ToolExtra & {
14
7
  taskId?: string;
15
8
  taskStore?: RequestTaskStore;
@@ -7,13 +7,16 @@ import { DEFAULT_TASK_TTL_MS, MAX_CONCURRENT_TASKS, MAX_TASK_TTL_MS, } from '../
7
7
  import { ErrorCode, McpError } from '../lib/errors.js';
8
8
  import { isRecord } from '../lib/utils.js';
9
9
  import { buildToolErrorResponse, maybeStripStructuredContentFromResult, withDefaultIcons, } from './shared.js';
10
- export const taskContext = new AsyncLocalStorage();
10
+ const taskContext = new AsyncLocalStorage();
11
11
  const TASK_DIAGNOSTICS_CHANNEL = channel('filesystem-mcp:tasks');
12
12
  function publishTaskDiagnostics(event) {
13
13
  if (TASK_DIAGNOSTICS_CHANNEL.hasSubscribers) {
14
14
  TASK_DIAGNOSTICS_CHANNEL.publish(event);
15
15
  }
16
16
  }
17
+ function getDynamicProperty(target, key) {
18
+ return Reflect.get(target, key);
19
+ }
17
20
  // --- Type Guards & Helpers ---
18
21
  function isExperimentalTaskRegistration(value) {
19
22
  if (!isRecord(value))
@@ -22,8 +25,7 @@ function isExperimentalTaskRegistration(value) {
22
25
  return (registerToolTask === undefined || typeof registerToolTask === 'function');
23
26
  }
24
27
  function getExperimentalTaskRegistration(server) {
25
- const serverWithExperimental = server;
26
- const { experimental } = serverWithExperimental;
28
+ const experimental = getDynamicProperty(server, 'experimental');
27
29
  if (!isRecord(experimental))
28
30
  return undefined;
29
31
  const { tasks } = experimental;
@@ -32,11 +34,10 @@ function getExperimentalTaskRegistration(server) {
32
34
  return tasks;
33
35
  }
34
36
  function hasTaskToolCapability(server) {
35
- const serverRecord = server;
36
- const { server: serverRuntime } = serverRecord;
37
+ const serverRuntime = getDynamicProperty(server, 'server');
37
38
  if (!isRecord(serverRuntime))
38
39
  return true; // Assume capability if runtime structure is opaque
39
- const { getCapabilities } = serverRuntime;
40
+ const getCapabilities = getDynamicProperty(serverRuntime, 'getCapabilities');
40
41
  if (typeof getCapabilities !== 'function')
41
42
  return true;
42
43
  const capabilities = getCapabilities.call(serverRuntime);
@@ -14,7 +14,6 @@ export const TREE_TOOL = {
14
14
  outputSchema: TreeOutputSchema,
15
15
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
16
16
  taskSupport: 'optional',
17
- gotchas: ['`maxDepth=0` returns only the root node.'],
18
17
  };
19
18
  async function handleTree(args, signal, onProgress) {
20
19
  const basePath = resolvePathOrRoot(args.path);
@@ -45,6 +44,7 @@ export function registerTreeTool(server, options = {}) {
45
44
  return executeToolWithDiagnostics({
46
45
  toolName: 'tree',
47
46
  extra,
47
+ outputSchema: TreeOutputSchema,
48
48
  timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
49
49
  context: { path: targetPath },
50
50
  run: async (signal) => {
@@ -14,9 +14,6 @@ export const WRITE_FILE_TOOL = {
14
14
  inputSchema: WriteFileInputSchema,
15
15
  outputSchema: WriteFileOutputSchema,
16
16
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
17
- gotchas: [
18
- '`write` replaces ALL existing content — use `edit` for partial updates.',
19
- ],
20
17
  taskSupport: 'forbidden',
21
18
  };
22
19
  async function handleWriteFile(args, signal) {
@@ -35,6 +32,7 @@ export function registerWriteFileTool(server, options = {}) {
35
32
  const handler = (args, extra) => executeToolWithDiagnostics({
36
33
  toolName: 'write',
37
34
  extra,
35
+ outputSchema: WriteFileOutputSchema,
38
36
  timedSignal: {},
39
37
  context: { path: args.path },
40
38
  run: (signal) => handleWriteFile(args, signal),
@@ -48,8 +46,6 @@ export function registerWriteFileTool(server, options = {}) {
48
46
  if (result.isError)
49
47
  return `🛠 write: ${name} • failed`;
50
48
  const sc = result.structuredContent;
51
- if (!sc.ok)
52
- return `🛠 write: ${name} • failed`;
53
49
  return `🛠 write: ${name} • ${formatBytes(sc.bytesWritten ?? 0)}`;
54
50
  },
55
51
  });
package/dist/tools.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { type ToolContract } from './tools/contract.js';
3
3
  import type { ToolRegistrationOptions } from './tools/shared.js';
4
- export { buildToolErrorResponse, buildToolResponse } from './tools/shared.js';
5
4
  export declare const ALL_TOOLS: ToolContract[];
6
5
  export declare function registerAllTools(server: McpServer, options?: ToolRegistrationOptions): void;
package/dist/tools.js CHANGED
@@ -17,7 +17,6 @@ import { GET_MULTIPLE_FILE_INFO_TOOL, registerGetMultipleFileInfoTool, } from '.
17
17
  import { GET_FILE_INFO_TOOL, registerGetFileInfoTool } from './tools/stat.js';
18
18
  import { registerTreeTool, TREE_TOOL } from './tools/tree.js';
19
19
  import { registerWriteFileTool, WRITE_FILE_TOOL } from './tools/write-file.js';
20
- export { buildToolErrorResponse, buildToolResponse } from './tools/shared.js';
21
20
  const TOOL_ENTRIES = [
22
21
  {
23
22
  contract: LIST_ALLOWED_DIRECTORIES_TOOL,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.13.1",
3
+ "version": "1.14.0",
4
4
  "mcpName": "io.github.j0hanz/filesystem-mcp",
5
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",
@@ -40,6 +40,7 @@
40
40
  "test": "node scripts/tasks.mjs test",
41
41
  "test:fast": "node --test --import tsx/esm src/__tests__/**/*.test.ts node-tests/**/*.test.ts",
42
42
  "test:coverage": "node scripts/tasks.mjs test --coverage",
43
+ "inspector:smoke": "node scripts/inspector-smoke.mjs",
43
44
  "knip": "knip",
44
45
  "knip:fix": "knip --fix",
45
46
  "inspector": "npm run build && npx -y @modelcontextprotocol/inspector node dist/index.js ${workspaceFolder}",
@@ -86,17 +87,16 @@
86
87
  "@eslint/js": "^10.0.1",
87
88
  "@trivago/prettier-plugin-sort-imports": "^6.0.2",
88
89
  "@types/node": "^24",
89
- "eslint": "^10.0.2",
90
+ "eslint": "^10.0.3",
90
91
  "eslint-config-prettier": "^10.1.8",
91
92
  "eslint-plugin-de-morgan": "^2.1.1",
92
93
  "eslint-plugin-depend": "^1.5.0",
93
94
  "eslint-plugin-unused-imports": "^4.4.1",
94
- "jscpd": "^4.0.8",
95
- "knip": "^5.85.0",
95
+ "knip": "^5.86.0",
96
96
  "prettier": "^3.8.1",
97
97
  "tsx": "^4.21.0",
98
98
  "typescript": "^5.9.3",
99
- "typescript-eslint": "^8.56.1"
99
+ "typescript-eslint": "^8.57.0"
100
100
  },
101
101
  "engines": {
102
102
  "node": ">=24"