@j0hanz/filesystem-mcp 1.2.0 → 1.2.2
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/cli.js +12 -4
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/lib/errors.js +4 -2
- package/dist/lib/resource-store.js +1 -1
- package/dist/schemas.d.ts +19 -0
- package/dist/server.js +0 -18
- package/dist/tools/apply-patch.js +16 -2
- package/dist/tools/calculate-hash.js +47 -12
- package/dist/tools/create-directory.js +12 -2
- package/dist/tools/delete-file.js +2 -1
- package/dist/tools/diff-files.js +15 -1
- package/dist/tools/edit-file.js +7 -6
- package/dist/tools/list-directory.js +2 -1
- package/dist/tools/move-file.js +3 -2
- package/dist/tools/read-multiple.js +20 -2
- package/dist/tools/read.js +15 -1
- package/dist/tools/replace-in-files.js +43 -10
- package/dist/tools/roots.js +2 -1
- package/dist/tools/search-content.js +58 -19
- package/dist/tools/search-files.js +55 -11
- package/dist/tools/shared.d.ts +2 -0
- package/dist/tools/shared.js +36 -7
- package/dist/tools/stat-many.js +21 -2
- package/dist/tools/stat.js +11 -1
- package/dist/tools/task-support.js +3 -3
- package/dist/tools/tree.js +2 -1
- package/dist/tools/write-file.js +12 -2
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -3,11 +3,13 @@ import { getSystemErrorMessage, getSystemErrorName } from 'node:util';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { Command, CommanderError, InvalidArgumentError } from 'commander';
|
|
5
5
|
import packageJsonRaw from '../package.json' with { type: 'json' };
|
|
6
|
+
import { processInParallel } from './lib/fs-helpers.js';
|
|
6
7
|
import { getReservedDeviceNameForPath, isWindowsDriveRelativePath, normalizePath, } from './lib/path-validation.js';
|
|
7
8
|
import { isRecord } from './lib/type-guards.js';
|
|
8
9
|
const PackageJsonSchema = z.object({ version: z.string() });
|
|
9
10
|
const { version: SERVER_VERSION } = PackageJsonSchema.parse(packageJsonRaw);
|
|
10
11
|
const IS_WINDOWS = process.platform === 'win32';
|
|
12
|
+
const CLI_VALIDATE_CONCURRENCY = 8;
|
|
11
13
|
export class CliExitError extends Error {
|
|
12
14
|
exitCode;
|
|
13
15
|
constructor(message, exitCode) {
|
|
@@ -92,11 +94,17 @@ async function validateDirectoryPath(inputPath) {
|
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
async function normalizeCliDirectories(args) {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
const { results, errors } = await processInParallel([...args], validateDirectoryPath, CLI_VALIDATE_CONCURRENCY);
|
|
98
|
+
if (errors.length === 0) {
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
let first = errors[0];
|
|
102
|
+
for (const failure of errors) {
|
|
103
|
+
if (first && failure.index < first.index) {
|
|
104
|
+
first = failure;
|
|
105
|
+
}
|
|
98
106
|
}
|
|
99
|
-
|
|
107
|
+
throw first?.error ?? new Error('Failed to validate directories');
|
|
100
108
|
}
|
|
101
109
|
function parseAllowedDirArgument(value, previous) {
|
|
102
110
|
validateCliPath(value);
|
package/dist/config.d.ts
CHANGED
|
@@ -102,6 +102,7 @@ export declare const ErrorCode: {
|
|
|
102
102
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
103
103
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
104
104
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
105
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
105
106
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
106
107
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
107
108
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,7 @@ export const ErrorCode = {
|
|
|
5
5
|
E_NOT_DIRECTORY: 'E_NOT_DIRECTORY',
|
|
6
6
|
E_TOO_LARGE: 'E_TOO_LARGE',
|
|
7
7
|
E_TIMEOUT: 'E_TIMEOUT',
|
|
8
|
+
E_CANCELLED: 'E_CANCELLED',
|
|
8
9
|
E_INVALID_PATTERN: 'E_INVALID_PATTERN',
|
|
9
10
|
E_INVALID_INPUT: 'E_INVALID_INPUT',
|
|
10
11
|
E_PERMISSION_DENIED: 'E_PERMISSION_DENIED',
|
package/dist/lib/errors.js
CHANGED
|
@@ -146,8 +146,6 @@ function isTimeoutErrorSingle(error) {
|
|
|
146
146
|
return false;
|
|
147
147
|
if (error.name === 'TimeoutError')
|
|
148
148
|
return true;
|
|
149
|
-
if (isAbortErrorSingle(error))
|
|
150
|
-
return true;
|
|
151
149
|
const code = getNodeErrorCodeLabel(error);
|
|
152
150
|
if (code === 'ETIMEDOUT')
|
|
153
151
|
return true;
|
|
@@ -184,6 +182,7 @@ const ERROR_SUGGESTIONS = {
|
|
|
184
182
|
[ErrorCode.E_NOT_DIRECTORY]: 'The path points to a file, not a directory. Use read to read file contents.',
|
|
185
183
|
[ErrorCode.E_TOO_LARGE]: 'The file exceeds the size limit. Use head to read a partial preview, or narrow the scope of what you read.',
|
|
186
184
|
[ErrorCode.E_TIMEOUT]: 'The operation timed out. Try a smaller scope (narrower path), fewer results (maxResults), or search fewer files.',
|
|
185
|
+
[ErrorCode.E_CANCELLED]: 'The operation was cancelled. This is not an error — no retry is needed unless you want to re-run the operation.',
|
|
187
186
|
[ErrorCode.E_INVALID_PATTERN]: 'The glob or regex pattern is invalid. Check syntax and escape special characters.',
|
|
188
187
|
[ErrorCode.E_INVALID_INPUT]: 'One or more input parameters are invalid. Check the tool documentation for correct usage.',
|
|
189
188
|
[ErrorCode.E_PERMISSION_DENIED]: 'Permission denied by the operating system. Check file permissions.',
|
|
@@ -220,6 +219,9 @@ function classifyMessageError(error) {
|
|
|
220
219
|
return undefined;
|
|
221
220
|
}
|
|
222
221
|
function classifyError(error) {
|
|
222
|
+
if (isAbortError(error)) {
|
|
223
|
+
return ErrorCode.E_CANCELLED;
|
|
224
|
+
}
|
|
223
225
|
if (isTimeoutLikeError(error)) {
|
|
224
226
|
return ErrorCode.E_TIMEOUT;
|
|
225
227
|
}
|
|
@@ -85,7 +85,7 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
85
85
|
function getText(uri) {
|
|
86
86
|
const existing = byUri.get(uri);
|
|
87
87
|
if (!existing) {
|
|
88
|
-
throw new McpError(ErrorCode.E_NOT_FOUND, `Resource not found: ${uri}
|
|
88
|
+
throw new McpError(ErrorCode.E_NOT_FOUND, `Resource not found: ${uri}. The cached result may have been evicted. Re-run the originating tool to regenerate.`);
|
|
89
89
|
}
|
|
90
90
|
return existing;
|
|
91
91
|
}
|
package/dist/schemas.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare const ToolErrorResponseSchema: z.ZodObject<{
|
|
|
21
21
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
22
22
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
23
23
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
24
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
24
25
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
25
26
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
26
27
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -112,6 +113,7 @@ export declare const ListAllowedDirectoriesOutputSchema: z.ZodObject<{
|
|
|
112
113
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
113
114
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
114
115
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
116
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
115
117
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
116
118
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
117
119
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -159,6 +161,7 @@ export declare const ListDirectoryOutputSchema: z.ZodObject<{
|
|
|
159
161
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
160
162
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
161
163
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
164
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
162
165
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
163
166
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
164
167
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -182,6 +185,7 @@ export declare const SearchFilesOutputSchema: z.ZodObject<{
|
|
|
182
185
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
183
186
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
184
187
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
188
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
185
189
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
186
190
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
187
191
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -220,6 +224,7 @@ export declare const SearchContentOutputSchema: z.ZodObject<{
|
|
|
220
224
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
221
225
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
222
226
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
227
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
223
228
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
224
229
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
225
230
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -271,6 +276,7 @@ export declare const TreeOutputSchema: z.ZodObject<{
|
|
|
271
276
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
272
277
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
273
278
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
279
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
274
280
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
275
281
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
276
282
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -307,6 +313,7 @@ export declare const ReadFileOutputSchema: z.ZodObject<{
|
|
|
307
313
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
308
314
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
309
315
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
316
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
310
317
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
311
318
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
312
319
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -357,6 +364,7 @@ export declare const ReadMultipleFilesOutputSchema: z.ZodObject<{
|
|
|
357
364
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
358
365
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
359
366
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
367
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
360
368
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
361
369
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
362
370
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -397,6 +405,7 @@ export declare const GetFileInfoOutputSchema: z.ZodObject<{
|
|
|
397
405
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
398
406
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
399
407
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
408
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
400
409
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
401
410
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
402
411
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -446,6 +455,7 @@ export declare const GetMultipleFileInfoOutputSchema: z.ZodObject<{
|
|
|
446
455
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
447
456
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
448
457
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
458
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
449
459
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
450
460
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
451
461
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -471,6 +481,7 @@ export declare const CreateDirectoryOutputSchema: z.ZodObject<{
|
|
|
471
481
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
472
482
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
473
483
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
484
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
474
485
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
475
486
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
476
487
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -498,6 +509,7 @@ export declare const WriteFileOutputSchema: z.ZodObject<{
|
|
|
498
509
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
499
510
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
500
511
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
512
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
501
513
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
502
514
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
503
515
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -531,6 +543,7 @@ export declare const EditFileOutputSchema: z.ZodObject<{
|
|
|
531
543
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
532
544
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
533
545
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
546
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
534
547
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
535
548
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
536
549
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -558,6 +571,7 @@ export declare const MoveFileOutputSchema: z.ZodObject<{
|
|
|
558
571
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
559
572
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
560
573
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
574
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
561
575
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
562
576
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
563
577
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -585,6 +599,7 @@ export declare const DeleteFileOutputSchema: z.ZodObject<{
|
|
|
585
599
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
586
600
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
587
601
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
602
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
588
603
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
589
604
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
590
605
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -613,6 +628,7 @@ export declare const CalculateHashOutputSchema: z.ZodObject<{
|
|
|
613
628
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
614
629
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
615
630
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
631
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
616
632
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
617
633
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
618
634
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -645,6 +661,7 @@ export declare const DiffFilesOutputSchema: z.ZodObject<{
|
|
|
645
661
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
646
662
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
647
663
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
664
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
648
665
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
649
666
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
650
667
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -675,6 +692,7 @@ export declare const ApplyPatchOutputSchema: z.ZodObject<{
|
|
|
675
692
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
676
693
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
677
694
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
695
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
678
696
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
679
697
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
680
698
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
|
@@ -718,6 +736,7 @@ export declare const SearchAndReplaceOutputSchema: z.ZodObject<{
|
|
|
718
736
|
readonly E_NOT_DIRECTORY: "E_NOT_DIRECTORY";
|
|
719
737
|
readonly E_TOO_LARGE: "E_TOO_LARGE";
|
|
720
738
|
readonly E_TIMEOUT: "E_TIMEOUT";
|
|
739
|
+
readonly E_CANCELLED: "E_CANCELLED";
|
|
721
740
|
readonly E_INVALID_PATTERN: "E_INVALID_PATTERN";
|
|
722
741
|
readonly E_INVALID_INPUT: "E_INVALID_INPUT";
|
|
723
742
|
readonly E_PERMISSION_DENIED: "E_PERMISSION_DENIED";
|
package/dist/server.js
CHANGED
|
@@ -300,24 +300,6 @@ export async function createServer(options = {}) {
|
|
|
300
300
|
registerGetHelpPrompt(server, serverInstructions, localIcon);
|
|
301
301
|
registerResultResources(server, resourceStore, localIcon);
|
|
302
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
|
-
}
|
|
321
303
|
registerAllTools(server, {
|
|
322
304
|
resourceStore,
|
|
323
305
|
isInitialized: () => rootsManager.isInitialized(),
|
|
@@ -5,7 +5,7 @@ import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
|
|
|
5
5
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
6
6
|
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
7
7
|
import { validateExistingPath } from '../lib/path-validation.js';
|
|
8
|
-
import { ApplyPatchInputSchema, } from '../schemas.js';
|
|
8
|
+
import { ApplyPatchInputSchema, ApplyPatchOutputSchema } from '../schemas.js';
|
|
9
9
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
10
10
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
11
11
|
const APPLY_PATCH_TOOL = {
|
|
@@ -14,6 +14,7 @@ const APPLY_PATCH_TOOL = {
|
|
|
14
14
|
'Generate the patch with `diff_files`, then validate with `dryRun: true` before writing. ' +
|
|
15
15
|
'On failure, regenerate a fresh patch via `diff_files` against the current file content and retry.',
|
|
16
16
|
inputSchema: ApplyPatchInputSchema,
|
|
17
|
+
outputSchema: ApplyPatchOutputSchema,
|
|
17
18
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
18
19
|
};
|
|
19
20
|
function assertPatchTargetSizeWithinLimit(filePath, size, maxFileSize) {
|
|
@@ -72,7 +73,20 @@ export function registerApplyPatchTool(server, options = {}) {
|
|
|
72
73
|
guard: options.isInitialized,
|
|
73
74
|
progressMessage: (args) => {
|
|
74
75
|
const name = path.basename(args.path);
|
|
75
|
-
return
|
|
76
|
+
return args.dryRun
|
|
77
|
+
? `🛠 apply_patch: ${name} [dry run]`
|
|
78
|
+
: `🛠 apply_patch: ${name}`;
|
|
79
|
+
},
|
|
80
|
+
completionMessage: (args, result) => {
|
|
81
|
+
const name = path.basename(args.path);
|
|
82
|
+
if (result.isError)
|
|
83
|
+
return `🛠 apply_patch: ${name} • failed`;
|
|
84
|
+
const sc = result.structuredContent;
|
|
85
|
+
if (!sc.ok)
|
|
86
|
+
return `🛠 apply_patch: ${name} • failed`;
|
|
87
|
+
if (args.dryRun)
|
|
88
|
+
return `🛠 apply_patch: ${name} • dry run OK`;
|
|
89
|
+
return `🛠 apply_patch: ${name} • applied`;
|
|
76
90
|
},
|
|
77
91
|
});
|
|
78
92
|
if (registerToolTaskIfAvailable(server, 'apply_patch', APPLY_PATCH_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
@@ -8,7 +8,7 @@ import { isIgnoredByGitignore, loadRootGitignore, } from '../lib/file-operations
|
|
|
8
8
|
import { globEntries } from '../lib/file-operations/glob-engine.js';
|
|
9
9
|
import { assertNotAborted, withAbort } from '../lib/fs-helpers.js';
|
|
10
10
|
import { validateExistingPath } from '../lib/path-validation.js';
|
|
11
|
-
import { CalculateHashInputSchema, } from '../schemas.js';
|
|
11
|
+
import { CalculateHashInputSchema, CalculateHashOutputSchema, } from '../schemas.js';
|
|
12
12
|
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
13
13
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
14
14
|
const WINDOWS_PATH_SEPARATOR = /\\/gu;
|
|
@@ -16,6 +16,7 @@ const CALCULATE_HASH_TOOL = {
|
|
|
16
16
|
title: 'Calculate Hash',
|
|
17
17
|
description: 'Calculate SHA-256 hash of a file or directory.',
|
|
18
18
|
inputSchema: CalculateHashInputSchema,
|
|
19
|
+
outputSchema: CalculateHashOutputSchema,
|
|
19
20
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
20
21
|
};
|
|
21
22
|
async function hashFile(filePath, encoding, signal) {
|
|
@@ -152,20 +153,54 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
152
153
|
timedSignal: {},
|
|
153
154
|
context: { path: args.path },
|
|
154
155
|
run: async (signal) => {
|
|
156
|
+
const baseName = path.basename(args.path);
|
|
157
|
+
let progressCursor = 0;
|
|
155
158
|
notifyProgress(extra, {
|
|
156
159
|
current: 0,
|
|
157
|
-
message: `🕮 calculate_hash: ${
|
|
160
|
+
message: `🕮 calculate_hash: ${baseName}`,
|
|
158
161
|
});
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
162
|
+
const baseReporter = createProgressReporter(extra);
|
|
163
|
+
const progressWithMessage = ({ current, total, }) => {
|
|
164
|
+
if (current > progressCursor)
|
|
165
|
+
progressCursor = current;
|
|
166
|
+
const fileWord = current === 1 ? 'file' : 'files';
|
|
167
|
+
baseReporter({
|
|
168
|
+
current,
|
|
169
|
+
...(total !== undefined ? { total } : {}),
|
|
170
|
+
message: `🕮 calculate_hash: ${baseName} — ${current} ${fileWord} hashed`,
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
try {
|
|
174
|
+
const result = await handleCalculateHash(args, signal, progressWithMessage);
|
|
175
|
+
const sc = result.structuredContent;
|
|
176
|
+
const totalFiles = sc.ok ? (sc.fileCount ?? 1) : 1;
|
|
177
|
+
const finalCurrent = Math.max(totalFiles + 1, progressCursor + 1);
|
|
178
|
+
let suffix;
|
|
179
|
+
if (!sc.ok) {
|
|
180
|
+
suffix = 'failed';
|
|
181
|
+
}
|
|
182
|
+
else if (sc.fileCount !== undefined && sc.fileCount > 1) {
|
|
183
|
+
suffix = `${sc.fileCount} files • ${(sc.hash ?? '').slice(0, 8)}...`;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
suffix = `${(sc.hash ?? '').slice(0, 8)}...`;
|
|
187
|
+
}
|
|
188
|
+
notifyProgress(extra, {
|
|
189
|
+
current: finalCurrent,
|
|
190
|
+
total: finalCurrent,
|
|
191
|
+
message: `🕮 calculate_hash: ${baseName} • ${suffix}`,
|
|
192
|
+
});
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
197
|
+
notifyProgress(extra, {
|
|
198
|
+
current: finalCurrent,
|
|
199
|
+
total: finalCurrent,
|
|
200
|
+
message: `🕮 calculate_hash: ${baseName} • failed`,
|
|
201
|
+
});
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
169
204
|
},
|
|
170
205
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
|
|
171
206
|
});
|
|
@@ -3,13 +3,14 @@ import * as path from 'node:path';
|
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
5
5
|
import { validatePathForWrite } from '../lib/path-validation.js';
|
|
6
|
-
import { CreateDirectoryInputSchema, } from '../schemas.js';
|
|
6
|
+
import { CreateDirectoryInputSchema, CreateDirectoryOutputSchema, } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, IDEMPOTENT_WRITE_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
const CREATE_DIRECTORY_TOOL = {
|
|
10
10
|
title: 'Create Directory',
|
|
11
11
|
description: 'Create a new directory at the specified path (recursive)',
|
|
12
12
|
inputSchema: CreateDirectoryInputSchema,
|
|
13
|
+
outputSchema: CreateDirectoryOutputSchema,
|
|
13
14
|
annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
|
|
14
15
|
};
|
|
15
16
|
async function handleCreateDirectory(args, signal) {
|
|
@@ -31,7 +32,16 @@ export function registerCreateDirectoryTool(server, options = {}) {
|
|
|
31
32
|
});
|
|
32
33
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
33
34
|
guard: options.isInitialized,
|
|
34
|
-
progressMessage: (args) =>
|
|
35
|
+
progressMessage: (args) => {
|
|
36
|
+
const name = path.basename(args.path) || args.path;
|
|
37
|
+
return `🛠 mkdir: ${name}`;
|
|
38
|
+
},
|
|
39
|
+
completionMessage: (args, result) => {
|
|
40
|
+
const name = path.basename(args.path) || args.path;
|
|
41
|
+
if (result.isError)
|
|
42
|
+
return `🛠 mkdir: ${name} • failed`;
|
|
43
|
+
return `🛠 mkdir: ${name} • created`;
|
|
44
|
+
},
|
|
35
45
|
});
|
|
36
46
|
if (registerToolTaskIfAvailable(server, 'mkdir', CREATE_DIRECTORY_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
37
47
|
return;
|
|
@@ -3,13 +3,14 @@ import * as path from 'node:path';
|
|
|
3
3
|
import { ErrorCode, isNodeError } from '../lib/errors.js';
|
|
4
4
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
5
5
|
import { validatePathForWrite } from '../lib/path-validation.js';
|
|
6
|
-
import { DeleteFileInputSchema, } from '../schemas.js';
|
|
6
|
+
import { DeleteFileInputSchema, DeleteFileOutputSchema } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
const DELETE_FILE_TOOL = {
|
|
10
10
|
title: 'Delete File',
|
|
11
11
|
description: 'Delete a file or directory.',
|
|
12
12
|
inputSchema: DeleteFileInputSchema,
|
|
13
|
+
outputSchema: DeleteFileOutputSchema,
|
|
13
14
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
14
15
|
};
|
|
15
16
|
async function handleDeleteFile(args, signal) {
|
package/dist/tools/diff-files.js
CHANGED
|
@@ -5,7 +5,7 @@ import { MAX_TEXT_FILE_SIZE } from '../lib/constants.js';
|
|
|
5
5
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
6
6
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
7
7
|
import { validateExistingPath } from '../lib/path-validation.js';
|
|
8
|
-
import { DiffFilesInputSchema, } from '../schemas.js';
|
|
8
|
+
import { DiffFilesInputSchema, DiffFilesOutputSchema } from '../schemas.js';
|
|
9
9
|
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
10
10
|
const DIFF_FILES_TOOL = {
|
|
11
11
|
title: 'Diff Files',
|
|
@@ -13,6 +13,7 @@ const DIFF_FILES_TOOL = {
|
|
|
13
13
|
'Output feeds directly into `apply_patch`. ' +
|
|
14
14
|
'Check `isIdentical` in the response — if true, the files are already in sync and no patch is needed.',
|
|
15
15
|
inputSchema: DiffFilesInputSchema,
|
|
16
|
+
outputSchema: DiffFilesOutputSchema,
|
|
16
17
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
17
18
|
};
|
|
18
19
|
function assertDiffFileSizeWithinLimit(filePath, size, maxFileSize) {
|
|
@@ -86,5 +87,18 @@ export function registerDiffFilesTool(server, options = {}) {
|
|
|
86
87
|
const name2 = path.basename(args.modified);
|
|
87
88
|
return `🕮 diff_files: ${name1} ⟷ ${name2}`;
|
|
88
89
|
},
|
|
90
|
+
completionMessage: (args, result) => {
|
|
91
|
+
const n1 = path.basename(args.original);
|
|
92
|
+
const n2 = path.basename(args.modified);
|
|
93
|
+
if (result.isError)
|
|
94
|
+
return `🕮 diff_files: ${n1} ⟷ ${n2} • failed`;
|
|
95
|
+
const sc = result.structuredContent;
|
|
96
|
+
if (!sc.ok)
|
|
97
|
+
return `🕮 diff_files: ${n1} ⟷ ${n2} • failed`;
|
|
98
|
+
if (sc.isIdentical)
|
|
99
|
+
return `🕮 diff_files: ${n1} ⟷ ${n2} • identical`;
|
|
100
|
+
const hunks = (sc.diff?.match(/@@/g) ?? []).length;
|
|
101
|
+
return `🕮 diff_files: ${n1} ⟷ ${n2} • ${hunks} hunk${hunks !== 1 ? 's' : ''}`;
|
|
102
|
+
},
|
|
89
103
|
}));
|
|
90
104
|
}
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as path from 'node:path';
|
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { atomicWriteFile } from '../lib/fs-helpers.js';
|
|
5
5
|
import { validateExistingPath } from '../lib/path-validation.js';
|
|
6
|
-
import { EditFileInputSchema } from '../schemas.js';
|
|
6
|
+
import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
8
8
|
const EDIT_FILE_TOOL = {
|
|
9
9
|
title: 'Edit File',
|
|
@@ -12,6 +12,7 @@ const EDIT_FILE_TOOL = {
|
|
|
12
12
|
'`oldText` must match exactly — include 3–5 lines of surrounding context to uniquely target the location. ' +
|
|
13
13
|
'Use `dryRun: true` to validate edits before writing.',
|
|
14
14
|
inputSchema: EditFileInputSchema,
|
|
15
|
+
outputSchema: EditFileOutputSchema,
|
|
15
16
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
16
17
|
};
|
|
17
18
|
function applyEdits(content, edits) {
|
|
@@ -82,19 +83,19 @@ export function registerEditFileTool(server, options = {}) {
|
|
|
82
83
|
guard: options.isInitialized,
|
|
83
84
|
progressMessage: (args) => {
|
|
84
85
|
const name = path.basename(args.path);
|
|
85
|
-
return `🛠 edit: ${name}
|
|
86
|
+
return `🛠 edit: ${name} [${args.edits.length} edits]`;
|
|
86
87
|
},
|
|
87
88
|
completionMessage: (args, result) => {
|
|
88
89
|
const name = path.basename(args.path);
|
|
89
90
|
if (result.isError)
|
|
90
|
-
return `🛠 edit: ${name}
|
|
91
|
+
return `🛠 edit: ${name} • Failed`;
|
|
91
92
|
const sc = result.structuredContent;
|
|
92
93
|
if (!sc.ok)
|
|
93
|
-
return `🛠 edit: ${name}
|
|
94
|
+
return `🛠 edit: ${name} • Failed`;
|
|
94
95
|
if (sc.lineRange) {
|
|
95
|
-
return `🛠 edit: ${name}
|
|
96
|
+
return `🛠 edit: ${name} • [${sc.lineRange[0]}-${sc.lineRange[1]}]`;
|
|
96
97
|
}
|
|
97
|
-
return `🛠 edit: ${name}
|
|
98
|
+
return `🛠 edit: ${name} • [${sc.appliedEdits ?? 0} edits]`;
|
|
98
99
|
},
|
|
99
100
|
}));
|
|
100
101
|
}
|
|
@@ -3,7 +3,7 @@ import { formatOperationSummary, joinLines } from '../config.js';
|
|
|
3
3
|
import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import { listDirectory } from '../lib/file-operations/list-directory.js';
|
|
6
|
-
import { ListDirectoryInputSchema, } from '../schemas.js';
|
|
6
|
+
import { ListDirectoryInputSchema, ListDirectoryOutputSchema, } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
8
8
|
const LIST_DIRECTORY_TOOL = {
|
|
9
9
|
title: 'List Directory',
|
|
@@ -13,6 +13,7 @@ const LIST_DIRECTORY_TOOL = {
|
|
|
13
13
|
'Use includeIgnored=true to include ignored directories like node_modules. ' +
|
|
14
14
|
'For recursive searches, use find instead.',
|
|
15
15
|
inputSchema: ListDirectoryInputSchema,
|
|
16
|
+
outputSchema: ListDirectoryOutputSchema,
|
|
16
17
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
17
18
|
};
|
|
18
19
|
function buildListTextResult(result) {
|
package/dist/tools/move-file.js
CHANGED
|
@@ -3,13 +3,14 @@ import * as path from 'node:path';
|
|
|
3
3
|
import { ErrorCode, isNodeError } from '../lib/errors.js';
|
|
4
4
|
import { withAbort } from '../lib/fs-helpers.js';
|
|
5
5
|
import { validateExistingPath, validatePathForWrite, } from '../lib/path-validation.js';
|
|
6
|
-
import { MoveFileInputSchema } from '../schemas.js';
|
|
6
|
+
import { MoveFileInputSchema, MoveFileOutputSchema } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
const MOVE_FILE_TOOL = {
|
|
10
10
|
title: 'Move File',
|
|
11
11
|
description: 'Move or rename a file or directory.',
|
|
12
12
|
inputSchema: MoveFileInputSchema,
|
|
13
|
+
outputSchema: MoveFileOutputSchema,
|
|
13
14
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
14
15
|
};
|
|
15
16
|
async function handleMoveFile(args, signal) {
|
|
@@ -47,7 +48,7 @@ export function registerMoveFileTool(server, options = {}) {
|
|
|
47
48
|
});
|
|
48
49
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
49
50
|
guard: options.isInitialized,
|
|
50
|
-
progressMessage: (args) => `🛠 mv: ${path.basename(args.source)}
|
|
51
|
+
progressMessage: (args) => `🛠 mv: ${path.basename(args.source)} • ${path.basename(args.destination)}`,
|
|
51
52
|
});
|
|
52
53
|
if (registerToolTaskIfAvailable(server, 'mv', MOVE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
53
54
|
return;
|
|
@@ -2,7 +2,7 @@ import * as path from 'node:path';
|
|
|
2
2
|
import { DEFAULT_READ_MANY_MAX_TOTAL_SIZE, DEFAULT_SEARCH_TIMEOUT_MS, } from '../lib/constants.js';
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { readMultipleFiles } from '../lib/file-operations/read-multiple-files.js';
|
|
5
|
-
import { ReadMultipleFilesInputSchema, } from '../schemas.js';
|
|
5
|
+
import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
|
|
6
6
|
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
const READ_MULTIPLE_FILES_TOOL = {
|
|
@@ -11,6 +11,7 @@ const READ_MULTIPLE_FILES_TOOL = {
|
|
|
11
11
|
'Returns contents and metadata for each file. ' +
|
|
12
12
|
'For single file, use read for simpler output.',
|
|
13
13
|
inputSchema: ReadMultipleFilesInputSchema,
|
|
14
|
+
outputSchema: ReadMultipleFilesOutputSchema,
|
|
14
15
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
15
16
|
};
|
|
16
17
|
async function handleReadMultipleFiles(args, signal, resourceStore) {
|
|
@@ -122,7 +123,24 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
122
123
|
};
|
|
123
124
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
124
125
|
guard: options.isInitialized,
|
|
125
|
-
progressMessage: (args) =>
|
|
126
|
+
progressMessage: (args) => {
|
|
127
|
+
const first = path.basename(args.paths[0] ?? '');
|
|
128
|
+
const extra = args.paths.length > 1 ? `, ${path.basename(args.paths[1] ?? '')}…` : '';
|
|
129
|
+
return `🕮 read_many: ${args.paths.length} files [${first}${extra}]`;
|
|
130
|
+
},
|
|
131
|
+
completionMessage: (_args, result) => {
|
|
132
|
+
if (result.isError)
|
|
133
|
+
return `🕮 read_many • failed`;
|
|
134
|
+
const sc = result.structuredContent;
|
|
135
|
+
if (!sc.ok)
|
|
136
|
+
return `🕮 read_many • failed`;
|
|
137
|
+
const total = sc.summary?.total ?? 0;
|
|
138
|
+
const succeeded = sc.summary?.succeeded ?? 0;
|
|
139
|
+
const failed = sc.summary?.failed ?? 0;
|
|
140
|
+
if (failed)
|
|
141
|
+
return `🕮 read_many: ${succeeded}/${total} read, ${failed} failed`;
|
|
142
|
+
return `🕮 read_many: ${total} files read`;
|
|
143
|
+
},
|
|
126
144
|
});
|
|
127
145
|
if (registerToolTaskIfAvailable(server, 'read_many', READ_MULTIPLE_FILES_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
128
146
|
return;
|
package/dist/tools/read.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as path from 'node:path';
|
|
|
2
2
|
import { DEFAULT_SEARCH_TIMEOUT_MS, MAX_TEXT_FILE_SIZE, } from '../lib/constants.js';
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { readFile } from '../lib/fs-helpers.js';
|
|
5
|
-
import { ReadFileInputSchema } from '../schemas.js';
|
|
5
|
+
import { ReadFileInputSchema, ReadFileOutputSchema } from '../schemas.js';
|
|
6
6
|
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
const READ_FILE_TOOL = {
|
|
@@ -11,6 +11,7 @@ const READ_FILE_TOOL = {
|
|
|
11
11
|
'Use head parameter to preview the first N lines of large files. ' +
|
|
12
12
|
'For multiple files, use read_many for efficiency.',
|
|
13
13
|
inputSchema: ReadFileInputSchema,
|
|
14
|
+
outputSchema: ReadFileOutputSchema,
|
|
14
15
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
15
16
|
};
|
|
16
17
|
async function handleReadFile(args, signal, resourceStore) {
|
|
@@ -89,6 +90,19 @@ export function registerReadFileTool(server, options = {}) {
|
|
|
89
90
|
}
|
|
90
91
|
return `🕮 read: ${name}`;
|
|
91
92
|
},
|
|
93
|
+
completionMessage: (args, result) => {
|
|
94
|
+
const name = path.basename(args.path);
|
|
95
|
+
if (result.isError)
|
|
96
|
+
return `🕮 read: ${name} • failed`;
|
|
97
|
+
const sc = result.structuredContent;
|
|
98
|
+
if (!sc.ok)
|
|
99
|
+
return `🕮 read: ${name} • failed`;
|
|
100
|
+
if (sc.hasMoreLines)
|
|
101
|
+
return `🕮 read: ${name} • truncated [${sc.totalLines ?? '?'} lines]`;
|
|
102
|
+
if (sc.startLine !== undefined)
|
|
103
|
+
return `🕮 read: ${name} • lines ${sc.startLine}–${sc.endLine ?? '?'}`;
|
|
104
|
+
return `🕮 read: ${name} • ${sc.totalLines ?? '?'} lines`;
|
|
105
|
+
},
|
|
92
106
|
});
|
|
93
107
|
if (registerToolTaskIfAvailable(server, 'read', READ_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
94
108
|
return;
|
|
@@ -7,7 +7,7 @@ import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.j
|
|
|
7
7
|
import { globEntries } from '../lib/file-operations/glob-engine.js';
|
|
8
8
|
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
9
9
|
import { validateExistingPath, validatePathForWrite, } from '../lib/path-validation.js';
|
|
10
|
-
import { SearchAndReplaceInputSchema, } from '../schemas.js';
|
|
10
|
+
import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
|
|
11
11
|
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, notifyProgress, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
12
12
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
13
13
|
const SEARCH_AND_REPLACE_TOOL = {
|
|
@@ -18,6 +18,7 @@ const SEARCH_AND_REPLACE_TOOL = {
|
|
|
18
18
|
'Always run with `dryRun: true` first to verify matches before writing. ' +
|
|
19
19
|
'Literal mode (default) matches exact text; `isRegex: true` enables RE2 regex with capture groups ($1, $2).',
|
|
20
20
|
inputSchema: SearchAndReplaceInputSchema,
|
|
21
|
+
outputSchema: SearchAndReplaceOutputSchema,
|
|
21
22
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
22
23
|
};
|
|
23
24
|
const MAX_FAILURES = 20;
|
|
@@ -230,18 +231,50 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
230
231
|
timedSignal: {},
|
|
231
232
|
...(args.path ? { context: { path: args.path } } : {}),
|
|
232
233
|
run: async (signal) => {
|
|
234
|
+
const dryLabel = args.dryRun ? ' [dry run]' : '';
|
|
235
|
+
const context = `"${args.searchPattern}" in ${args.filePattern}${dryLabel}`;
|
|
236
|
+
let progressCursor = 0;
|
|
233
237
|
notifyProgress(extra, {
|
|
234
238
|
current: 0,
|
|
235
|
-
message: `🛠 search_and_replace: ${
|
|
239
|
+
message: `🛠 search_and_replace: ${context}`,
|
|
236
240
|
});
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
241
|
+
const baseReporter = createProgressReporter(extra);
|
|
242
|
+
const progressWithMessage = ({ current, total, }) => {
|
|
243
|
+
if (current > progressCursor)
|
|
244
|
+
progressCursor = current;
|
|
245
|
+
baseReporter({
|
|
246
|
+
current,
|
|
247
|
+
...(total !== undefined ? { total } : {}),
|
|
248
|
+
message: `🛠 search_and_replace: "${args.searchPattern}" — ${current} files processed`,
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
try {
|
|
252
|
+
const result = await handleSearchAndReplace(args, signal, progressWithMessage);
|
|
253
|
+
const sc = result.structuredContent;
|
|
254
|
+
const finalCurrent = Math.max((sc.processedFiles ?? 0) + 1, progressCursor + 1);
|
|
255
|
+
const matchWord = (sc.matches ?? 0) === 1 ? 'match' : 'matches';
|
|
256
|
+
const fileWord = (sc.filesChanged ?? 0) === 1 ? 'file' : 'files';
|
|
257
|
+
let endSuffix = `${sc.matches ?? 0} ${matchWord} in ${sc.filesChanged ?? 0} ${fileWord}`;
|
|
258
|
+
if (sc.failedFiles)
|
|
259
|
+
endSuffix += `, ${sc.failedFiles} failed`;
|
|
260
|
+
if (sc.dryRun)
|
|
261
|
+
endSuffix += ' [dry run]';
|
|
262
|
+
notifyProgress(extra, {
|
|
263
|
+
current: finalCurrent,
|
|
264
|
+
total: finalCurrent,
|
|
265
|
+
message: `🛠 search_and_replace: ${context} • ${endSuffix}`,
|
|
266
|
+
});
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
271
|
+
notifyProgress(extra, {
|
|
272
|
+
current: finalCurrent,
|
|
273
|
+
total: finalCurrent,
|
|
274
|
+
message: `🛠 search_and_replace: ${context} • failed`,
|
|
275
|
+
});
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
245
278
|
},
|
|
246
279
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
|
|
247
280
|
});
|
package/dist/tools/roots.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { joinLines } from '../config.js';
|
|
2
2
|
import { ErrorCode } from '../lib/errors.js';
|
|
3
3
|
import { getAllowedDirectories } from '../lib/path-validation.js';
|
|
4
|
-
import { ListAllowedDirectoriesInputSchema, } from '../schemas.js';
|
|
4
|
+
import { ListAllowedDirectoriesInputSchema, ListAllowedDirectoriesOutputSchema, } from '../schemas.js';
|
|
5
5
|
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
6
6
|
const LIST_ALLOWED_DIRECTORIES_TOOL = {
|
|
7
7
|
title: 'Workspace Roots',
|
|
@@ -9,6 +9,7 @@ const LIST_ALLOWED_DIRECTORIES_TOOL = {
|
|
|
9
9
|
'Call this first to see available directories. ' +
|
|
10
10
|
'All other tools only work within these directories.',
|
|
11
11
|
inputSchema: ListAllowedDirectoriesInputSchema,
|
|
12
|
+
outputSchema: ListAllowedDirectoriesOutputSchema,
|
|
12
13
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
13
14
|
};
|
|
14
15
|
function buildTextRoots(dirs) {
|
|
@@ -4,7 +4,7 @@ import { formatOperationSummary, joinLines } from '../config.js';
|
|
|
4
4
|
import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
|
|
5
5
|
import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
6
6
|
import { searchContent } from '../lib/file-operations/search-content.js';
|
|
7
|
-
import { SearchContentInputSchema, } from '../schemas.js';
|
|
7
|
+
import { SearchContentInputSchema, SearchContentOutputSchema, } from '../schemas.js';
|
|
8
8
|
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
9
9
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
10
10
|
const MAX_INLINE_MATCHES = 50;
|
|
@@ -16,6 +16,7 @@ const SEARCH_CONTENT_TOOL = {
|
|
|
16
16
|
'Use `filePattern` to scope by file type (e.g. `**/*.ts`) and avoid noisy results. ' +
|
|
17
17
|
'Use includeHidden=true to include hidden files and directories.',
|
|
18
18
|
inputSchema: SearchContentInputSchema,
|
|
19
|
+
outputSchema: SearchContentOutputSchema,
|
|
19
20
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
20
21
|
};
|
|
21
22
|
function assertValidRegexPattern(pattern) {
|
|
@@ -201,29 +202,67 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
201
202
|
context: { path: args.path ?? '.' },
|
|
202
203
|
run: async (signal) => {
|
|
203
204
|
const normalizedArgs = SearchContentInputSchema.parse(args);
|
|
205
|
+
const scope = normalizedArgs.filePattern;
|
|
206
|
+
const { pattern } = normalizedArgs;
|
|
207
|
+
let progressCursor = 0;
|
|
204
208
|
notifyProgress(extra, {
|
|
205
209
|
current: 0,
|
|
206
|
-
message: `🔎︎ grep: ${
|
|
210
|
+
message: `🔎︎ grep: ${pattern} in ${scope}`,
|
|
207
211
|
});
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
const baseReporter = createProgressReporter(extra);
|
|
213
|
+
const progressWithMessage = ({ current, total, }) => {
|
|
214
|
+
if (current > progressCursor)
|
|
215
|
+
progressCursor = current;
|
|
216
|
+
const fileWord = current === 1 ? 'file' : 'files';
|
|
217
|
+
baseReporter({
|
|
218
|
+
current,
|
|
219
|
+
...(total !== undefined ? { total } : {}),
|
|
220
|
+
message: `🔎︎ grep: ${pattern} • ${current} ${fileWord} scanned`,
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
try {
|
|
224
|
+
const result = await handleSearchContent(normalizedArgs, signal, options.resourceStore, progressWithMessage);
|
|
225
|
+
const sc = result.structuredContent;
|
|
226
|
+
const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
|
|
227
|
+
const filesMatched = sc.ok ? (sc.filesMatched ?? 0) : 0;
|
|
228
|
+
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
229
|
+
let suffix;
|
|
230
|
+
if (count === 0) {
|
|
231
|
+
suffix = `No matches in ${scope}`;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const matchWord = count === 1 ? 'match' : 'matches';
|
|
235
|
+
const fileInfo = filesMatched > 0
|
|
236
|
+
? ` in ${filesMatched} ${filesMatched === 1 ? 'file' : 'files'}`
|
|
237
|
+
: '';
|
|
238
|
+
suffix = `${count} ${matchWord}${fileInfo}`;
|
|
239
|
+
if (stoppedReason === 'timeout') {
|
|
240
|
+
suffix += ' [stopped — timeout]';
|
|
241
|
+
}
|
|
242
|
+
else if (stoppedReason === 'maxResults') {
|
|
243
|
+
suffix += ' [truncated — max results]';
|
|
244
|
+
}
|
|
245
|
+
else if (stoppedReason === 'maxFiles') {
|
|
246
|
+
suffix += ' [truncated — max files]';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const finalCurrent = Math.max((sc.filesScanned ?? 0) + 1, progressCursor + 1);
|
|
250
|
+
notifyProgress(extra, {
|
|
251
|
+
current: finalCurrent,
|
|
252
|
+
total: finalCurrent,
|
|
253
|
+
message: `🔎︎ grep: ${pattern} • ${suffix}`,
|
|
254
|
+
});
|
|
255
|
+
return result;
|
|
214
256
|
}
|
|
215
|
-
|
|
216
|
-
|
|
257
|
+
catch (error) {
|
|
258
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
259
|
+
notifyProgress(extra, {
|
|
260
|
+
current: finalCurrent,
|
|
261
|
+
total: finalCurrent,
|
|
262
|
+
message: `🔎︎ grep: ${pattern} in ${scope} • failed`,
|
|
263
|
+
});
|
|
264
|
+
throw error;
|
|
217
265
|
}
|
|
218
|
-
else {
|
|
219
|
-
suffix = `${count} matches`;
|
|
220
|
-
}
|
|
221
|
-
const finalCurrent = (sc.filesScanned ?? 0) + 1;
|
|
222
|
-
notifyProgress(extra, {
|
|
223
|
-
current: finalCurrent,
|
|
224
|
-
message: `🔎︎ grep: ${normalizedArgs.pattern} ➟ ${suffix}`,
|
|
225
|
-
});
|
|
226
|
-
return result;
|
|
227
266
|
},
|
|
228
267
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path ?? '.'),
|
|
229
268
|
});
|
|
@@ -3,7 +3,7 @@ import { formatOperationSummary, joinLines } from '../config.js';
|
|
|
3
3
|
import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_TIMEOUT_MS, } from '../lib/constants.js';
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import { searchFiles } from '../lib/file-operations/search-files.js';
|
|
6
|
-
import { SearchFilesInputSchema, } from '../schemas.js';
|
|
6
|
+
import { SearchFilesInputSchema, SearchFilesOutputSchema } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
const SEARCH_FILES_TOOL = {
|
|
@@ -13,6 +13,7 @@ const SEARCH_FILES_TOOL = {
|
|
|
13
13
|
'For text search inside files, use grep. ' +
|
|
14
14
|
'To bulk-edit the matched files, pass the same glob pattern to search_and_replace.',
|
|
15
15
|
inputSchema: SearchFilesInputSchema,
|
|
16
|
+
outputSchema: SearchFilesOutputSchema,
|
|
16
17
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
17
18
|
};
|
|
18
19
|
async function handleSearchFiles(args, signal, onProgress) {
|
|
@@ -88,19 +89,62 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
88
89
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
89
90
|
context: { path: args.path ?? '.' },
|
|
90
91
|
run: async (signal) => {
|
|
92
|
+
const scope = args.path ?? '.';
|
|
93
|
+
const { pattern } = args;
|
|
94
|
+
let progressCursor = 0;
|
|
91
95
|
notifyProgress(extra, {
|
|
92
96
|
current: 0,
|
|
93
|
-
message: `🔎︎ find: ${
|
|
97
|
+
message: `🔎︎ find: ${pattern} in ${scope}`,
|
|
94
98
|
});
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
const baseReporter = createProgressReporter(extra);
|
|
100
|
+
const progressWithMessage = ({ current, total, }) => {
|
|
101
|
+
if (current > progressCursor)
|
|
102
|
+
progressCursor = current;
|
|
103
|
+
const fileWord = current === 1 ? 'file' : 'files';
|
|
104
|
+
baseReporter({
|
|
105
|
+
current,
|
|
106
|
+
...(total !== undefined ? { total } : {}),
|
|
107
|
+
message: `🔎︎ find: ${pattern} — ${current} ${fileWord} scanned`,
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
const result = await handleSearchFiles(args, signal, progressWithMessage);
|
|
112
|
+
const sc = result.structuredContent;
|
|
113
|
+
const count = sc.ok ? (sc.totalMatches ?? 0) : 0;
|
|
114
|
+
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
115
|
+
let suffix;
|
|
116
|
+
if (count === 0) {
|
|
117
|
+
suffix = `No matches in ${scope}`;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
suffix = `${count} ${count === 1 ? 'match' : 'matches'}`;
|
|
121
|
+
if (stoppedReason === 'timeout') {
|
|
122
|
+
suffix += ' [stopped — timeout]';
|
|
123
|
+
}
|
|
124
|
+
else if (stoppedReason === 'maxResults') {
|
|
125
|
+
suffix += ' [truncated — max results]';
|
|
126
|
+
}
|
|
127
|
+
else if (stoppedReason === 'maxFiles') {
|
|
128
|
+
suffix += ' [truncated — max files]';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const finalCurrent = Math.max((sc.filesScanned ?? 0) + 1, progressCursor + 1);
|
|
132
|
+
notifyProgress(extra, {
|
|
133
|
+
current: finalCurrent,
|
|
134
|
+
total: finalCurrent,
|
|
135
|
+
message: `🔎︎ find: ${pattern} • ${suffix}`,
|
|
136
|
+
});
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
141
|
+
notifyProgress(extra, {
|
|
142
|
+
current: finalCurrent,
|
|
143
|
+
total: finalCurrent,
|
|
144
|
+
message: `🔎︎ find: ${pattern} in ${scope} • failed`,
|
|
145
|
+
});
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
104
148
|
},
|
|
105
149
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_INVALID_PATTERN, args.path),
|
|
106
150
|
});
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export declare const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS: {
|
|
|
19
19
|
readonly idempotentHint: true;
|
|
20
20
|
readonly openWorldHint: false;
|
|
21
21
|
};
|
|
22
|
+
export declare function shouldStripStructuredOutput(): boolean;
|
|
23
|
+
export declare function maybeStripStructuredContentFromResult<T extends object>(result: T): T;
|
|
22
24
|
type ResourceEntry = ReturnType<ResourceStore['putText']>;
|
|
23
25
|
export declare function maybeExternalizeTextContent(resourceStore: ResourceStore | undefined, content: string, params: {
|
|
24
26
|
name: string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getAllowedDirectories } from '../lib/path-validation.js';
|
|
|
5
5
|
const MAX_INLINE_CONTENT_CHARS = 20_000;
|
|
6
6
|
const MAX_INLINE_PREVIEW_CHARS = 4_000;
|
|
7
7
|
const PROGRESS_RATE_LIMIT_MS = 50;
|
|
8
|
+
const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes']);
|
|
8
9
|
export const READ_ONLY_TOOL_ANNOTATIONS = {
|
|
9
10
|
readOnlyHint: true,
|
|
10
11
|
idempotentHint: true,
|
|
@@ -20,6 +21,30 @@ export const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS = {
|
|
|
20
21
|
idempotentHint: true,
|
|
21
22
|
openWorldHint: false,
|
|
22
23
|
};
|
|
24
|
+
export function shouldStripStructuredOutput() {
|
|
25
|
+
const value = process.env['FS_CONTEXT_STRIP_STRUCTURED'];
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
return false;
|
|
28
|
+
return TRUE_ENV_VALUES.has(value.trim().toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
export function maybeStripStructuredContentFromResult(result) {
|
|
31
|
+
if (!shouldStripStructuredOutput())
|
|
32
|
+
return result;
|
|
33
|
+
if (!Object.hasOwn(result, 'structuredContent'))
|
|
34
|
+
return result;
|
|
35
|
+
const rest = { ...result };
|
|
36
|
+
delete rest['structuredContent'];
|
|
37
|
+
return rest;
|
|
38
|
+
}
|
|
39
|
+
function maybeStripOutputSchema(tool) {
|
|
40
|
+
if (!shouldStripStructuredOutput())
|
|
41
|
+
return tool;
|
|
42
|
+
if (!Object.hasOwn(tool, 'outputSchema'))
|
|
43
|
+
return tool;
|
|
44
|
+
const mutable = { ...tool };
|
|
45
|
+
delete mutable['outputSchema'];
|
|
46
|
+
return mutable;
|
|
47
|
+
}
|
|
23
48
|
function buildTextPreview(text) {
|
|
24
49
|
if (text.length <= MAX_INLINE_PREVIEW_CHARS)
|
|
25
50
|
return text;
|
|
@@ -71,13 +96,14 @@ function canSendProgress(extra) {
|
|
|
71
96
|
extra.sendNotification !== undefined);
|
|
72
97
|
}
|
|
73
98
|
export function withDefaultIcons(tool, iconInfo) {
|
|
74
|
-
if (!iconInfo)
|
|
75
|
-
return tool;
|
|
99
|
+
if (!iconInfo) {
|
|
100
|
+
return maybeStripOutputSchema(tool);
|
|
101
|
+
}
|
|
76
102
|
const existingIcons = tool.icons;
|
|
77
103
|
if (existingIcons && existingIcons.length > 0) {
|
|
78
|
-
return tool;
|
|
104
|
+
return maybeStripOutputSchema(tool);
|
|
79
105
|
}
|
|
80
|
-
|
|
106
|
+
const withIcons = {
|
|
81
107
|
...tool,
|
|
82
108
|
icons: [
|
|
83
109
|
{
|
|
@@ -86,6 +112,7 @@ export function withDefaultIcons(tool, iconInfo) {
|
|
|
86
112
|
},
|
|
87
113
|
],
|
|
88
114
|
};
|
|
115
|
+
return maybeStripOutputSchema(withIcons);
|
|
89
116
|
}
|
|
90
117
|
export function buildFileInfoPayload(info) {
|
|
91
118
|
return {
|
|
@@ -247,7 +274,7 @@ export function wrapToolHandler(handler, options) {
|
|
|
247
274
|
return async (args, extra) => {
|
|
248
275
|
const resolvedExtra = extra ?? {};
|
|
249
276
|
if (options.guard && !options.guard()) {
|
|
250
|
-
return buildNotInitializedResult();
|
|
277
|
+
return maybeStripStructuredContentFromResult(buildNotInitializedResult());
|
|
251
278
|
}
|
|
252
279
|
if (options.progressMessage) {
|
|
253
280
|
const message = options.progressMessage(args);
|
|
@@ -255,9 +282,11 @@ export function wrapToolHandler(handler, options) {
|
|
|
255
282
|
const completionFn = completionMessage
|
|
256
283
|
? (result) => completionMessage(args, result)
|
|
257
284
|
: undefined;
|
|
258
|
-
|
|
285
|
+
const result = await withProgress(message, resolvedExtra, () => handler(args, resolvedExtra), completionFn);
|
|
286
|
+
return maybeStripStructuredContentFromResult(result);
|
|
259
287
|
}
|
|
260
|
-
|
|
288
|
+
const result = await handler(args, resolvedExtra);
|
|
289
|
+
return maybeStripStructuredContentFromResult(result);
|
|
261
290
|
};
|
|
262
291
|
}
|
|
263
292
|
export function resolvePathOrRoot(pathValue) {
|
package/dist/tools/stat-many.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
1
2
|
import { formatBytes, joinLines } from '../config.js';
|
|
2
3
|
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
3
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
5
|
import { getMultipleFileInfo } from '../lib/file-operations/file-info.js';
|
|
5
|
-
import { GetMultipleFileInfoInputSchema, } from '../schemas.js';
|
|
6
|
+
import { GetMultipleFileInfoInputSchema, GetMultipleFileInfoOutputSchema, } from '../schemas.js';
|
|
6
7
|
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
9
|
const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
9
10
|
title: 'Get Multiple File Info',
|
|
10
11
|
description: 'Get metadata for multiple files or directories in one request.',
|
|
11
12
|
inputSchema: GetMultipleFileInfoInputSchema,
|
|
13
|
+
outputSchema: GetMultipleFileInfoOutputSchema,
|
|
12
14
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
13
15
|
};
|
|
14
16
|
function formatFileInfoDetail(info) {
|
|
@@ -73,7 +75,24 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
73
75
|
};
|
|
74
76
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
75
77
|
guard: options.isInitialized,
|
|
76
|
-
progressMessage: (args) =>
|
|
78
|
+
progressMessage: (args) => {
|
|
79
|
+
const first = path.basename(args.paths[0] ?? '');
|
|
80
|
+
const extra = args.paths.length > 1 ? `, ${path.basename(args.paths[1] ?? '')}…` : '';
|
|
81
|
+
return `🕮 stat_many: ${args.paths.length} paths [${first}${extra}]`;
|
|
82
|
+
},
|
|
83
|
+
completionMessage: (_args, result) => {
|
|
84
|
+
if (result.isError)
|
|
85
|
+
return `🕮 stat_many • failed`;
|
|
86
|
+
const sc = result.structuredContent;
|
|
87
|
+
if (!sc.ok)
|
|
88
|
+
return `🕮 stat_many • failed`;
|
|
89
|
+
const total = sc.summary?.total ?? 0;
|
|
90
|
+
const succeeded = sc.summary?.succeeded ?? 0;
|
|
91
|
+
const failed = sc.summary?.failed ?? 0;
|
|
92
|
+
if (failed)
|
|
93
|
+
return `🕮 stat_many: ${succeeded}/${total} OK, ${failed} failed`;
|
|
94
|
+
return `🕮 stat_many: ${total} OK`;
|
|
95
|
+
},
|
|
77
96
|
});
|
|
78
97
|
if (registerToolTaskIfAvailable(server, 'stat_many', GET_MULTIPLE_FILE_INFO_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
79
98
|
return;
|
package/dist/tools/stat.js
CHANGED
|
@@ -3,12 +3,13 @@ import { formatBytes, joinLines } from '../config.js';
|
|
|
3
3
|
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import { getFileInfo } from '../lib/file-operations/file-info.js';
|
|
6
|
-
import { GetFileInfoInputSchema, } from '../schemas.js';
|
|
6
|
+
import { GetFileInfoInputSchema, GetFileInfoOutputSchema } from '../schemas.js';
|
|
7
7
|
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
8
8
|
const GET_FILE_INFO_TOOL = {
|
|
9
9
|
title: 'Get File Info',
|
|
10
10
|
description: 'Get metadata (size, modified time, permissions, mime type) for a file or directory.',
|
|
11
11
|
inputSchema: GetFileInfoInputSchema,
|
|
12
|
+
outputSchema: GetFileInfoOutputSchema,
|
|
12
13
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
13
14
|
};
|
|
14
15
|
function formatFileInfoDetails(info) {
|
|
@@ -47,5 +48,14 @@ export function registerGetFileInfoTool(server, options = {}) {
|
|
|
47
48
|
server.registerTool('stat', withDefaultIcons({ ...GET_FILE_INFO_TOOL }, options.iconInfo), wrapToolHandler(handler, {
|
|
48
49
|
guard: options.isInitialized,
|
|
49
50
|
progressMessage: (args) => `🕮 stat: ${path.basename(args.path)}`,
|
|
51
|
+
completionMessage: (args, result) => {
|
|
52
|
+
const name = path.basename(args.path);
|
|
53
|
+
if (result.isError)
|
|
54
|
+
return `🕮 stat: ${name} • failed`;
|
|
55
|
+
const sc = result.structuredContent;
|
|
56
|
+
if (!sc.ok || !sc.info)
|
|
57
|
+
return `🕮 stat: ${name} • failed`;
|
|
58
|
+
return `🕮 stat: ${sc.info.name} [${sc.info.type}, ${formatBytes(sc.info.size)}]`;
|
|
59
|
+
},
|
|
50
60
|
}));
|
|
51
61
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
3
3
|
import { isRecord } from '../lib/type-guards.js';
|
|
4
|
-
import { buildToolErrorResponse, withDefaultIcons } from './shared.js';
|
|
4
|
+
import { buildToolErrorResponse, maybeStripStructuredContentFromResult, withDefaultIcons, } from './shared.js';
|
|
5
5
|
function isExperimentalTaskRegistration(value) {
|
|
6
6
|
if (!value || typeof value !== 'object')
|
|
7
7
|
return false;
|
|
@@ -191,13 +191,13 @@ async function tryStoreTaskResult(taskStore, taskId, status, result) {
|
|
|
191
191
|
}
|
|
192
192
|
async function runTaskInBackground(run, args, extra, taskStore, taskId) {
|
|
193
193
|
try {
|
|
194
|
-
const result = await run(args, extra);
|
|
194
|
+
const result = maybeStripStructuredContentFromResult(await run(args, extra));
|
|
195
195
|
const status = isErrorResult(result) ? 'failed' : 'completed';
|
|
196
196
|
await tryStoreTaskResult(taskStore, taskId, status, result);
|
|
197
197
|
await notifyTaskStatusIfPossible(extra, taskStore, taskId);
|
|
198
198
|
}
|
|
199
199
|
catch (error) {
|
|
200
|
-
const fallback = buildToolErrorResponse(error, ErrorCode.E_UNKNOWN);
|
|
200
|
+
const fallback = maybeStripStructuredContentFromResult(buildToolErrorResponse(error, ErrorCode.E_UNKNOWN));
|
|
201
201
|
try {
|
|
202
202
|
await tryStoreTaskResult(taskStore, taskId, 'failed', fallback);
|
|
203
203
|
await notifyTaskStatusIfPossible(extra, taskStore, taskId);
|
package/dist/tools/tree.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as path from 'node:path';
|
|
|
2
2
|
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { formatTreeAscii, treeDirectory } from '../lib/file-operations/tree.js';
|
|
5
|
-
import { TreeInputSchema } from '../schemas.js';
|
|
5
|
+
import { TreeInputSchema, TreeOutputSchema } from '../schemas.js';
|
|
6
6
|
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
const TREE_TOOL = {
|
|
@@ -11,6 +11,7 @@ const TREE_TOOL = {
|
|
|
11
11
|
'Returns an ASCII tree for quick scanning and a structured JSON tree for programmatic use. ' +
|
|
12
12
|
'Note: maxDepth=0 returns only the root node with empty children array.',
|
|
13
13
|
inputSchema: TreeInputSchema,
|
|
14
|
+
outputSchema: TreeOutputSchema,
|
|
14
15
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
15
16
|
};
|
|
16
17
|
async function handleTree(args, signal) {
|
package/dist/tools/write-file.js
CHANGED
|
@@ -3,13 +3,14 @@ import * as path from 'node:path';
|
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
5
5
|
import { validatePathForWrite } from '../lib/path-validation.js';
|
|
6
|
-
import { WriteFileInputSchema, } from '../schemas.js';
|
|
6
|
+
import { WriteFileInputSchema, WriteFileOutputSchema } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
const WRITE_FILE_TOOL = {
|
|
10
10
|
title: 'Write File',
|
|
11
11
|
description: 'Write content to a file. Creates the file if it does not exist.',
|
|
12
12
|
inputSchema: WriteFileInputSchema,
|
|
13
|
+
outputSchema: WriteFileOutputSchema,
|
|
13
14
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
14
15
|
};
|
|
15
16
|
async function handleWriteFile(args, signal) {
|
|
@@ -35,7 +36,16 @@ export function registerWriteFileTool(server, options = {}) {
|
|
|
35
36
|
});
|
|
36
37
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
37
38
|
guard: options.isInitialized,
|
|
38
|
-
progressMessage: (args) => `🛠 write: ${path.basename(args.path)}`,
|
|
39
|
+
progressMessage: (args) => `🛠 write: ${path.basename(args.path)} [${args.content.length} chars]`,
|
|
40
|
+
completionMessage: (args, result) => {
|
|
41
|
+
const name = path.basename(args.path);
|
|
42
|
+
if (result.isError)
|
|
43
|
+
return `🛠 write: ${name} • failed`;
|
|
44
|
+
const sc = result.structuredContent;
|
|
45
|
+
if (!sc.ok)
|
|
46
|
+
return `🛠 write: ${name} • failed`;
|
|
47
|
+
return `🛠 write: ${name} • ${sc.bytesWritten ?? 0} bytes`;
|
|
48
|
+
},
|
|
39
49
|
});
|
|
40
50
|
if (registerToolTaskIfAvailable(server, 'write', WRITE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
41
51
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@j0hanz/filesystem-mcp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"mcpName": "io.github.j0hanz/filesystem-mcp",
|
|
5
5
|
"description": "MCP Server that enables LLMs to interact with the local filesystem.",
|
|
6
6
|
"type": "module",
|
|
@@ -31,12 +31,14 @@
|
|
|
31
31
|
"start": "node dist/index.js",
|
|
32
32
|
"format": "prettier --write .",
|
|
33
33
|
"type-check": "node scripts/tasks.mjs type-check",
|
|
34
|
+
"type-check:src": "node node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
|
|
35
|
+
"type-check:tests": "node node_modules/typescript/bin/tsc -p tsconfig.test.json --noEmit",
|
|
34
36
|
"type-check:diagnostics": "tsc --noEmit --extendedDiagnostics",
|
|
35
37
|
"type-check:trace": "node -e \"require('fs').rmSync('.ts-trace',{recursive:true,force:true})\" && tsc --noEmit --generateTrace .ts-trace",
|
|
36
38
|
"lint": "eslint .",
|
|
37
39
|
"lint:fix": "eslint . --fix",
|
|
38
40
|
"test": "node scripts/tasks.mjs test",
|
|
39
|
-
"test:fast": "node --test --import tsx/esm src/__tests__/**/*.test.ts",
|
|
41
|
+
"test:fast": "node --test --import tsx/esm src/__tests__/**/*.test.ts node-tests/**/*.test.ts",
|
|
40
42
|
"test:coverage": "node scripts/tasks.mjs test --coverage",
|
|
41
43
|
"knip": "knip",
|
|
42
44
|
"knip:fix": "knip --fix",
|