@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.
- package/README.md +514 -188
- package/dist/cli.js +29 -12
- package/dist/completions.js +50 -24
- package/dist/config.d.ts +3 -2
- package/dist/config.js +1 -1
- package/dist/index.js +14 -12
- package/dist/instructions.md +109 -97
- package/dist/lib/constants.js +25 -14
- package/dist/lib/errors.js +11 -6
- package/dist/lib/file-operations/common.d.ts +4 -0
- package/dist/lib/file-operations/common.js +9 -0
- package/dist/lib/file-operations/file-info.js +22 -10
- package/dist/lib/file-operations/gitignore.js +14 -11
- package/dist/lib/file-operations/glob-engine.d.ts +1 -0
- package/dist/lib/file-operations/glob-engine.js +46 -33
- package/dist/lib/file-operations/list-directory.js +31 -35
- package/dist/lib/file-operations/read-multiple-files.js +70 -62
- package/dist/lib/file-operations/search-content.js +83 -64
- package/dist/lib/file-operations/search-files.js +32 -30
- package/dist/lib/file-operations/search-worker.js +22 -12
- package/dist/lib/file-operations/tree.js +43 -34
- package/dist/lib/fs-helpers.js +61 -124
- package/dist/lib/observability.js +29 -28
- package/dist/lib/path-format.d.ts +1 -0
- package/dist/lib/path-format.js +7 -0
- package/dist/lib/path-policy.js +22 -20
- package/dist/lib/path-validation.js +13 -7
- package/dist/lib/resource-store.d.ts +2 -0
- package/dist/lib/resource-store.js +26 -5
- package/dist/lib/type-guards.d.ts +1 -0
- package/dist/lib/type-guards.js +3 -0
- package/dist/prompts.d.ts +1 -5
- package/dist/prompts.js +9 -16
- package/dist/resources.d.ts +1 -5
- package/dist/resources.js +12 -26
- package/dist/schemas.d.ts +213 -30
- package/dist/schemas.js +52 -90
- package/dist/server.js +85 -44
- package/dist/tools/apply-patch.js +24 -24
- package/dist/tools/calculate-hash.js +42 -45
- package/dist/tools/create-directory.js +18 -21
- package/dist/tools/delete-file.js +36 -39
- package/dist/tools/diff-files.js +16 -21
- package/dist/tools/edit-file.js +16 -20
- package/dist/tools/list-directory.js +25 -25
- package/dist/tools/move-file.js +18 -21
- package/dist/tools/read-multiple.js +56 -68
- package/dist/tools/read.js +27 -32
- package/dist/tools/replace-in-files.js +28 -35
- package/dist/tools/roots.js +9 -10
- package/dist/tools/search-content.js +74 -74
- package/dist/tools/search-files.js +45 -52
- package/dist/tools/shared.d.ts +44 -6
- package/dist/tools/shared.js +86 -64
- package/dist/tools/stat-many.js +45 -68
- package/dist/tools/stat.js +11 -39
- package/dist/tools/task-support.d.ts +9 -1
- package/dist/tools/task-support.js +86 -81
- package/dist/tools/tree.js +13 -30
- package/dist/tools/write-file.js +18 -21
- package/dist/tools.js +23 -18
- 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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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('
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
|
500
|
-
|
|
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
|
|
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
|
|
604
|
-
|
|
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
|
-
|
|
618
|
-
.
|
|
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
|
-
.
|
|
624
|
-
|
|
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
|
-
|
|
643
|
-
|
|
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
|
|
646
|
-
|
|
647
|
-
.
|
|
648
|
-
.
|
|
649
|
-
.
|
|
650
|
-
|
|
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
|
-
.
|
|
653
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.
|
|
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
|
|
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}`,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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,
|
|
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,
|
|
10
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
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
|
-
|
|
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 =
|
|
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 ??
|
|
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
|
|
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) =>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
}
|