@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 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 validations = [];
96
- for (const arg of args) {
97
- validations.push(validateDirectoryPath(arg));
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
- return Promise.all(validations);
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',
@@ -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 `🛠 apply_patch: ${name}`;
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: ${path.basename(args.path)}`,
160
+ message: `🕮 calculate_hash: ${baseName}`,
158
161
  });
159
- const result = await handleCalculateHash(args, signal, createProgressReporter(extra));
160
- const sc = result.structuredContent;
161
- const totalFiles = sc.ok ? (sc.fileCount ?? 1) : 1;
162
- const finalCurrent = totalFiles + 1;
163
- const suffix = sc.ok ? `${(sc.hash ?? '').slice(0, 8)}...` : 'failed';
164
- notifyProgress(extra, {
165
- current: finalCurrent,
166
- message: `🕮 calculate_hash: ${path.basename(args.path)} ${suffix}`,
167
- });
168
- return result;
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) => `🛠 mkdir: ${path.basename(args.path)}`,
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) {
@@ -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
  }
@@ -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} (${args.edits.length} edits)`;
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} Failed`;
91
+ return `🛠 edit: ${name} Failed`;
91
92
  const sc = result.structuredContent;
92
93
  if (!sc.ok)
93
- return `🛠 edit: ${name} Failed`;
94
+ return `🛠 edit: ${name} Failed`;
94
95
  if (sc.lineRange) {
95
- return `🛠 edit: ${name} [${sc.lineRange[0]}-${sc.lineRange[1]}]`;
96
+ return `🛠 edit: ${name} [${sc.lineRange[0]}-${sc.lineRange[1]}]`;
96
97
  }
97
- return `🛠 edit: ${name} (${sc.appliedEdits ?? 0} edits)`;
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) {
@@ -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)} ${path.basename(args.destination)}`,
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) => `🕮 read_many: ${args.paths.length} files`,
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;
@@ -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: ${args.filePattern}`,
239
+ message: `🛠 search_and_replace: ${context}`,
236
240
  });
237
- const result = await handleSearchAndReplace(args, signal, createProgressReporter(extra));
238
- const sc = result.structuredContent;
239
- const finalCurrent = (sc.processedFiles ?? 0) + 1;
240
- notifyProgress(extra, {
241
- current: finalCurrent,
242
- message: `🛠 search_and_replace: ${args.filePattern} ➟ ${String(sc.filesChanged ?? 0)} files`,
243
- });
244
- return result;
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
  });
@@ -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: ${normalizedArgs.pattern}`,
210
+ message: `🔎︎ grep: ${pattern} in ${scope}`,
207
211
  });
208
- const result = await handleSearchContent(normalizedArgs, signal, options.resourceStore, createProgressReporter(extra));
209
- const sc = result.structuredContent;
210
- const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
211
- let suffix;
212
- if (count === 0) {
213
- suffix = 'No matches';
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
- else if (count === 1) {
216
- suffix = '1 match';
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: ${args.pattern}`,
97
+ message: `🔎︎ find: ${pattern} in ${scope}`,
94
98
  });
95
- const result = await handleSearchFiles(args, signal, createProgressReporter(extra));
96
- const sc = result.structuredContent;
97
- const suffix = sc.ok && sc.totalMatches ? String(sc.totalMatches) : 'No matches';
98
- const finalCurrent = (sc.filesScanned ?? 0) + 1;
99
- notifyProgress(extra, {
100
- current: finalCurrent,
101
- message: `🔎︎ find: ${args.pattern} ➟ ${suffix}`,
102
- });
103
- return result;
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
  });
@@ -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;
@@ -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
- return {
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
- return withProgress(message, resolvedExtra, () => handler(args, resolvedExtra), completionFn);
285
+ const result = await withProgress(message, resolvedExtra, () => handler(args, resolvedExtra), completionFn);
286
+ return maybeStripStructuredContentFromResult(result);
259
287
  }
260
- return handler(args, resolvedExtra);
288
+ const result = await handler(args, resolvedExtra);
289
+ return maybeStripStructuredContentFromResult(result);
261
290
  };
262
291
  }
263
292
  export function resolvePathOrRoot(pathValue) {
@@ -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) => `🕮 stat_many: ${args.paths.length} paths`,
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;
@@ -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);
@@ -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) {
@@ -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.0",
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",