@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
package/dist/schemas.js CHANGED
@@ -23,30 +23,18 @@ const PathSchemaBase = z
23
23
  const OptionalPathSchema = PathSchemaBase.optional();
24
24
  const RequiredPathSchema = PathSchemaBase.min(1, 'Path required');
25
25
  const FileTypeSchema = z.enum(['file', 'directory', 'symlink', 'other']);
26
- const TreeEntryTypeSchema = z.enum(['file', 'directory', 'symlink', 'other']);
27
26
  const ListDirectorySortSchema = z.enum(['name', 'size', 'modified', 'type']);
28
27
  const SearchFilesSortSchema = z.enum(['name', 'size', 'modified', 'path']);
29
- const SearchFilesStopReasonSchema = z.enum([
30
- 'maxResults',
31
- 'maxFiles',
32
- 'timeout',
33
- ]);
28
+ const SearchStopReasonSchema = z.enum(['maxResults', 'maxFiles', 'timeout']);
34
29
  const ListDirectoryStopReasonSchema = z.enum(['maxEntries', 'aborted']);
35
- const SearchContentStopReasonSchema = z.enum([
36
- 'maxResults',
37
- 'maxFiles',
38
- 'timeout',
39
- ]);
40
30
  const TreeEntrySchema = z.lazy(() => z.strictObject({
41
31
  name: z.string().describe('Name'),
42
- type: TreeEntryTypeSchema.describe('Type'),
32
+ type: FileTypeSchema.describe('Type'),
43
33
  relativePath: z.string().describe('Relative path'),
44
34
  children: z.array(TreeEntrySchema).optional().describe('Children'),
45
35
  }));
46
36
  const ErrorSchema = z.strictObject({
47
- code: z
48
- .enum(Object.values(ErrorCode))
49
- .describe('Error code (e.g. E_NOT_FOUND)'),
37
+ code: z.enum(ErrorCode).describe('Error code (e.g. E_NOT_FOUND)'),
50
38
  message: z.string().describe('Human-readable message'),
51
39
  path: z.string().optional().describe('Relevant path'),
52
40
  suggestion: z.string().optional().describe('Fix suggestion'),
@@ -65,35 +53,30 @@ const LineNumberSchema = z
65
53
  .number()
66
54
  .int({ error: 'Must be integer' })
67
55
  .min(1, 'Min: 1');
56
+ function addReadRangeIssue(ctx, path, message) {
57
+ ctx.addIssue({
58
+ code: 'custom',
59
+ path: [path],
60
+ message,
61
+ });
62
+ }
68
63
  const validateReadRange = (value, ctx) => {
69
64
  const hasHead = value.head !== undefined;
70
65
  const hasStart = value.startLine !== undefined;
71
66
  const hasEnd = value.endLine !== undefined;
72
67
  if (hasHead && (hasStart || hasEnd)) {
73
- ctx.addIssue({
74
- code: 'custom',
75
- path: ['head'],
76
- message: "Cannot use 'head' with 'startLine'/'endLine'",
77
- });
68
+ addReadRangeIssue(ctx, 'head', "Cannot use 'head' with 'startLine'/'endLine'");
78
69
  }
79
70
  if (hasEnd && !hasStart) {
80
- ctx.addIssue({
81
- code: 'custom',
82
- path: ['endLine'],
83
- message: "'endLine' requires 'startLine'",
84
- });
71
+ addReadRangeIssue(ctx, 'endLine', "'endLine' requires 'startLine'");
85
72
  }
86
73
  if (value.startLine !== undefined &&
87
74
  value.endLine !== undefined &&
88
75
  value.endLine < value.startLine) {
89
- ctx.addIssue({
90
- code: 'custom',
91
- path: ['endLine'],
92
- message: "'endLine' must be >= 'startLine'",
93
- });
76
+ addReadRangeIssue(ctx, 'endLine', "'endLine' must be >= 'startLine'");
94
77
  }
95
78
  };
96
- const FileInfoSchema = z.object({
79
+ const FileInfoSchema = z.strictObject({
97
80
  name: z.string().describe('Name'),
98
81
  path: z.string().describe('Absolute path'),
99
82
  type: FileTypeSchema.describe('Type'),
@@ -112,11 +95,6 @@ const OperationSummarySchema = z.object({
112
95
  succeeded: z.number().describe('Succeeded'),
113
96
  failed: z.number().describe('Failed'),
114
97
  });
115
- const ReadRangeInputSchema = z.strictObject({
116
- head: HeadLinesSchema,
117
- startLine: LineNumberSchema.optional(),
118
- endLine: LineNumberSchema.optional(),
119
- });
120
98
  export const ListDirectoryInputSchema = z.strictObject({
121
99
  path: OptionalPathSchema.describe(DESC_PATH_ROOT),
122
100
  includeHidden: z
@@ -199,13 +177,6 @@ export const SearchFilesInputSchema = z.strictObject({
199
177
  .max(100, 'Max: 100')
200
178
  .optional()
201
179
  .describe('Maximum directory depth to scan'),
202
- maxFilesScanned: z
203
- .number()
204
- .int({ error: 'Must be integer' })
205
- .min(1, 'Min: 1')
206
- .max(200000, 'Max: 200,000')
207
- .optional()
208
- .describe('Hard cap on files scanned'),
209
180
  });
210
181
  export const TreeInputSchema = z.strictObject({
211
182
  path: OptionalPathSchema.describe(DESC_PATH_ROOT),
@@ -242,17 +213,17 @@ export const SearchContentInputSchema = z.strictObject({
242
213
  .string()
243
214
  .min(1, 'Pattern required')
244
215
  .max(1000, 'Max 1000 chars')
245
- .describe('Search text or regex (if isRegex=true)'),
216
+ .describe('Literal text to search for by default; treated as RE2 regex when isRegex is true.'),
246
217
  isRegex: z
247
218
  .boolean()
248
219
  .optional()
249
220
  .default(false)
250
- .describe('Treat pattern as regex'),
221
+ .describe('Treat pattern as a RE2 regular expression. RE2 does not support lookahead, lookbehind, or backreferences.'),
251
222
  caseSensitive: z
252
223
  .boolean()
253
224
  .optional()
254
225
  .default(false)
255
- .describe('Case-sensitive matching'),
226
+ .describe('Case-sensitive matching (default: false — searches are case-insensitive).'),
256
227
  wholeWord: z
257
228
  .boolean()
258
229
  .optional()
@@ -274,14 +245,6 @@ export const SearchContentInputSchema = z.strictObject({
274
245
  .optional()
275
246
  .default(500)
276
247
  .describe('Maximum match rows to return'),
277
- maxFilesScanned: z
278
- .number()
279
- .int({ error: 'Must be integer' })
280
- .min(1, 'Min: 1')
281
- .max(200000, 'Max: 200,000')
282
- .optional()
283
- .default(20000)
284
- .describe('Hard cap on files scanned'),
285
248
  filePattern: z
286
249
  .string()
287
250
  .min(1, 'Pattern required')
@@ -300,15 +263,16 @@ export const SearchContentInputSchema = z.strictObject({
300
263
  .default(false)
301
264
  .describe('Include ignored items (node_modules, etc).'),
302
265
  });
303
- export const ReadFileInputSchema = ReadRangeInputSchema.extend({
266
+ export const ReadFileInputSchema = z
267
+ .strictObject({
304
268
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
305
269
  head: HeadLinesSchema.describe('Read first N lines (preview)'),
306
270
  startLine: LineNumberSchema.optional().describe('Start line (1-based, inclusive)'),
307
271
  endLine: LineNumberSchema.optional().describe('End line (1-based, inclusive). Requires startLine.'),
308
272
  })
309
- .strict()
310
273
  .superRefine(validateReadRange);
311
- export const ReadMultipleFilesInputSchema = ReadRangeInputSchema.extend({
274
+ export const ReadMultipleFilesInputSchema = z
275
+ .strictObject({
312
276
  paths: z
313
277
  .array(RequiredPathSchema)
314
278
  .min(1, 'Min 1 path required')
@@ -318,7 +282,6 @@ export const ReadMultipleFilesInputSchema = ReadRangeInputSchema.extend({
318
282
  startLine: LineNumberSchema.optional().describe('Start line (1-based, inclusive) per file'),
319
283
  endLine: LineNumberSchema.optional().describe('End line (1-based, inclusive) per file. Requires startLine.'),
320
284
  })
321
- .strict()
322
285
  .superRefine(validateReadRange);
323
286
  export const GetFileInfoInputSchema = z.strictObject({
324
287
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
@@ -383,7 +346,7 @@ export const SearchFilesOutputSchema = SearchSummarySchema.extend({
383
346
  .optional(),
384
347
  filesScanned: z.number().optional().describe('Files scanned'),
385
348
  skippedInaccessible: z.number().optional().describe('Inaccessible files'),
386
- stoppedReason: SearchFilesStopReasonSchema.optional().describe('Why search stopped'),
349
+ stoppedReason: SearchStopReasonSchema.optional().describe('Why search stopped'),
387
350
  });
388
351
  export const SearchContentOutputSchema = SearchSummarySchema.extend({
389
352
  ok: z.boolean(),
@@ -414,7 +377,7 @@ export const SearchContentOutputSchema = SearchSummarySchema.extend({
414
377
  .number()
415
378
  .optional()
416
379
  .describe('Lines skipped due to regex timeout'),
417
- stoppedReason: SearchContentStopReasonSchema.optional().describe('Why search stopped'),
380
+ stoppedReason: SearchStopReasonSchema.optional().describe('Why search stopped'),
418
381
  });
419
382
  export const TreeOutputSchema = z.object({
420
383
  ok: z.boolean(),
@@ -496,15 +459,20 @@ export const EditFileInputSchema = z.strictObject({
496
459
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
497
460
  edits: z
498
461
  .array(z.object({
499
- oldText: z.string().describe('Exact string to replace'),
500
- newText: z.string().describe('Replacement string'),
462
+ oldText: z
463
+ .string()
464
+ .describe('Exact literal string to replace — must match character-for-character including whitespace and indentation. Include 3–5 lines of surrounding context to uniquely identify the location.'),
465
+ newText: z
466
+ .string()
467
+ .describe('Replacement string — preserve the indentation style of surrounding code.'),
501
468
  }))
502
- .min(1, 'Min 1 edit required'),
469
+ .min(1, 'Min 1 edit required')
470
+ .describe('List of replacements to apply sequentially. Each edit replaces the first occurrence of oldText.'),
503
471
  dryRun: z
504
472
  .boolean()
505
473
  .optional()
506
474
  .default(false)
507
- .describe('Check only, no writes'),
475
+ .describe('Preview edits without writing. Check unmatchedEdits in the response to verify all oldText values were found.'),
508
476
  });
509
477
  export const EditFileOutputSchema = z.object({
510
478
  ok: z.boolean(),
@@ -582,13 +550,6 @@ export const DiffFilesInputSchema = z.strictObject({
582
550
  .optional()
583
551
  .default(false)
584
552
  .describe('Strip trailing carriage returns before diffing'),
585
- maxFileSize: z
586
- .number()
587
- .int({ error: 'Must be integer' })
588
- .min(1, 'Min: 1')
589
- .max(100 * 1024 * 1024, 'Max: 104,857,600 (100 MiB)')
590
- .optional()
591
- .describe('Maximum bytes per input file to diff'),
592
553
  });
593
554
  export const DiffFilesOutputSchema = z.object({
594
555
  ok: z.boolean(),
@@ -600,8 +561,9 @@ export const DiffFilesOutputSchema = z.object({
600
561
  });
601
562
  export const ApplyPatchInputSchema = z.strictObject({
602
563
  path: RequiredPathSchema.describe('Path to file to patch'),
603
- patch: z.string().describe('Unified diff content to apply'),
604
- fuzzy: z.boolean().optional().default(false).describe('Allow fuzzy patching'),
564
+ patch: z
565
+ .string()
566
+ .describe('Unified diff content to apply — must include @@ hunk headers. Generate with `diff_files`.'),
605
567
  fuzzFactor: z
606
568
  .number()
607
569
  .int({ error: 'Must be integer' })
@@ -614,14 +576,11 @@ export const ApplyPatchInputSchema = z.strictObject({
614
576
  .optional()
615
577
  .default(true)
616
578
  .describe('Auto-convert line endings to match target file'),
617
- maxFileSize: z
618
- .number()
619
- .int({ error: 'Must be integer' })
620
- .min(1, 'Min: 1')
621
- .max(100 * 1024 * 1024, 'Max: 104,857,600 (100 MiB)')
579
+ dryRun: z
580
+ .boolean()
622
581
  .optional()
623
- .describe('Maximum bytes for the target file before patching'),
624
- dryRun: z.boolean().optional().default(false).describe('Check only'),
582
+ .default(false)
583
+ .describe('Validate the patch can be applied without writing. Check `applied` in the response before committing.'),
625
584
  });
626
585
  export const ApplyPatchOutputSchema = z.object({
627
586
  ok: z.boolean(),
@@ -639,18 +598,21 @@ export const SearchAndReplaceInputSchema = z.strictObject({
639
598
  error: 'Invalid glob or unsafe path (absolute/.. forbidden)',
640
599
  })
641
600
  .describe('Glob pattern (e.g. "**/*.ts")'),
642
- excludePatterns: z.array(z.string()).optional().default([]),
643
- searchPattern: z.string().min(1, 'Search pattern required'),
601
+ searchPattern: z
602
+ .string()
603
+ .min(1, 'Search pattern required')
604
+ .describe('Text to search for. Matched literally by default; treated as RE2 regex when isRegex is true.'),
644
605
  replacement: z.string().describe('Replacement text'),
645
- isRegex: z.boolean().optional().default(false),
646
- maxFileSize: z
647
- .number()
648
- .int({ error: 'Must be integer' })
649
- .min(1, 'Min: 1')
650
- .max(100 * 1024 * 1024, 'Max: 104,857,600 (100 MiB)')
606
+ isRegex: z
607
+ .boolean()
608
+ .optional()
609
+ .default(false)
610
+ .describe('Treat searchPattern as a RE2 regular expression. Supports capture group references ($1, $2) in replacement.'),
611
+ dryRun: z
612
+ .boolean()
651
613
  .optional()
652
- .describe('Maximum bytes to read/replace per matched file'),
653
- dryRun: z.boolean().optional().default(false),
614
+ .default(false)
615
+ .describe('Preview matches without writing. Check changedFiles and matches in the response before committing.'),
654
616
  });
655
617
  export const SearchAndReplaceOutputSchema = z.object({
656
618
  ok: z.boolean(),
package/dist/server.js CHANGED
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { InMemoryTaskMessageQueue, InMemoryTaskStore, } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
5
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
- import { InitializedNotificationSchema, RootsListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
7
+ import { InitializedNotificationSchema, RootsListChangedNotificationSchema, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
8
  import { z } from 'zod';
9
9
  import packageJsonRaw from '../package.json' with { type: 'json' };
10
10
  import { registerCompletions } from './completions.js';
@@ -12,33 +12,52 @@ import { formatUnknownErrorMessage } from './lib/errors.js';
12
12
  import { assertNotAborted, createTimedAbortSignal, withAbort, } from './lib/fs-helpers.js';
13
13
  import { getAllowedDirectories, getValidRootDirectories, isPathWithinDirectories, normalizePath, setAllowedDirectoriesResolved, } from './lib/path-validation.js';
14
14
  import { createInMemoryResourceStore } from './lib/resource-store.js';
15
+ import { isRecord } from './lib/type-guards.js';
15
16
  import { registerGetHelpPrompt } from './prompts.js';
16
17
  import { registerInstructionResource, registerResultResources, } from './resources.js';
17
18
  import { registerAllTools } from './tools.js';
19
+ import { withDefaultIcons } from './tools/shared.js';
18
20
  const PackageJsonSchema = z.object({
19
21
  version: z.string(),
20
22
  description: z.string().optional(),
21
23
  homepage: z.string().optional(),
22
24
  });
23
25
  const { version: SERVER_VERSION, description: SERVER_DESCRIPTION, homepage: SERVER_HOMEPAGE, } = PackageJsonSchema.parse(packageJsonRaw);
24
- function normalizeAllowedDirectories(dirs) {
25
- return dirs
26
- .map((dir) => dir.trim())
27
- .filter((dir) => dir.length > 0)
28
- .map(normalizePath);
26
+ function normalizeCLIDirectories(dirs) {
27
+ const normalized = [];
28
+ for (const dir of dirs) {
29
+ const trimmed = dir.trim();
30
+ if (trimmed.length === 0)
31
+ continue;
32
+ normalized.push(normalizePath(trimmed));
33
+ }
34
+ return normalized;
29
35
  }
30
36
  const ROOTS_TIMEOUT_MS = 5000;
31
37
  const ROOTS_DEBOUNCE_MS = 100;
32
38
  const MCP_LOGGER_NAME = 'filesystem-mcp';
39
+ const LOG_LEVEL_ORDER = {
40
+ debug: 0,
41
+ info: 1,
42
+ notice: 2,
43
+ warning: 3,
44
+ error: 4,
45
+ critical: 5,
46
+ alert: 6,
47
+ emergency: 7,
48
+ };
33
49
  function canSendMcpLogs(server) {
34
50
  const capabilities = server.server.getClientCapabilities();
35
- if (!capabilities || typeof capabilities !== 'object')
51
+ if (!isRecord(capabilities))
36
52
  return false;
37
53
  if (!('logging' in capabilities))
38
54
  return false;
39
55
  return Boolean(capabilities.logging);
40
56
  }
41
- function logToMcp(server, level, data) {
57
+ function logToMcp(server, level, data, minLevel = 'debug') {
58
+ if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[minLevel]) {
59
+ return;
60
+ }
42
61
  if (!server || !canSendMcpLogs(server)) {
43
62
  console.error(data);
44
63
  return;
@@ -49,7 +68,7 @@ function logToMcp(server, level, data) {
49
68
  data,
50
69
  };
51
70
  void server.sendLoggingMessage(params).catch((error) => {
52
- console.error(`Failed to send MCP log: ${level} │ ${data}`, data, formatUnknownErrorMessage(error));
71
+ console.error(`Failed to send MCP log: ${level} │ ${data}`, formatUnknownErrorMessage(error));
53
72
  });
54
73
  }
55
74
  class RootsManager {
@@ -57,8 +76,10 @@ class RootsManager {
57
76
  rootDirectories = [];
58
77
  clientInitialized = false;
59
78
  options;
60
- constructor(options) {
79
+ loggingState;
80
+ constructor(options, loggingState) {
61
81
  this.options = options;
82
+ this.loggingState = loggingState ?? { minimumLevel: 'debug' };
62
83
  }
63
84
  isInitialized() {
64
85
  return this.clientInitialized;
@@ -91,7 +112,7 @@ class RootsManager {
91
112
  this.rootsUpdateTimeout.unref();
92
113
  }
93
114
  async recomputeAllowedDirectories() {
94
- const cliAllowedDirs = normalizeAllowedDirectories(this.options.cliAllowedDirs ?? []);
115
+ const cliAllowedDirs = normalizeCLIDirectories(this.options.cliAllowedDirs ?? []);
95
116
  const allowCwd = this.options.allowCwd === true;
96
117
  const allowCwdDirs = allowCwd ? [normalizePath(process.cwd())] : [];
97
118
  const baseline = [...cliAllowedDirs, ...allowCwdDirs];
@@ -109,10 +130,10 @@ class RootsManager {
109
130
  }
110
131
  logMissingDirectories(server) {
111
132
  if (this.options.allowCwd) {
112
- logToMcp(server, 'notice', 'No allowed directories specified. Using the current working directory as an allowed directory.');
133
+ logToMcp(server, 'notice', 'No allowed directories specified. Using the current working directory as an allowed directory.', this.loggingState.minimumLevel);
113
134
  return;
114
135
  }
115
- logToMcp(server, 'warning', 'No allowed directories specified. Please provide directories as command-line arguments or enable --allow-cwd to use the current working directory.');
136
+ logToMcp(server, 'warning', 'No allowed directories specified. Please provide directories as command-line arguments or enable --allow-cwd to use the current working directory.', this.loggingState.minimumLevel);
116
137
  }
117
138
  async updateRootsFromClient(server) {
118
139
  try {
@@ -128,7 +149,7 @@ class RootsManager {
128
149
  this.rootDirectories = await resolveRootDirectories(roots);
129
150
  }
130
151
  catch (error) {
131
- logToMcp(server, 'debug', `[DEBUG] MCP Roots protocol unavailable or failed: ${formatUnknownErrorMessage(error)}`);
152
+ logToMcp(server, 'debug', `[DEBUG] MCP Roots protocol unavailable or failed: ${formatUnknownErrorMessage(error)}`, this.loggingState.minimumLevel);
132
153
  }
133
154
  finally {
134
155
  await this.recomputeAllowedDirectories();
@@ -143,12 +164,10 @@ function getRootsManager(server) {
143
164
  }
144
165
  return manager;
145
166
  }
146
- const RootSchema = z
147
- .object({
167
+ const RootSchema = z.strictObject({
148
168
  uri: z.string(),
149
169
  name: z.string().optional(),
150
- })
151
- .strict();
170
+ });
152
171
  const RootsResponseSchema = z.object({
153
172
  roots: z.array(RootSchema).optional(),
154
173
  });
@@ -157,7 +176,13 @@ function extractRoots(value) {
157
176
  if (!parsed.success || !parsed.data.roots) {
158
177
  return [];
159
178
  }
160
- return parsed.data.roots.filter(isRoot).map(normalizeRoot);
179
+ const roots = [];
180
+ for (const root of parsed.data.roots) {
181
+ if (isRoot(root)) {
182
+ roots.push(normalizeRoot(root));
183
+ }
184
+ }
185
+ return roots;
161
186
  }
162
187
  async function resolveRootDirectories(roots) {
163
188
  if (roots.length === 0)
@@ -171,16 +196,13 @@ async function resolveRootDirectories(roots) {
171
196
  }
172
197
  }
173
198
  function isRoot(value) {
174
- return (value !== null &&
175
- typeof value === 'object' &&
176
- 'uri' in value &&
177
- typeof value.uri === 'string');
199
+ return isRecord(value) && typeof value['uri'] === 'string';
178
200
  }
179
201
  function normalizeRoot(root) {
180
202
  return root.name ? { uri: root.uri, name: root.name } : { uri: root.uri };
181
203
  }
182
204
  async function filterRootsWithinBaseline(roots, baseline, signal) {
183
- const normalizedBaseline = normalizeAllowedDirectories(baseline);
205
+ const normalizedBaseline = normalizeCLIDirectories(baseline);
184
206
  const filtered = [];
185
207
  for (const root of roots) {
186
208
  const normalizedRoot = normalizePath(root);
@@ -204,16 +226,19 @@ async function isRootWithinBaseline(normalizedRoot, baseline, signal) {
204
226
  return false;
205
227
  }
206
228
  }
207
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
208
- let serverInstructions = `
229
+ async function loadServerInstructions() {
230
+ const defaultInstructions = `
209
231
  Filesystem MCP Instructions
210
232
  (Detailed instructions failed to load - check logs)
211
233
  `;
212
- try {
213
- serverInstructions = await fs.readFile(path.join(currentDir, 'instructions.md'), 'utf-8');
214
- }
215
- catch (error) {
216
- console.error('[WARNING] Failed to load instructions.md:', formatUnknownErrorMessage(error));
234
+ try {
235
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
236
+ return await fs.readFile(path.join(currentDir, 'instructions.md'), 'utf-8');
237
+ }
238
+ catch (error) {
239
+ console.error('[WARNING] Failed to load instructions.md:', formatUnknownErrorMessage(error));
240
+ return defaultInstructions;
241
+ }
217
242
  }
218
243
  async function getLocalIconInfo() {
219
244
  const name = 'logo.svg';
@@ -232,6 +257,7 @@ async function getLocalIconInfo() {
232
257
  }
233
258
  export async function createServer(options = {}) {
234
259
  const resourceStore = createInMemoryResourceStore();
260
+ const serverInstructions = await loadServerInstructions();
235
261
  const localIcon = await getLocalIconInfo();
236
262
  const taskStore = new InMemoryTaskStore();
237
263
  const taskMessageQueue = new InMemoryTaskMessageQueue();
@@ -254,29 +280,44 @@ export async function createServer(options = {}) {
254
280
  if (serverInstructions) {
255
281
  serverConfig.instructions = serverInstructions;
256
282
  }
257
- const server = new McpServer({
283
+ const server = new McpServer(withDefaultIcons({
258
284
  name: 'filesystem-mcp',
259
285
  title: 'Filesystem MCP',
260
286
  version: SERVER_VERSION,
261
287
  ...(SERVER_DESCRIPTION ? { description: SERVER_DESCRIPTION } : {}),
262
288
  ...(SERVER_HOMEPAGE ? { websiteUrl: SERVER_HOMEPAGE } : {}),
263
- ...(localIcon
264
- ? {
265
- icons: [
266
- {
267
- src: localIcon.src,
268
- mimeType: localIcon.mimeType,
269
- },
270
- ],
271
- }
272
- : {}),
273
- }, serverConfig);
274
- const rootsManager = new RootsManager(options);
289
+ }, localIcon), serverConfig);
290
+ const loggingState = {
291
+ minimumLevel: 'debug',
292
+ };
293
+ const rootsManager = new RootsManager(options, loggingState);
275
294
  rootsManagers.set(server, rootsManager);
295
+ server.server.setRequestHandler(SetLevelRequestSchema, (req) => {
296
+ loggingState.minimumLevel = req.params.level;
297
+ return {};
298
+ });
276
299
  registerInstructionResource(server, serverInstructions, localIcon);
277
300
  registerGetHelpPrompt(server, serverInstructions, localIcon);
278
301
  registerResultResources(server, resourceStore, localIcon);
279
302
  registerCompletions(server);
303
+ {
304
+ const typedServer = server;
305
+ const origReg = typedServer.registerTool.bind(server);
306
+ typedServer.registerTool = (...regArgs) => {
307
+ const handlerIdx = regArgs.length - 1;
308
+ const origHandler = regArgs[handlerIdx];
309
+ if (typeof origHandler !== 'function')
310
+ return origReg(...regArgs);
311
+ regArgs[handlerIdx] = async (...hArgs) => {
312
+ const r = await origHandler(...hArgs);
313
+ if (!r || typeof r !== 'object')
314
+ return r;
315
+ const record = r;
316
+ return Object.fromEntries(Object.entries(record).filter(([key]) => key !== 'structuredContent'));
317
+ };
318
+ return origReg(...regArgs);
319
+ };
320
+ }
280
321
  registerAllTools(server, {
281
322
  resourceStore,
282
323
  isInitialized: () => rootsManager.isInitialized(),
@@ -3,20 +3,18 @@ import * as path from 'node:path';
3
3
  import { applyPatch } from 'diff';
4
4
  import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
5
5
  import { ErrorCode, McpError } from '../lib/errors.js';
6
- import { atomicWriteFile, createTimedAbortSignal, withAbort, } from '../lib/fs-helpers.js';
7
- import { withToolDiagnostics } from '../lib/observability.js';
6
+ import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
8
7
  import { validateExistingPath } from '../lib/path-validation.js';
9
- import { ApplyPatchInputSchema, ApplyPatchOutputSchema } from '../schemas.js';
10
- import { buildToolErrorResponse, buildToolResponse, withDefaultIcons, withToolErrorHandling, wrapToolHandler, } from './shared.js';
8
+ import { ApplyPatchInputSchema, } from '../schemas.js';
9
+ import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
10
+ import { registerToolTaskIfAvailable } from './task-support.js';
11
11
  const APPLY_PATCH_TOOL = {
12
12
  title: 'Apply Patch',
13
- description: 'Apply a unified patch to a file.',
13
+ description: 'Apply a unified diff patch to a file. ' +
14
+ 'Generate the patch with `diff_files`, then validate with `dryRun: true` before writing. ' +
15
+ 'On failure, regenerate a fresh patch via `diff_files` against the current file content and retry.',
14
16
  inputSchema: ApplyPatchInputSchema,
15
- outputSchema: ApplyPatchOutputSchema,
16
- annotations: {
17
- readOnlyHint: false,
18
- openWorldHint: false,
19
- },
17
+ annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
20
18
  };
21
19
  function assertPatchTargetSizeWithinLimit(filePath, size, maxFileSize) {
22
20
  if (size <= maxFileSize)
@@ -33,19 +31,19 @@ function assertPatchHasHunks(patch) {
33
31
  }
34
32
  }
35
33
  async function handleApplyPatch(args, signal) {
36
- const maxFileSize = args.maxFileSize ?? MAX_TEXT_FILE_SIZE;
34
+ const maxFileSize = MAX_TEXT_FILE_SIZE;
37
35
  const validPath = await validateExistingPath(args.path, signal);
38
36
  const stats = await withAbort(fs.stat(validPath), signal);
39
37
  assertPatchTargetSizeWithinLimit(validPath, stats.size, maxFileSize);
40
38
  const content = await fs.readFile(validPath, { encoding: 'utf-8', signal });
41
- const fuzzFactor = args.fuzzFactor ?? (args.fuzzy ? 2 : 0);
39
+ const fuzzFactor = args.fuzzFactor ?? 0;
42
40
  assertPatchHasHunks(args.patch);
43
41
  const patched = applyPatch(content, args.patch, {
44
42
  fuzzFactor,
45
43
  autoConvertLineEndings: args.autoConvertLineEndings,
46
44
  });
47
45
  if (patched === false) {
48
- throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch application failed. The file content may have changed or patch context is insufficient. Generate a fresh patch via diff_files against the current file, then retry. If differences are minor, enable fuzzy matching (fuzzy=true or fuzzFactor).');
46
+ throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch application failed. The file content may have changed or patch context is insufficient. Generate a fresh patch via diff_files against the current file, then retry. If differences are minor, enable fuzzy matching with the fuzzFactor parameter.');
49
47
  }
50
48
  if (args.dryRun) {
51
49
  return buildToolResponse('Dry run successful. Patch can be applied.', {
@@ -62,20 +60,22 @@ async function handleApplyPatch(args, signal) {
62
60
  });
63
61
  }
64
62
  export function registerApplyPatchTool(server, options = {}) {
65
- const handler = (args, extra) => withToolDiagnostics('apply_patch', () => withToolErrorHandling(async () => {
66
- const { signal, cleanup } = createTimedAbortSignal(extra.signal);
67
- try {
68
- return await handleApplyPatch(args, signal);
69
- }
70
- finally {
71
- cleanup();
72
- }
73
- }, (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path)), { path: args.path });
74
- server.registerTool('apply_patch', withDefaultIcons({ ...APPLY_PATCH_TOOL }, options.iconInfo), wrapToolHandler(handler, {
63
+ const handler = (args, extra) => executeToolWithDiagnostics({
64
+ toolName: 'apply_patch',
65
+ extra,
66
+ timedSignal: {},
67
+ context: { path: args.path },
68
+ run: (signal) => handleApplyPatch(args, signal),
69
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
70
+ });
71
+ const wrappedHandler = wrapToolHandler(handler, {
75
72
  guard: options.isInitialized,
76
73
  progressMessage: (args) => {
77
74
  const name = path.basename(args.path);
78
75
  return `🛠 apply_patch: ${name}`;
79
76
  },
80
- }));
77
+ });
78
+ if (registerToolTaskIfAvailable(server, 'apply_patch', APPLY_PATCH_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
79
+ return;
80
+ server.registerTool('apply_patch', withDefaultIcons({ ...APPLY_PATCH_TOOL }, options.iconInfo), wrappedHandler);
81
81
  }