@j0hanz/filesystem-mcp 1.5.3 → 1.6.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/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.js +5 -0
- package/dist/lib/fs-helpers.js +1 -6
- package/dist/schemas.d.ts +20 -8
- package/dist/schemas.js +54 -10
- package/dist/tools/create-directory.d.ts +4 -1
- package/dist/tools/create-directory.js +21 -14
- package/dist/tools/edit-file.d.ts +4 -1
- package/dist/tools/edit-file.js +46 -17
- package/dist/tools/move-file.d.ts +4 -1
- package/dist/tools/move-file.js +93 -22
- package/dist/tools/read-multiple.js +1 -1
- package/dist/tools/replace-in-files.d.ts +7 -1
- package/dist/tools/replace-in-files.js +7 -4
- package/dist/tools/search-content.js +1 -1
- package/dist/tools/search-files.js +1 -1
- package/dist/tools/shared.js +6 -8
- package/dist/tools/stat-many.js +1 -1
- package/dist/tools/tree.js +1 -1
- package/package.json +1 -1
package/dist/lib/errors.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ interface DetailedError {
|
|
|
9
9
|
}
|
|
10
10
|
export declare function isNodeError(error: unknown): error is NodeJS.ErrnoException;
|
|
11
11
|
export declare function formatUnknownErrorMessage(error: unknown): string;
|
|
12
|
+
export declare function normalizeUnknownError(error: unknown): Error;
|
|
12
13
|
export declare function isAbortError(error: unknown): boolean;
|
|
13
14
|
export declare function isTimeoutLikeError(error: unknown): boolean;
|
|
14
15
|
export declare class McpError extends Error {
|
package/dist/lib/errors.js
CHANGED
|
@@ -94,6 +94,11 @@ export function formatUnknownErrorMessage(error) {
|
|
|
94
94
|
return String(error);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
+
export function normalizeUnknownError(error) {
|
|
98
|
+
return error instanceof Error
|
|
99
|
+
? error
|
|
100
|
+
: new Error(formatUnknownErrorMessage(error));
|
|
101
|
+
}
|
|
97
102
|
const NODE_ERROR_CODE_MAP = {
|
|
98
103
|
ENOENT: ErrorCode.E_NOT_FOUND,
|
|
99
104
|
EACCES: ErrorCode.E_PERMISSION_DENIED,
|
package/dist/lib/fs-helpers.js
CHANGED
|
@@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
5
5
|
import { Writable } from 'node:stream';
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
7
|
import { BINARY_CHECK_BUFFER_SIZE, KNOWN_BINARY_EXTENSIONS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from './constants.js';
|
|
8
|
-
import { ErrorCode,
|
|
8
|
+
import { ErrorCode, McpError, normalizeUnknownError } from './errors.js';
|
|
9
9
|
import { assertAllowedFileAccess } from './path-policy.js';
|
|
10
10
|
import { validateExistingPath } from './path-validation.js';
|
|
11
11
|
function createAbortError(message = 'Operation aborted') {
|
|
@@ -20,11 +20,6 @@ function normalizeAbortReason(reason, message) {
|
|
|
20
20
|
function isFiniteNumber(value) {
|
|
21
21
|
return typeof value === 'number' && Number.isFinite(value);
|
|
22
22
|
}
|
|
23
|
-
function normalizeUnknownError(error) {
|
|
24
|
-
return error instanceof Error
|
|
25
|
-
? error
|
|
26
|
-
: new Error(formatUnknownErrorMessage(error));
|
|
27
|
-
}
|
|
28
23
|
export function assertNotAborted(signal, message) {
|
|
29
24
|
if (!signal)
|
|
30
25
|
return;
|
package/dist/schemas.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export declare const ToolErrorResponseSchema: z.ZodObject<{
|
|
|
33
33
|
suggestion: z.ZodOptional<z.ZodString>;
|
|
34
34
|
}, z.core.$strict>;
|
|
35
35
|
}, z.core.$strict>;
|
|
36
|
+
declare const HeadLinesSchema: z.ZodOptional<z.ZodInt>;
|
|
37
|
+
declare const LineNumberSchema: z.ZodNumber;
|
|
36
38
|
export declare const ListDirectoryInputSchema: z.ZodObject<{
|
|
37
39
|
path: z.ZodOptional<z.ZodString>;
|
|
38
40
|
includeHidden: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
@@ -85,16 +87,16 @@ export declare const SearchContentInputSchema: z.ZodObject<{
|
|
|
85
87
|
includeIgnored: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
86
88
|
}, z.core.$strict>;
|
|
87
89
|
export declare const ReadFileInputSchema: z.ZodObject<{
|
|
90
|
+
head: typeof HeadLinesSchema;
|
|
91
|
+
startLine: z.ZodOptional<typeof LineNumberSchema>;
|
|
92
|
+
endLine: z.ZodOptional<typeof LineNumberSchema>;
|
|
88
93
|
path: z.ZodString;
|
|
89
|
-
head: z.ZodOptional<z.ZodInt>;
|
|
90
|
-
startLine: z.ZodOptional<z.ZodNumber>;
|
|
91
|
-
endLine: z.ZodOptional<z.ZodNumber>;
|
|
92
94
|
}, z.core.$strict>;
|
|
93
95
|
export declare const ReadMultipleFilesInputSchema: z.ZodObject<{
|
|
96
|
+
head: typeof HeadLinesSchema;
|
|
97
|
+
startLine: z.ZodOptional<typeof LineNumberSchema>;
|
|
98
|
+
endLine: z.ZodOptional<typeof LineNumberSchema>;
|
|
94
99
|
paths: z.ZodArray<z.ZodString>;
|
|
95
|
-
head: z.ZodOptional<z.ZodInt>;
|
|
96
|
-
startLine: z.ZodOptional<z.ZodNumber>;
|
|
97
|
-
endLine: z.ZodOptional<z.ZodNumber>;
|
|
98
100
|
}, z.core.$strict>;
|
|
99
101
|
export declare const GetFileInfoInputSchema: z.ZodObject<{
|
|
100
102
|
path: z.ZodString;
|
|
@@ -472,11 +474,13 @@ export declare const GetMultipleFileInfoOutputSchema: z.ZodObject<{
|
|
|
472
474
|
}, z.core.$strict>>;
|
|
473
475
|
}, z.core.$strict>;
|
|
474
476
|
export declare const CreateDirectoryInputSchema: z.ZodObject<{
|
|
475
|
-
path: z.ZodString
|
|
477
|
+
path: z.ZodOptional<z.ZodString>;
|
|
478
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
476
479
|
}, z.core.$strict>;
|
|
477
480
|
export declare const CreateDirectoryOutputSchema: z.ZodObject<{
|
|
478
481
|
ok: z.ZodBoolean;
|
|
479
482
|
path: z.ZodOptional<z.ZodString>;
|
|
483
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
480
484
|
error: z.ZodOptional<z.ZodObject<{
|
|
481
485
|
code: z.ZodEnum<{
|
|
482
486
|
readonly E_ACCESS_DENIED: "E_ACCESS_DENIED";
|
|
@@ -532,6 +536,7 @@ export declare const EditFileInputSchema: z.ZodObject<{
|
|
|
532
536
|
newText: z.ZodString;
|
|
533
537
|
}, z.core.$strict>>;
|
|
534
538
|
dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
539
|
+
ignoreWhitespace: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
535
540
|
}, z.core.$strict>;
|
|
536
541
|
export declare const EditFileOutputSchema: z.ZodObject<{
|
|
537
542
|
ok: z.ZodBoolean;
|
|
@@ -560,13 +565,19 @@ export declare const EditFileOutputSchema: z.ZodObject<{
|
|
|
560
565
|
}, z.core.$strict>>;
|
|
561
566
|
}, z.core.$strict>;
|
|
562
567
|
export declare const MoveFileInputSchema: z.ZodObject<{
|
|
563
|
-
source: z.ZodString
|
|
568
|
+
source: z.ZodOptional<z.ZodString>;
|
|
569
|
+
sources: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
564
570
|
destination: z.ZodString;
|
|
565
571
|
}, z.core.$strict>;
|
|
566
572
|
export declare const MoveFileOutputSchema: z.ZodObject<{
|
|
567
573
|
ok: z.ZodBoolean;
|
|
568
574
|
source: z.ZodOptional<z.ZodString>;
|
|
575
|
+
sources: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
569
576
|
destination: z.ZodOptional<z.ZodString>;
|
|
577
|
+
failed: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
578
|
+
source: z.ZodString;
|
|
579
|
+
error: z.ZodString;
|
|
580
|
+
}, z.core.$strict>>>;
|
|
570
581
|
error: z.ZodOptional<z.ZodObject<{
|
|
571
582
|
code: z.ZodEnum<{
|
|
572
583
|
readonly E_ACCESS_DENIED: "E_ACCESS_DENIED";
|
|
@@ -717,6 +728,7 @@ export declare const SearchAndReplaceInputSchema: z.ZodObject<{
|
|
|
717
728
|
dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
718
729
|
includeHidden: z.ZodOptional<z.ZodBoolean>;
|
|
719
730
|
includeIgnored: z.ZodOptional<z.ZodBoolean>;
|
|
731
|
+
returnDiff: z.ZodOptional<z.ZodBoolean>;
|
|
720
732
|
}, z.core.$strict>;
|
|
721
733
|
export declare const SearchAndReplaceOutputSchema: z.ZodObject<{
|
|
722
734
|
ok: z.ZodBoolean;
|
package/dist/schemas.js
CHANGED
|
@@ -81,6 +81,13 @@ const validateReadRange = (value, ctx) => {
|
|
|
81
81
|
addReadRangeIssue(ctx, 'endLine', "'endLine' must be >= 'startLine'");
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
|
+
function createReadRangeInputFields(descriptions) {
|
|
85
|
+
return {
|
|
86
|
+
head: HeadLinesSchema.describe(descriptions.head),
|
|
87
|
+
startLine: LineNumberSchema.optional().describe(descriptions.startLine),
|
|
88
|
+
endLine: LineNumberSchema.optional().describe(descriptions.endLine),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
84
91
|
const FileInfoSchema = z.strictObject({
|
|
85
92
|
name: z.string().describe('Name'),
|
|
86
93
|
path: z.string().describe('Absolute path'),
|
|
@@ -280,9 +287,11 @@ export const SearchContentInputSchema = z.strictObject({
|
|
|
280
287
|
export const ReadFileInputSchema = z
|
|
281
288
|
.strictObject({
|
|
282
289
|
path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
290
|
+
...createReadRangeInputFields({
|
|
291
|
+
head: 'Read first N lines (preview)',
|
|
292
|
+
startLine: 'Start line (1-based, inclusive)',
|
|
293
|
+
endLine: 'End line (1-based, inclusive). Requires startLine.',
|
|
294
|
+
}),
|
|
286
295
|
})
|
|
287
296
|
.superRefine(validateReadRange);
|
|
288
297
|
export const ReadMultipleFilesInputSchema = z
|
|
@@ -292,9 +301,11 @@ export const ReadMultipleFilesInputSchema = z
|
|
|
292
301
|
.min(1, 'Min 1 path required')
|
|
293
302
|
.max(100, 'Max 100 files')
|
|
294
303
|
.describe('Files to read. e.g. ["src/index.ts"]'),
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
304
|
+
...createReadRangeInputFields({
|
|
305
|
+
head: 'Read first N lines of each file',
|
|
306
|
+
startLine: 'Start line (1-based, inclusive) per file',
|
|
307
|
+
endLine: 'End line (1-based, inclusive) per file. Requires startLine.',
|
|
308
|
+
}),
|
|
298
309
|
})
|
|
299
310
|
.superRefine(validateReadRange);
|
|
300
311
|
export const GetFileInfoInputSchema = z.strictObject({
|
|
@@ -459,12 +470,22 @@ export const GetMultipleFileInfoOutputSchema = z.strictObject({
|
|
|
459
470
|
summary: OperationSummarySchema.optional(),
|
|
460
471
|
error: ErrorSchema.optional(),
|
|
461
472
|
});
|
|
462
|
-
export const CreateDirectoryInputSchema = z
|
|
463
|
-
|
|
473
|
+
export const CreateDirectoryInputSchema = z
|
|
474
|
+
.strictObject({
|
|
475
|
+
path: RequiredPathSchema.optional().describe(DESC_PATH_REQUIRED),
|
|
476
|
+
paths: z
|
|
477
|
+
.array(RequiredPathSchema)
|
|
478
|
+
.optional()
|
|
479
|
+
.describe('Absolute paths to directories to create'),
|
|
480
|
+
})
|
|
481
|
+
.refine((data) => data.path !== undefined || data.paths !== undefined, {
|
|
482
|
+
message: "Either 'path' or 'paths' must be provided",
|
|
483
|
+
path: ['path'],
|
|
464
484
|
});
|
|
465
485
|
export const CreateDirectoryOutputSchema = z.strictObject({
|
|
466
486
|
ok: z.boolean(),
|
|
467
487
|
path: z.string().optional(),
|
|
488
|
+
paths: z.array(z.string()).optional(),
|
|
468
489
|
error: ErrorSchema.optional(),
|
|
469
490
|
});
|
|
470
491
|
export const WriteFileInputSchema = z.strictObject({
|
|
@@ -495,6 +516,11 @@ export const EditFileInputSchema = z.strictObject({
|
|
|
495
516
|
.optional()
|
|
496
517
|
.default(false)
|
|
497
518
|
.describe('Preview edits without writing. Check unmatchedEdits in the response to verify all oldText values were found.'),
|
|
519
|
+
ignoreWhitespace: z
|
|
520
|
+
.boolean()
|
|
521
|
+
.optional()
|
|
522
|
+
.default(false)
|
|
523
|
+
.describe('Ignore leading/trailing whitespace and treat all whitespace sequences as equivalent when matching oldText.'),
|
|
498
524
|
});
|
|
499
525
|
export const EditFileOutputSchema = z.strictObject({
|
|
500
526
|
ok: z.boolean(),
|
|
@@ -510,14 +536,28 @@ export const EditFileOutputSchema = z.strictObject({
|
|
|
510
536
|
.describe('Edits that could not be applied'),
|
|
511
537
|
error: ErrorSchema.optional(),
|
|
512
538
|
});
|
|
513
|
-
export const MoveFileInputSchema = z
|
|
514
|
-
|
|
539
|
+
export const MoveFileInputSchema = z
|
|
540
|
+
.strictObject({
|
|
541
|
+
source: RequiredPathSchema.optional().describe('Path to move (deprecated: use sources)'),
|
|
542
|
+
sources: z.array(RequiredPathSchema).optional().describe('Paths to move'),
|
|
515
543
|
destination: RequiredPathSchema.describe('New path'),
|
|
544
|
+
})
|
|
545
|
+
.refine((data) => (data.source ?? data.sources) !== undefined, {
|
|
546
|
+
message: "Either 'source' or 'sources' must be provided",
|
|
547
|
+
path: ['source'],
|
|
516
548
|
});
|
|
517
549
|
export const MoveFileOutputSchema = z.strictObject({
|
|
518
550
|
ok: z.boolean(),
|
|
519
551
|
source: z.string().optional(),
|
|
552
|
+
sources: z.array(z.string()).optional(),
|
|
520
553
|
destination: z.string().optional(),
|
|
554
|
+
failed: z
|
|
555
|
+
.array(z.strictObject({
|
|
556
|
+
source: z.string().describe('Source path'),
|
|
557
|
+
error: z.string().describe('Error message'),
|
|
558
|
+
}))
|
|
559
|
+
.optional()
|
|
560
|
+
.describe('List of files that failed to move'),
|
|
521
561
|
error: ErrorSchema.optional(),
|
|
522
562
|
});
|
|
523
563
|
export const DeleteFileInputSchema = z.strictObject({
|
|
@@ -643,6 +683,10 @@ export const SearchAndReplaceInputSchema = z.strictObject({
|
|
|
643
683
|
.boolean()
|
|
644
684
|
.optional()
|
|
645
685
|
.describe('Include files and directories ignored by .gitignore rules (e.g. node_modules, dist). Default: false.'),
|
|
686
|
+
returnDiff: z
|
|
687
|
+
.boolean()
|
|
688
|
+
.optional()
|
|
689
|
+
.describe('Return unified diff of changes even if dryRun is false. Default: false.'),
|
|
646
690
|
});
|
|
647
691
|
export const SearchAndReplaceOutputSchema = z.strictObject({
|
|
648
692
|
ok: z.boolean(),
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import { CreateDirectoryInputSchema, CreateDirectoryOutputSchema } from '../schemas.js';
|
|
4
|
+
import { type ToolContract, type ToolRegistrationOptions, type ToolResponse } from './shared.js';
|
|
3
5
|
export declare const CREATE_DIRECTORY_TOOL: ToolContract;
|
|
6
|
+
export declare function handleCreateDirectory(args: z.infer<typeof CreateDirectoryInputSchema>, signal?: AbortSignal): Promise<ToolResponse<z.infer<typeof CreateDirectoryOutputSchema>>>;
|
|
4
7
|
export declare function registerCreateDirectoryTool(server: McpServer, options?: ToolRegistrationOptions): void;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
|
-
import
|
|
3
|
-
import { ErrorCode } from '../lib/errors.js';
|
|
2
|
+
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
4
3
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
5
4
|
import { validatePathForWrite } from '../lib/path-validation.js';
|
|
6
5
|
import { CreateDirectoryInputSchema, CreateDirectoryOutputSchema, } from '../schemas.js';
|
|
@@ -15,12 +14,20 @@ export const CREATE_DIRECTORY_TOOL = {
|
|
|
15
14
|
annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
|
|
16
15
|
nuances: ['Succeeds silently if the directory already exists (idempotent).'],
|
|
17
16
|
};
|
|
18
|
-
async function handleCreateDirectory(args, signal) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
export async function handleCreateDirectory(args, signal) {
|
|
18
|
+
const allPaths = [];
|
|
19
|
+
if (args.path)
|
|
20
|
+
allPaths.push(args.path);
|
|
21
|
+
if (args.paths)
|
|
22
|
+
allPaths.push(...args.paths);
|
|
23
|
+
if (allPaths.length === 0) {
|
|
24
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'No paths provided to create.');
|
|
25
|
+
}
|
|
26
|
+
const validPaths = await Promise.all(allPaths.map((p) => validatePathForWrite(p, signal)));
|
|
27
|
+
await Promise.all(validPaths.map((p) => withAbort(fs.mkdir(p, { recursive: true }), signal)));
|
|
28
|
+
return buildToolResponse(`Successfully created ${validPaths.length} director${validPaths.length === 1 ? 'y' : 'ies'}`, {
|
|
22
29
|
ok: true,
|
|
23
|
-
|
|
30
|
+
paths: validPaths,
|
|
24
31
|
});
|
|
25
32
|
}
|
|
26
33
|
export function registerCreateDirectoryTool(server, options = {}) {
|
|
@@ -28,21 +35,21 @@ export function registerCreateDirectoryTool(server, options = {}) {
|
|
|
28
35
|
toolName: 'mkdir',
|
|
29
36
|
extra,
|
|
30
37
|
timedSignal: {},
|
|
31
|
-
context: { path: args.path },
|
|
38
|
+
context: { path: args.path ?? args.paths?.[0] },
|
|
32
39
|
run: (signal) => handleCreateDirectory(args, signal),
|
|
33
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
|
|
40
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path ?? args.paths?.[0]),
|
|
34
41
|
});
|
|
35
42
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
36
43
|
guard: options.isInitialized,
|
|
37
44
|
progressMessage: (args) => {
|
|
38
|
-
const
|
|
39
|
-
return `🛠 mkdir: ${
|
|
45
|
+
const count = (args.path ? 1 : 0) + (args.paths?.length ?? 0);
|
|
46
|
+
return `🛠 mkdir: ${count} director${count === 1 ? 'y' : 'ies'}`;
|
|
40
47
|
},
|
|
41
48
|
completionMessage: (args, result) => {
|
|
42
|
-
const
|
|
49
|
+
const count = (args.path ? 1 : 0) + (args.paths?.length ?? 0);
|
|
43
50
|
if (result.isError)
|
|
44
|
-
return `🛠 mkdir: ${
|
|
45
|
-
return `🛠 mkdir: ${
|
|
51
|
+
return `🛠 mkdir: ${count} • failed`;
|
|
52
|
+
return `🛠 mkdir: ${count} • created`;
|
|
46
53
|
},
|
|
47
54
|
});
|
|
48
55
|
const validatedHandler = withValidatedArgs(CreateDirectoryInputSchema, wrappedHandler);
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
|
|
4
|
+
import { type ToolContract, type ToolRegistrationOptions, type ToolResponse } from './shared.js';
|
|
3
5
|
export declare const EDIT_FILE_TOOL: ToolContract;
|
|
6
|
+
export declare function handleEditFile(args: z.infer<typeof EditFileInputSchema>, signal?: AbortSignal): Promise<ToolResponse<z.infer<typeof EditFileOutputSchema>>>;
|
|
4
7
|
export declare function registerEditFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -23,28 +23,57 @@ export const EDIT_FILE_TOOL = {
|
|
|
23
23
|
'`oldText` must match exactly; unmatched items are reported in `unmatchedEdits`.',
|
|
24
24
|
],
|
|
25
25
|
};
|
|
26
|
-
function
|
|
26
|
+
function escapeRegExp(string) {
|
|
27
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
}
|
|
29
|
+
function applyEdits(content, edits, ignoreWhitespace) {
|
|
27
30
|
let newContent = content;
|
|
28
31
|
let appliedEdits = 0;
|
|
29
32
|
const unmatchedEdits = [];
|
|
30
33
|
let minLine;
|
|
31
34
|
let maxLine;
|
|
32
35
|
for (const edit of edits) {
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
if (ignoreWhitespace) {
|
|
37
|
+
const pattern = escapeRegExp(edit.oldText).replace(/\s+/g, '\\s+');
|
|
38
|
+
const regex = new RegExp(pattern);
|
|
39
|
+
const match = regex.exec(newContent);
|
|
40
|
+
if (!match) {
|
|
41
|
+
unmatchedEdits.push(edit.oldText);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const { index } = match;
|
|
45
|
+
const matchLength = match[0].length;
|
|
46
|
+
const linesBefore = newContent.slice(0, index).split('\n').length;
|
|
47
|
+
const newTextLines = edit.newText.split('\n').length;
|
|
48
|
+
const startLine = linesBefore;
|
|
49
|
+
const endLine = linesBefore + newTextLines - 1;
|
|
50
|
+
if (minLine === undefined || startLine < minLine)
|
|
51
|
+
minLine = startLine;
|
|
52
|
+
if (maxLine === undefined || endLine > maxLine)
|
|
53
|
+
maxLine = endLine;
|
|
54
|
+
newContent =
|
|
55
|
+
newContent.slice(0, index) +
|
|
56
|
+
edit.newText +
|
|
57
|
+
newContent.slice(index + matchLength);
|
|
58
|
+
appliedEdits += 1;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
if (!newContent.includes(edit.oldText)) {
|
|
62
|
+
unmatchedEdits.push(edit.oldText);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const index = newContent.indexOf(edit.oldText);
|
|
66
|
+
const linesBefore = newContent.slice(0, index).split('\n').length;
|
|
67
|
+
const newTextLines = edit.newText.split('\n').length;
|
|
68
|
+
const startLine = linesBefore;
|
|
69
|
+
const endLine = linesBefore + newTextLines - 1;
|
|
70
|
+
if (minLine === undefined || startLine < minLine)
|
|
71
|
+
minLine = startLine;
|
|
72
|
+
if (maxLine === undefined || endLine > maxLine)
|
|
73
|
+
maxLine = endLine;
|
|
74
|
+
newContent = newContent.replace(edit.oldText, () => edit.newText);
|
|
75
|
+
appliedEdits += 1;
|
|
36
76
|
}
|
|
37
|
-
const index = newContent.indexOf(edit.oldText);
|
|
38
|
-
const linesBefore = newContent.slice(0, index).split('\n').length;
|
|
39
|
-
const newTextLines = edit.newText.split('\n').length;
|
|
40
|
-
const startLine = linesBefore;
|
|
41
|
-
const endLine = linesBefore + newTextLines - 1;
|
|
42
|
-
if (minLine === undefined || startLine < minLine)
|
|
43
|
-
minLine = startLine;
|
|
44
|
-
if (maxLine === undefined || endLine > maxLine)
|
|
45
|
-
maxLine = endLine;
|
|
46
|
-
newContent = newContent.replace(edit.oldText, () => edit.newText);
|
|
47
|
-
appliedEdits += 1;
|
|
48
77
|
}
|
|
49
78
|
const result = {
|
|
50
79
|
content: newContent,
|
|
@@ -56,10 +85,10 @@ function applyEdits(content, edits) {
|
|
|
56
85
|
}
|
|
57
86
|
return result;
|
|
58
87
|
}
|
|
59
|
-
async function handleEditFile(args, signal) {
|
|
88
|
+
export async function handleEditFile(args, signal) {
|
|
60
89
|
const validPath = await validateExistingPath(args.path, signal);
|
|
61
90
|
const content = await fs.readFile(validPath, { encoding: 'utf-8', signal });
|
|
62
|
-
const { content: newContent, appliedEdits, unmatchedEdits, lineRange, } = applyEdits(content, args.edits);
|
|
91
|
+
const { content: newContent, appliedEdits, unmatchedEdits, lineRange, } = applyEdits(content, args.edits, args.ignoreWhitespace);
|
|
63
92
|
const structured = {
|
|
64
93
|
ok: true,
|
|
65
94
|
path: validPath,
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import { MoveFileInputSchema, MoveFileOutputSchema } from '../schemas.js';
|
|
4
|
+
import { type ToolContract, type ToolRegistrationOptions, type ToolResponse } from './shared.js';
|
|
3
5
|
export declare const MOVE_FILE_TOOL: ToolContract;
|
|
6
|
+
export declare function handleMoveFile(args: z.infer<typeof MoveFileInputSchema>, signal?: AbortSignal): Promise<ToolResponse<z.infer<typeof MoveFileOutputSchema>>>;
|
|
4
7
|
export declare function registerMoveFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
|
package/dist/tools/move-file.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { ErrorCode, isNodeError } from '../lib/errors.js';
|
|
3
|
+
import { ErrorCode, formatUnknownErrorMessage, isNodeError, McpError, } from '../lib/errors.js';
|
|
4
4
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
5
5
|
import { validateExistingPath, validatePathForWrite, } from '../lib/path-validation.js';
|
|
6
6
|
import { MoveFileInputSchema, MoveFileOutputSchema } from '../schemas.js';
|
|
@@ -18,28 +18,95 @@ export const MOVE_FILE_TOOL = {
|
|
|
18
18
|
'On POSIX, an existing destination is silently overwritten; on Windows, rename fails with EEXIST if destination exists.',
|
|
19
19
|
],
|
|
20
20
|
};
|
|
21
|
-
async function handleMoveFile(args, signal) {
|
|
22
|
-
const
|
|
21
|
+
export async function handleMoveFile(args, signal) {
|
|
22
|
+
const sources = args.sources ?? (args.source ? [args.source] : []);
|
|
23
|
+
if (sources.length === 0) {
|
|
24
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'No sources provided.');
|
|
25
|
+
}
|
|
23
26
|
const validDest = await validatePathForWrite(args.destination, signal);
|
|
24
|
-
//
|
|
25
|
-
|
|
27
|
+
// Check if destination exists and is a directory
|
|
28
|
+
let destIsDirectory = false;
|
|
26
29
|
try {
|
|
27
|
-
await
|
|
30
|
+
const stats = await fs.stat(validDest);
|
|
31
|
+
destIsDirectory = stats.isDirectory();
|
|
28
32
|
}
|
|
29
33
|
catch (error) {
|
|
30
|
-
if (isNodeError(error) && error.code
|
|
31
|
-
// Cross-device link, fallback to copy + delete
|
|
32
|
-
await withAbort(fs.cp(validSource, validDest, { recursive: true }), signal);
|
|
33
|
-
await withAbort(fs.rm(validSource, { recursive: true, force: true }), signal);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
34
|
+
if (isNodeError(error) && error.code !== 'ENOENT') {
|
|
36
35
|
throw error;
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
if (sources.length > 1 && !destIsDirectory) {
|
|
39
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Destination must be an existing directory when moving multiple files.');
|
|
40
|
+
}
|
|
41
|
+
// Ensure destination parent directory exists if it's not an existing directory
|
|
42
|
+
if (!destIsDirectory) {
|
|
43
|
+
await withAbort(fs.mkdir(path.dirname(validDest), { recursive: true }), signal);
|
|
44
|
+
}
|
|
45
|
+
const movedSources = [];
|
|
46
|
+
const failed = [];
|
|
47
|
+
for (const src of sources) {
|
|
48
|
+
let validSource;
|
|
49
|
+
try {
|
|
50
|
+
validSource = await validateExistingPath(src, signal);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
failed.push({
|
|
54
|
+
source: src,
|
|
55
|
+
error: formatUnknownErrorMessage(error),
|
|
56
|
+
});
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const targetPath = destIsDirectory
|
|
60
|
+
? path.join(validDest, path.basename(validSource))
|
|
61
|
+
: validDest;
|
|
62
|
+
// Prevent moving a file onto itself
|
|
63
|
+
if (path.resolve(validSource) === path.resolve(targetPath)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Prevent moving a directory into its own subdirectory
|
|
67
|
+
// Fixes "Missing validation for moving directory into its own subdirectory" finding
|
|
68
|
+
if (path.resolve(targetPath).startsWith(path.resolve(validSource) + path.sep)) {
|
|
69
|
+
failed.push({
|
|
70
|
+
source: src,
|
|
71
|
+
error: `Cannot move directory '${src}' into its own subdirectory '${targetPath}'`,
|
|
72
|
+
});
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
await withAbort(fs.rename(validSource, targetPath), signal);
|
|
77
|
+
movedSources.push(validSource);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (isNodeError(error) && error.code === 'EXDEV') {
|
|
81
|
+
// Cross-device link, fallback to copy + delete
|
|
82
|
+
try {
|
|
83
|
+
await withAbort(fs.cp(validSource, targetPath, { recursive: true }), signal);
|
|
84
|
+
await withAbort(fs.rm(validSource, { recursive: true, force: true }), signal);
|
|
85
|
+
movedSources.push(validSource);
|
|
86
|
+
}
|
|
87
|
+
catch (copyError) {
|
|
88
|
+
failed.push({
|
|
89
|
+
source: src,
|
|
90
|
+
error: formatUnknownErrorMessage(copyError),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
failed.push({
|
|
96
|
+
source: src,
|
|
97
|
+
error: formatUnknownErrorMessage(error),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const message = failed.length > 0
|
|
103
|
+
? `Moved ${movedSources.length} item${movedSources.length === 1 ? '' : 's'}; failed to move ${failed.length} item${failed.length === 1 ? '' : 's'}`
|
|
104
|
+
: `Successfully moved ${movedSources.length} item${movedSources.length === 1 ? '' : 's'} to ${args.destination}`;
|
|
105
|
+
return buildToolResponse(message, {
|
|
106
|
+
ok: failed.length === 0,
|
|
107
|
+
sources: movedSources,
|
|
42
108
|
destination: validDest,
|
|
109
|
+
...(failed.length > 0 ? { failed } : {}),
|
|
43
110
|
});
|
|
44
111
|
}
|
|
45
112
|
export function registerMoveFileTool(server, options = {}) {
|
|
@@ -47,19 +114,23 @@ export function registerMoveFileTool(server, options = {}) {
|
|
|
47
114
|
toolName: 'mv',
|
|
48
115
|
extra,
|
|
49
116
|
timedSignal: {},
|
|
50
|
-
context: { path: args.source },
|
|
117
|
+
context: { path: args.source ?? args.sources?.[0] },
|
|
51
118
|
run: (signal) => handleMoveFile(args, signal),
|
|
52
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.source),
|
|
119
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.source ?? args.sources?.[0]),
|
|
53
120
|
});
|
|
54
121
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
55
122
|
guard: options.isInitialized,
|
|
56
|
-
progressMessage: (args) =>
|
|
123
|
+
progressMessage: (args) => {
|
|
124
|
+
const count = (args.source ? 1 : 0) + (args.sources?.length ?? 0);
|
|
125
|
+
const dest = path.basename(args.destination);
|
|
126
|
+
return `🛠 mv: ${count} item${count === 1 ? '' : 's'} → ${dest}`;
|
|
127
|
+
},
|
|
57
128
|
completionMessage: (args, result) => {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
129
|
+
const count = (args.source ? 1 : 0) + (args.sources?.length ?? 0);
|
|
130
|
+
const dest = path.basename(args.destination);
|
|
60
131
|
if (result.isError)
|
|
61
|
-
return `🛠 mv: ${
|
|
62
|
-
return `🛠 mv: ${
|
|
132
|
+
return `🛠 mv: ${count} → ${dest} • failed`;
|
|
133
|
+
return `🛠 mv: ${count} → ${dest} • moved`;
|
|
63
134
|
},
|
|
64
135
|
});
|
|
65
136
|
const validatedHandler = withValidatedArgs(MoveFileInputSchema, wrappedHandler);
|
|
@@ -14,7 +14,7 @@ export const READ_MULTIPLE_FILES_TOOL = {
|
|
|
14
14
|
inputSchema: ReadMultipleFilesInputSchema,
|
|
15
15
|
outputSchema: ReadMultipleFilesOutputSchema,
|
|
16
16
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
17
|
-
taskSupport: '
|
|
17
|
+
taskSupport: 'optional',
|
|
18
18
|
nuances: ['Total read budget is capped by `MAX_READ_MANY_TOTAL_SIZE`.'],
|
|
19
19
|
gotchas: [
|
|
20
20
|
'Per-file `truncationReason` can be `head`, `range`, or `externalized`.',
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema } from '../schemas.js';
|
|
4
|
+
import { type ToolContract, type ToolRegistrationOptions, type ToolResponse } from './shared.js';
|
|
3
5
|
export declare const SEARCH_AND_REPLACE_TOOL: ToolContract;
|
|
6
|
+
export declare function handleSearchAndReplace(args: z.infer<typeof SearchAndReplaceInputSchema>, signal?: AbortSignal, onProgress?: (progress: {
|
|
7
|
+
total?: number;
|
|
8
|
+
current: number;
|
|
9
|
+
}) => void): Promise<ToolResponse<z.infer<typeof SearchAndReplaceOutputSchema>>>;
|
|
4
10
|
export declare function registerSearchAndReplaceTool(server: McpServer, options?: ToolRegistrationOptions): void;
|
|
@@ -23,7 +23,7 @@ export const SEARCH_AND_REPLACE_TOOL = {
|
|
|
23
23
|
inputSchema: SearchAndReplaceInputSchema,
|
|
24
24
|
outputSchema: SearchAndReplaceOutputSchema,
|
|
25
25
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
26
|
-
taskSupport: '
|
|
26
|
+
taskSupport: 'optional',
|
|
27
27
|
gotchas: [
|
|
28
28
|
'Literal mode is default; `isRegex=true` enables RE2 + capture replacements (`$1`, `$2`).',
|
|
29
29
|
],
|
|
@@ -122,7 +122,8 @@ async function processEntry(entryPath, args, regex, maxFileSize, signal, summary
|
|
|
122
122
|
else {
|
|
123
123
|
newContent = content.replaceAll(args.searchPattern, () => args.replacement);
|
|
124
124
|
}
|
|
125
|
-
if (args.dryRun
|
|
125
|
+
if ((args.dryRun || args.returnDiff) &&
|
|
126
|
+
summary.diff.length < MAX_DIFF_SIZE) {
|
|
126
127
|
const patch = createTwoFilesPatch(path.basename(validPath), path.basename(validPath), content, newContent, 'Original', 'Modified');
|
|
127
128
|
// Only append if it won't exceed the limit too much
|
|
128
129
|
if (summary.diff.length + patch.length <= MAX_DIFF_SIZE + 1024) {
|
|
@@ -201,7 +202,7 @@ function reportReplaceProgress(onProgress, current, force = false) {
|
|
|
201
202
|
return;
|
|
202
203
|
onProgress({ current });
|
|
203
204
|
}
|
|
204
|
-
async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
205
|
+
export async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
205
206
|
const maxFileSize = MAX_TEXT_FILE_SIZE;
|
|
206
207
|
const root = await resolveSearchRoot(args.path, signal);
|
|
207
208
|
const regex = createReplacementRegex(args);
|
|
@@ -240,7 +241,9 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
|
240
241
|
? { changedFiles: summary.changedFiles }
|
|
241
242
|
: {}),
|
|
242
243
|
...(summary.changedFilesTruncated ? { changedFilesTruncated: true } : {}),
|
|
243
|
-
...(args.dryRun
|
|
244
|
+
...((args.dryRun || args.returnDiff) && summary.diff
|
|
245
|
+
? { diff: summary.diff }
|
|
246
|
+
: {}),
|
|
244
247
|
dryRun: args.dryRun,
|
|
245
248
|
});
|
|
246
249
|
}
|
|
@@ -26,7 +26,7 @@ export const SEARCH_CONTENT_TOOL = {
|
|
|
26
26
|
gotchas: [
|
|
27
27
|
'Skips binary and oversized files silently — check file type with `stat` if no matches appear.',
|
|
28
28
|
],
|
|
29
|
-
taskSupport: '
|
|
29
|
+
taskSupport: 'optional',
|
|
30
30
|
};
|
|
31
31
|
function assertValidRegexPattern(pattern) {
|
|
32
32
|
try {
|
|
@@ -38,7 +38,7 @@ export const SEARCH_FILES_TOOL = {
|
|
|
38
38
|
'Respects `.gitignore` unless `includeIgnored=true`.',
|
|
39
39
|
'Returns relative paths plus metadata; may truncate.',
|
|
40
40
|
],
|
|
41
|
-
taskSupport: '
|
|
41
|
+
taskSupport: 'optional',
|
|
42
42
|
};
|
|
43
43
|
async function handleSearchFiles(args, signal, onProgress) {
|
|
44
44
|
const basePath = resolvePathOrRoot(args.path);
|
package/dist/tools/shared.js
CHANGED
|
@@ -89,12 +89,6 @@ export function buildResourceLink(params) {
|
|
|
89
89
|
...(params.mimeType ? { mimeType: params.mimeType } : {}),
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
|
-
function buildContentBlock(text, structuredContent, extraContent = []) {
|
|
93
|
-
return {
|
|
94
|
-
content: [{ type: 'text', text }, ...extraContent],
|
|
95
|
-
structuredContent,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
92
|
function resolveDetailedError(error, defaultCode, path) {
|
|
99
93
|
const detailed = createDetailedError(error, path);
|
|
100
94
|
if (detailed.code === ErrorCode.E_UNKNOWN) {
|
|
@@ -104,7 +98,10 @@ function resolveDetailedError(error, defaultCode, path) {
|
|
|
104
98
|
return detailed;
|
|
105
99
|
}
|
|
106
100
|
export function buildToolResponse(text, structuredContent, extraContent = []) {
|
|
107
|
-
return
|
|
101
|
+
return {
|
|
102
|
+
content: [{ type: 'text', text }, ...extraContent],
|
|
103
|
+
structuredContent,
|
|
104
|
+
};
|
|
108
105
|
}
|
|
109
106
|
function parseToolArgs(schema, args) {
|
|
110
107
|
const candidate = args === undefined ? {} : args;
|
|
@@ -213,7 +210,8 @@ export function buildToolErrorResponse(error, defaultCode, path) {
|
|
|
213
210
|
error: errorContent,
|
|
214
211
|
};
|
|
215
212
|
return {
|
|
216
|
-
|
|
213
|
+
content: [{ type: 'text', text }],
|
|
214
|
+
structuredContent,
|
|
217
215
|
isError: true,
|
|
218
216
|
};
|
|
219
217
|
}
|
package/dist/tools/stat-many.js
CHANGED
|
@@ -13,7 +13,7 @@ export const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
|
13
13
|
inputSchema: GetMultipleFileInfoInputSchema,
|
|
14
14
|
outputSchema: GetMultipleFileInfoOutputSchema,
|
|
15
15
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
16
|
-
taskSupport: '
|
|
16
|
+
taskSupport: 'optional',
|
|
17
17
|
nuances: ['Use before read/search when file size/type uncertainty exists.'],
|
|
18
18
|
};
|
|
19
19
|
function formatFileInfoDetail(info) {
|
package/dist/tools/tree.js
CHANGED
|
@@ -14,7 +14,7 @@ export const TREE_TOOL = {
|
|
|
14
14
|
inputSchema: TreeInputSchema,
|
|
15
15
|
outputSchema: TreeOutputSchema,
|
|
16
16
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
17
|
-
taskSupport: '
|
|
17
|
+
taskSupport: 'optional',
|
|
18
18
|
gotchas: ['`maxDepth=0` returns only the root node.'],
|
|
19
19
|
};
|
|
20
20
|
async function handleTree(args, signal, onProgress) {
|