@j0hanz/filesystem-mcp 1.13.2 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +117 -100
  2. package/dist/config.d.ts +0 -1
  3. package/dist/lib/errors.js +5 -2
  4. package/dist/lib/file-operations/metadata.js +9 -3
  5. package/dist/lib/file-operations/search.d.ts +0 -1
  6. package/dist/lib/file-operations/search.js +5 -12
  7. package/dist/lib/fs-helpers.js +10 -11
  8. package/dist/lib/globs.d.ts +2 -0
  9. package/dist/lib/globs.js +19 -0
  10. package/dist/lib/zod-codecs.d.ts +2 -0
  11. package/dist/lib/zod-codecs.js +18 -0
  12. package/dist/pkg-info.d.ts +1 -0
  13. package/dist/pkg-info.js +2 -2
  14. package/dist/prompts.js +3 -3
  15. package/dist/resources/generated-instructions.js +3 -12
  16. package/dist/resources/tool-catalog.js +10 -41
  17. package/dist/resources/tool-info.d.ts +0 -1
  18. package/dist/resources/tool-info.js +11 -39
  19. package/dist/resources/workflows.js +8 -1
  20. package/dist/schemas.d.ts +179 -459
  21. package/dist/schemas.js +156 -165
  22. package/dist/server/roots-manager.js +1 -1
  23. package/dist/tools/apply-patch.js +19 -8
  24. package/dist/tools/calculate-hash.js +3 -5
  25. package/dist/tools/create-directory.js +1 -1
  26. package/dist/tools/delete-file.js +2 -4
  27. package/dist/tools/diff-files.js +1 -3
  28. package/dist/tools/edit-file.js +5 -2
  29. package/dist/tools/list-directory.js +10 -15
  30. package/dist/tools/move-file.js +14 -26
  31. package/dist/tools/read-multiple.js +12 -7
  32. package/dist/tools/read.js +1 -2
  33. package/dist/tools/replace-in-files.js +58 -94
  34. package/dist/tools/roots.js +2 -6
  35. package/dist/tools/search-content.js +150 -186
  36. package/dist/tools/search-files.js +5 -9
  37. package/dist/tools/shared.d.ts +7 -0
  38. package/dist/tools/shared.js +38 -11
  39. package/dist/tools/stat-many.js +6 -4
  40. package/dist/tools/stat.js +2 -2
  41. package/dist/tools/tree.js +1 -1
  42. package/dist/tools/write-file.js +1 -5
  43. package/package.json +2 -1
package/dist/schemas.js CHANGED
@@ -1,26 +1,20 @@
1
1
  import { z } from 'zod';
2
2
  import { DEFAULT_LIST_MAX_ENTRIES, DEFAULT_SEARCH_CONTENT_RESULTS, DEFAULT_SEARCH_RESULTS, DEFAULT_TREE_DEPTH, DEFAULT_TREE_ENTRIES, MAX_LIST_ENTRIES, MAX_SEARCH_DEPTH, MAX_SEARCH_RESULTS, MAX_TREE_DEPTH, MAX_TREE_ENTRIES, } from './lib/constants.js';
3
3
  import { ErrorCode } from './lib/errors.js';
4
- function isSafeGlobPattern(value) {
5
- if (value.length === 0)
6
- return false;
7
- if (value.includes('**/**/**'))
8
- return false;
9
- const absolutePattern = /^([/\\]|[A-Za-z]:[/\\]|\\\\)/u;
10
- if (absolutePattern.test(value)) {
11
- return false;
12
- }
13
- if (/[\\/]\.\.(?:[/\\]|$)/u.test(value) || value.startsWith('..')) {
14
- return false;
15
- }
16
- return true;
17
- }
4
+ import { isSafeGlobPattern } from './lib/globs.js';
18
5
  const MAX_PATH_LENGTH = 4096;
19
6
  const DESC_PATH_ROOT = 'Base directory (default: root). Absolute path required if multiple roots.';
20
7
  const DESC_PATH_REQUIRED = 'Absolute path to file or directory.';
21
8
  function defaultFalseBoolean(description) {
22
9
  return z.boolean().optional().default(false).describe(description);
23
10
  }
11
+ const SuccessFlagSchema = z.literal(true);
12
+ const NonNegativeIntegerSchema = z.int().min(0, 'Min: 0');
13
+ const PositiveIntegerSchema = z.int().min(1, 'Min: 1');
14
+ const IsoDateTimeSchema = z.iso.datetime();
15
+ const Sha256HexSchema = z
16
+ .string()
17
+ .regex(/^[a-f0-9]{64}$/u, 'Expected SHA-256 hex digest');
24
18
  const PathSchemaBase = z
25
19
  .string()
26
20
  .max(MAX_PATH_LENGTH, `Path too long (max ${MAX_PATH_LENGTH} chars)`);
@@ -61,11 +55,12 @@ const TailLinesSchema = z
61
55
  .optional()
62
56
  .describe('Read last N lines');
63
57
  const LineNumberSchema = z.int({ error: 'Must be integer' }).min(1, 'Min: 1');
64
- function addReadRangeIssue(ctx, path, message) {
58
+ function addReadRangeIssue(ctx, input, path, message) {
65
59
  ctx.addIssue({
66
60
  code: 'custom',
67
61
  path: [path],
68
62
  message,
63
+ input,
69
64
  });
70
65
  }
71
66
  const validateReadRange = (value, ctx) => {
@@ -74,18 +69,14 @@ const validateReadRange = (value, ctx) => {
74
69
  const hasStart = value.startLine !== undefined;
75
70
  const hasEnd = value.endLine !== undefined;
76
71
  if (hasHead && (hasStart || hasEnd)) {
77
- addReadRangeIssue(ctx, 'head', "Cannot use 'head' with 'startLine'/'endLine'");
72
+ addReadRangeIssue(ctx, value, 'head', "Cannot use 'head' with 'startLine'/'endLine'");
78
73
  }
79
74
  if (hasTail && (hasHead || hasStart || hasEnd)) {
80
- addReadRangeIssue(ctx, 'tail', "Cannot use 'tail' with 'head'/'startLine'/'endLine'");
81
- }
82
- if (hasEnd && !hasStart) {
83
- addReadRangeIssue(ctx, 'endLine', "'endLine' requires 'startLine'");
75
+ addReadRangeIssue(ctx, value, 'tail', "Cannot use 'tail' with 'head'/'startLine'/'endLine'");
84
76
  }
85
- if (value.startLine !== undefined &&
86
- value.endLine !== undefined &&
87
- value.endLine < value.startLine) {
88
- addReadRangeIssue(ctx, 'endLine', "'endLine' must be >= 'startLine'");
77
+ const effectiveStart = value.startLine ?? 1;
78
+ if (value.endLine !== undefined && value.endLine < effectiveStart) {
79
+ addReadRangeIssue(ctx, value, 'endLine', "'endLine' must be >= 'startLine'");
89
80
  }
90
81
  };
91
82
  function createReadRangeInputFields(descriptions) {
@@ -100,20 +91,20 @@ const FileInfoSchema = z.strictObject({
100
91
  name: z.string().describe('Name'),
101
92
  path: z.string().describe('Absolute path'),
102
93
  type: FileTypeSchema.describe('Type'),
103
- size: z.number().describe('Size (bytes)'),
104
- tokenEstimate: z.number().optional().describe('Est. tokens (size/4)'),
105
- created: z.string().describe('Created'),
106
- modified: z.string().describe('Modified'),
107
- accessed: z.string().describe('Accessed'),
94
+ size: NonNegativeIntegerSchema.describe('Size (bytes)'),
95
+ tokenEstimate: NonNegativeIntegerSchema.optional().describe('Est. tokens (size/4)'),
96
+ created: IsoDateTimeSchema.describe('Created'),
97
+ modified: IsoDateTimeSchema.describe('Modified'),
98
+ accessed: IsoDateTimeSchema.describe('Accessed'),
108
99
  permissions: z.string().describe('Permissions'),
109
100
  isHidden: z.boolean().describe('Hidden?'),
110
101
  mimeType: z.string().optional().describe('MIME type'),
111
102
  symlinkTarget: z.string().optional().describe('Target (symlink)'),
112
103
  });
113
104
  const OperationSummarySchema = z.strictObject({
114
- total: z.number().describe('Total'),
115
- succeeded: z.number().describe('Succeeded'),
116
- failed: z.number().describe('Failed'),
105
+ total: NonNegativeIntegerSchema.describe('Total'),
106
+ succeeded: NonNegativeIntegerSchema.describe('Succeeded'),
107
+ failed: NonNegativeIntegerSchema.describe('Failed'),
117
108
  });
118
109
  export const ListDirectoryInputSchema = z.strictObject({
119
110
  path: OptionalPathSchema.describe(DESC_PATH_ROOT),
@@ -139,6 +130,9 @@ export const ListDirectoryInputSchema = z.strictObject({
139
130
  .string()
140
131
  .min(1, 'Pattern required')
141
132
  .max(1000, 'Max 1000 chars')
133
+ .refine((val) => isSafeGlobPattern(val), {
134
+ error: 'Invalid glob or unsafe path (absolute/.. forbidden)',
135
+ })
142
136
  .optional()
143
137
  .describe('Optional glob pattern filter (e.g. "**/*.ts")'),
144
138
  includeSymlinkTargets: defaultFalseBoolean('Resolve and include symlink targets in results'),
@@ -236,7 +230,6 @@ export const SearchContentInputSchema = z.strictObject({
236
230
  .describe('Glob for candidate files (e.g. "**/*.ts")'),
237
231
  includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
238
232
  includeIgnored: defaultFalseBoolean('Include ignored items (node_modules, etc).'),
239
- multiline: defaultFalseBoolean('Multi-line mode. ^ and $ match line boundaries when isRegex=true.'),
240
233
  });
241
234
  export const ReadFileInputSchema = z
242
235
  .strictObject({
@@ -244,13 +237,13 @@ export const ReadFileInputSchema = z
244
237
  ...createReadRangeInputFields({
245
238
  head: 'Read first N lines (preview)',
246
239
  tail: 'Read last N lines',
247
- startLine: 'Start line (1-based, inclusive)',
248
- endLine: 'End line (1-based, inclusive). Requires startLine.',
240
+ startLine: 'Start line (1-based, inclusive). Defaults to 1 when endLine is set.',
241
+ endLine: 'End line (1-based, inclusive). Defaults to last line when startLine is set.',
249
242
  }),
250
243
  includeHash: defaultFalseBoolean('Include SHA-256 hash of full file content'),
251
244
  })
252
245
  .superRefine(validateReadRange)
253
- .describe("Use one read mode only: 'head', 'tail', or 'startLine'/'endLine'. 'endLine' requires 'startLine'.");
246
+ .describe("Use one read mode only: 'head', 'tail', or 'startLine'/'endLine'.");
254
247
  export const ReadMultipleFilesInputSchema = z
255
248
  .strictObject({
256
249
  paths: z
@@ -261,12 +254,12 @@ export const ReadMultipleFilesInputSchema = z
261
254
  ...createReadRangeInputFields({
262
255
  head: 'Read first N lines of each file',
263
256
  tail: 'Read last N lines of each file',
264
- startLine: 'Start line (1-based, inclusive) per file',
265
- endLine: 'End line (1-based, inclusive) per file. Requires startLine.',
257
+ startLine: 'Start line (1-based, inclusive) per file. Defaults to 1 when endLine is set.',
258
+ endLine: 'End line (1-based, inclusive) per file. Defaults to last line when startLine is set.',
266
259
  }),
267
260
  })
268
261
  .superRefine(validateReadRange)
269
- .describe("Use one read mode only: 'head', 'tail', or 'startLine'/'endLine'. 'endLine' requires 'startLine'.");
262
+ .describe("Use one read mode only: 'head', 'tail', or 'startLine'/'endLine'.");
270
263
  export const GetFileInfoInputSchema = z.strictObject({
271
264
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
272
265
  });
@@ -278,62 +271,49 @@ export const GetMultipleFileInfoInputSchema = z.strictObject({
278
271
  .describe('File/directory paths. e.g. ["src", "lib"]'),
279
272
  });
280
273
  export const ListAllowedDirectoriesOutputSchema = z.strictObject({
281
- ok: z.boolean(),
274
+ ok: SuccessFlagSchema,
282
275
  directories: z.array(z.string()).optional().describe('Allowed directories'),
283
- rootsCount: z.number().optional().describe('Number of roots'),
284
- hasMultipleRoots: z
285
- .boolean()
286
- .optional()
287
- .describe('Multiple roots configured'),
288
- error: ErrorSchema.optional(),
289
276
  });
290
277
  export const ListDirectoryOutputSchema = z.strictObject({
291
- ok: z.boolean(),
278
+ ok: SuccessFlagSchema,
292
279
  path: z.string().optional(),
293
280
  entries: z
294
281
  .array(z.strictObject({
295
282
  name: z.string().describe('Entry name'),
296
283
  relativePath: z.string().optional(),
297
284
  type: FileTypeSchema,
298
- size: z.number().optional(),
299
- modified: z.string().optional(),
285
+ size: NonNegativeIntegerSchema.optional(),
286
+ modified: IsoDateTimeSchema.optional(),
300
287
  }))
301
288
  .optional(),
302
- totalEntries: z.number().optional(),
289
+ totalEntries: NonNegativeIntegerSchema.optional(),
303
290
  truncated: z.boolean().optional(),
304
- entriesScanned: z.number().optional(),
305
- entriesVisible: z.number().optional(),
306
- totalFiles: z.number().optional(),
307
- totalDirectories: z.number().optional(),
308
- maxDepthReached: z.number().optional(),
291
+ totalFiles: NonNegativeIntegerSchema.optional(),
292
+ totalDirectories: NonNegativeIntegerSchema.optional(),
309
293
  stoppedReason: ListDirectoryStopReasonSchema.optional(),
310
- skippedInaccessible: z.number().optional(),
311
- symlinksNotFollowed: z.number().optional(),
294
+ skippedInaccessible: NonNegativeIntegerSchema.optional(),
312
295
  nextCursor: z
313
296
  .string()
314
297
  .optional()
315
298
  .describe('Cursor for the next page; absent on the final page'),
316
- error: ErrorSchema.optional(),
317
299
  });
318
300
  const SearchSummarySchema = z.strictObject({
319
- totalMatches: z.number().optional().describe('Total matches found'),
301
+ totalMatches: NonNegativeIntegerSchema.optional().describe('Total matches found'),
320
302
  truncated: z.boolean().optional().describe('Results truncated?'),
321
303
  resourceUri: z.string().optional().describe('Full results URI'),
322
- error: ErrorSchema.optional(),
323
304
  });
324
305
  export const SearchFilesOutputSchema = SearchSummarySchema.extend({
325
- ok: z.boolean(),
306
+ ok: SuccessFlagSchema,
326
307
  root: z.string().optional().describe('Search root'),
327
- pattern: z.string().optional().describe('Glob pattern used'),
328
308
  results: z
329
309
  .array(z.strictObject({
330
310
  path: z.string().describe('Relative path'),
331
- size: z.number().optional(),
332
- modified: z.string().optional(),
311
+ size: NonNegativeIntegerSchema.optional(),
312
+ modified: IsoDateTimeSchema.optional(),
333
313
  }))
334
314
  .optional(),
335
- filesScanned: z.number().optional().describe('Files scanned'),
336
- skippedInaccessible: z.number().optional().describe('Inaccessible files'),
315
+ filesScanned: NonNegativeIntegerSchema.optional().describe('Files scanned'),
316
+ skippedInaccessible: NonNegativeIntegerSchema.optional().describe('Inaccessible files'),
337
317
  stoppedReason: SearchStopReasonSchema.optional().describe('Why search stopped'),
338
318
  nextCursor: z
339
319
  .string()
@@ -341,70 +321,49 @@ export const SearchFilesOutputSchema = SearchSummarySchema.extend({
341
321
  .describe('Cursor for the next page; absent on the final page'),
342
322
  });
343
323
  export const SearchContentOutputSchema = SearchSummarySchema.extend({
344
- ok: z.boolean(),
345
- patternType: z
346
- .enum(['literal', 'regex'])
347
- .optional()
348
- .describe('Pattern interpretation'),
349
- caseSensitive: z.boolean().optional().describe('Case-sensitive matching'),
324
+ ok: SuccessFlagSchema,
350
325
  matches: z
351
326
  .array(z.strictObject({
352
327
  file: z.string().describe('Relative path'),
353
- line: z.number(),
354
- column: z
355
- .number()
356
- .optional()
357
- .describe('Column of first match (0-based)'),
328
+ line: PositiveIntegerSchema,
329
+ column: NonNegativeIntegerSchema.optional().describe('Column of first match (0-based)'),
358
330
  content: z.string(),
359
- matchCount: z.number(),
331
+ matchCount: PositiveIntegerSchema,
360
332
  contextBefore: z.array(z.string()).optional(),
361
333
  contextAfter: z.array(z.string()).optional(),
362
334
  }))
363
335
  .optional(),
364
- filesScanned: z.number().optional().describe('Files scanned'),
365
- filesMatched: z.number().optional().describe('Files with matches'),
366
- skippedTooLarge: z.number().optional().describe('Files skipped: too large'),
367
- skippedBinary: z.number().optional().describe('Files skipped: binary'),
368
- skippedInaccessible: z
369
- .number()
370
- .optional()
371
- .describe('Files skipped: inaccessible'),
372
- linesSkippedDueToRegexTimeout: z
373
- .number()
374
- .optional()
375
- .describe('Lines skipped due to regex timeout'),
336
+ filesScanned: NonNegativeIntegerSchema.optional().describe('Files scanned'),
337
+ filesMatched: NonNegativeIntegerSchema.optional().describe('Files with matches'),
338
+ skippedTooLarge: NonNegativeIntegerSchema.optional().describe('Files skipped: too large'),
339
+ skippedBinary: NonNegativeIntegerSchema.optional().describe('Files skipped: binary'),
340
+ skippedInaccessible: NonNegativeIntegerSchema.optional().describe('Files skipped: inaccessible'),
376
341
  stoppedReason: SearchStopReasonSchema.optional().describe('Why search stopped'),
377
342
  });
378
343
  export const TreeOutputSchema = z.strictObject({
379
- ok: z.boolean(),
344
+ ok: SuccessFlagSchema,
380
345
  root: z.string().optional(),
381
346
  tree: TreeEntrySchema.optional(),
382
347
  ascii: z.string().optional(),
383
348
  truncated: z.boolean().optional(),
384
- totalEntries: z.number().optional(),
385
- error: ErrorSchema.optional(),
349
+ totalEntries: NonNegativeIntegerSchema.optional(),
386
350
  });
387
351
  const ReadResultSchema = z.strictObject({
388
352
  content: z.string().optional().describe('Content'),
389
353
  truncated: z.boolean().optional().describe('Truncated?'),
390
354
  resourceUri: z.string().optional().describe('Full content URI'),
391
- totalLines: z.number().optional().describe('Total lines'),
392
- readMode: z
393
- .enum(['full', 'head', 'tail', 'range'])
394
- .optional()
395
- .describe('Mode'),
396
- head: z.number().optional().describe('Head lines'),
397
- tail: z.number().optional().describe('Tail lines'),
398
- startLine: z.number().optional().describe('Start line'),
399
- endLine: z.number().optional().describe('End line'),
400
- linesRead: z.number().optional().describe('Lines read'),
355
+ totalLines: NonNegativeIntegerSchema.optional().describe('Total lines'),
356
+ head: PositiveIntegerSchema.optional().describe('Head lines'),
357
+ tail: PositiveIntegerSchema.optional().describe('Tail lines'),
358
+ startLine: PositiveIntegerSchema.optional().describe('Start line'),
359
+ endLine: PositiveIntegerSchema.optional().describe('End line'),
360
+ linesRead: NonNegativeIntegerSchema.optional().describe('Lines read'),
401
361
  hasMoreLines: z.boolean().optional().describe('More lines?'),
402
362
  });
403
363
  export const ReadFileOutputSchema = ReadResultSchema.extend({
404
- ok: z.boolean(),
364
+ ok: SuccessFlagSchema,
405
365
  path: z.string().optional(),
406
- contentHash: z.string().optional().describe('SHA-256 of full file content'),
407
- error: ErrorSchema.optional(),
366
+ contentHash: Sha256HexSchema.optional().describe('SHA-256 of full file content'),
408
367
  });
409
368
  const ReadMultipleFileResultSchema = ReadResultSchema.extend({
410
369
  path: z.string().describe('File path'),
@@ -412,60 +371,77 @@ const ReadMultipleFileResultSchema = ReadResultSchema.extend({
412
371
  .enum(['head', 'tail', 'range', 'externalized'])
413
372
  .optional()
414
373
  .describe('Why content was truncated'),
415
- maxTotalSize: z.number().optional().describe('Max total size budget'),
416
- error: z.string().optional().describe('Error message'),
374
+ error: ErrorSchema.optional().describe('Structured error details'),
417
375
  });
418
376
  export const ReadMultipleFilesOutputSchema = z.strictObject({
419
- ok: z.boolean(),
377
+ ok: SuccessFlagSchema,
420
378
  results: z.array(ReadMultipleFileResultSchema).optional(),
421
379
  summary: OperationSummarySchema.optional(),
422
- error: ErrorSchema.optional(),
423
380
  });
424
381
  export const GetFileInfoOutputSchema = z.strictObject({
425
- ok: z.boolean(),
382
+ ok: SuccessFlagSchema,
426
383
  info: FileInfoSchema.optional(),
427
- error: ErrorSchema.optional(),
428
384
  });
429
385
  export const GetMultipleFileInfoOutputSchema = z.strictObject({
430
- ok: z.boolean(),
386
+ ok: SuccessFlagSchema,
431
387
  results: z
432
388
  .array(z.strictObject({
433
389
  path: z.string(),
434
390
  info: FileInfoSchema.optional(),
435
- error: z.string().optional(),
391
+ error: ErrorSchema.optional(),
436
392
  }))
437
393
  .optional(),
438
394
  summary: OperationSummarySchema.optional(),
439
- error: ErrorSchema.optional(),
440
395
  });
441
396
  export const CreateDirectoryInputSchema = z
442
397
  .strictObject({
443
398
  path: RequiredPathSchema.optional().describe(DESC_PATH_REQUIRED),
444
399
  paths: z
445
400
  .array(RequiredPathSchema)
401
+ .min(1, 'Min 1 path required')
446
402
  .optional()
447
403
  .describe('Absolute paths to directories to create'),
448
404
  })
449
- .refine((data) => data.path !== undefined || data.paths !== undefined, {
450
- error: "Either 'path' or 'paths' must be provided",
451
- path: ['path'],
405
+ .superRefine((data, ctx) => {
406
+ const hasPath = data.path !== undefined;
407
+ const hasPaths = data.paths !== undefined;
408
+ if (!hasPath && !hasPaths) {
409
+ ctx.addIssue({
410
+ code: 'custom',
411
+ path: ['path'],
412
+ message: "Either 'path' or 'paths' must be provided",
413
+ input: data,
414
+ });
415
+ }
416
+ if (hasPath && hasPaths) {
417
+ ctx.addIssue({
418
+ code: 'custom',
419
+ path: ['path'],
420
+ message: "Provide either 'path' or 'paths', not both",
421
+ input: data,
422
+ });
423
+ ctx.addIssue({
424
+ code: 'custom',
425
+ path: ['paths'],
426
+ message: "Provide either 'path' or 'paths', not both",
427
+ input: data,
428
+ });
429
+ }
452
430
  })
453
431
  .describe("Provide either 'path' or 'paths'.");
454
432
  export const CreateDirectoryOutputSchema = z.strictObject({
455
- ok: z.boolean(),
433
+ ok: SuccessFlagSchema,
456
434
  path: z.string().optional(),
457
435
  paths: z.array(z.string()).optional(),
458
- error: ErrorSchema.optional(),
459
436
  });
460
437
  export const WriteFileInputSchema = z.strictObject({
461
438
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
462
439
  content: z.string().describe('Content to write'),
463
440
  });
464
441
  export const WriteFileOutputSchema = z.strictObject({
465
- ok: z.boolean(),
442
+ ok: SuccessFlagSchema,
466
443
  path: z.string().optional(),
467
- bytesWritten: z.number().optional(),
468
- error: ErrorSchema.optional(),
444
+ bytesWritten: NonNegativeIntegerSchema.optional(),
469
445
  });
470
446
  export const EditFileInputSchema = z.strictObject({
471
447
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
@@ -486,11 +462,11 @@ export const EditFileInputSchema = z.strictObject({
486
462
  export const EditFileOutputSchema = z.strictObject({
487
463
  ok: z.boolean(),
488
464
  path: z.string().optional(),
489
- appliedEdits: z.number().optional(),
490
- linesAdded: z.number().optional().describe('Lines added'),
491
- linesRemoved: z.number().optional().describe('Lines removed'),
465
+ appliedEdits: NonNegativeIntegerSchema.optional(),
466
+ linesAdded: NonNegativeIntegerSchema.optional().describe('Lines added'),
467
+ linesRemoved: NonNegativeIntegerSchema.optional().describe('Lines removed'),
492
468
  lineRange: z
493
- .tuple([z.number(), z.number()])
469
+ .tuple([PositiveIntegerSchema, PositiveIntegerSchema])
494
470
  .optional()
495
471
  .describe('Line range modified [start, end] (1-based)'),
496
472
  unmatchedEdits: z
@@ -498,17 +474,42 @@ export const EditFileOutputSchema = z.strictObject({
498
474
  .optional()
499
475
  .describe('Edits that could not be applied'),
500
476
  diff: z.string().optional().describe('Unified diff of changes (dryRun)'),
501
- error: ErrorSchema.optional(),
502
477
  });
503
478
  export const MoveFileInputSchema = z
504
479
  .strictObject({
505
480
  source: RequiredPathSchema.optional().describe('Path to move (deprecated: use sources)'),
506
- sources: z.array(RequiredPathSchema).optional().describe('Paths to move'),
481
+ sources: z
482
+ .array(RequiredPathSchema)
483
+ .min(1, 'Min 1 source required')
484
+ .optional()
485
+ .describe('Paths to move'),
507
486
  destination: RequiredPathSchema.describe('New path'),
508
487
  })
509
- .refine((data) => (data.source ?? data.sources) !== undefined, {
510
- error: "Either 'source' or 'sources' must be provided",
511
- path: ['source'],
488
+ .superRefine((data, ctx) => {
489
+ const hasSource = data.source !== undefined;
490
+ const hasSources = data.sources !== undefined;
491
+ if (!hasSource && !hasSources) {
492
+ ctx.addIssue({
493
+ code: 'custom',
494
+ path: ['source'],
495
+ message: "Either 'source' or 'sources' must be provided",
496
+ input: data,
497
+ });
498
+ }
499
+ if (hasSource && hasSources) {
500
+ ctx.addIssue({
501
+ code: 'custom',
502
+ path: ['source'],
503
+ message: "Provide either 'source' or 'sources', not both",
504
+ input: data,
505
+ });
506
+ ctx.addIssue({
507
+ code: 'custom',
508
+ path: ['sources'],
509
+ message: "Provide either 'source' or 'sources', not both",
510
+ input: data,
511
+ });
512
+ }
512
513
  })
513
514
  .describe("Provide either 'source' or 'sources'.");
514
515
  export const MoveFileOutputSchema = z.strictObject({
@@ -519,11 +520,10 @@ export const MoveFileOutputSchema = z.strictObject({
519
520
  failed: z
520
521
  .array(z.strictObject({
521
522
  source: z.string().describe('Source path'),
522
- error: z.string().describe('Error message'),
523
+ error: ErrorSchema.describe('Structured error details'),
523
524
  }))
524
525
  .optional()
525
526
  .describe('List of files that failed to move'),
526
- error: ErrorSchema.optional(),
527
527
  });
528
528
  export const DeleteFileInputSchema = z.strictObject({
529
529
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
@@ -531,23 +531,18 @@ export const DeleteFileInputSchema = z.strictObject({
531
531
  ignoreIfNotExists: defaultFalseBoolean('No error if missing'),
532
532
  });
533
533
  export const DeleteFileOutputSchema = z.strictObject({
534
- ok: z.boolean(),
534
+ ok: SuccessFlagSchema,
535
535
  path: z.string().optional(),
536
- error: ErrorSchema.optional(),
537
536
  });
538
537
  export const CalculateHashInputSchema = z.strictObject({
539
538
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
540
539
  });
541
540
  export const CalculateHashOutputSchema = z.strictObject({
542
- ok: z.boolean(),
541
+ ok: SuccessFlagSchema,
543
542
  path: z.string().optional(),
544
- hash: z.string().optional().describe('SHA-256 hash'),
543
+ hash: Sha256HexSchema.optional().describe('SHA-256 hash'),
545
544
  isDirectory: z.boolean().optional().describe('True if path is a directory'),
546
- fileCount: z
547
- .number()
548
- .optional()
549
- .describe('Number of files hashed (directories only)'),
550
- error: ErrorSchema.optional(),
545
+ fileCount: NonNegativeIntegerSchema.optional().describe('Number of files hashed (directories only)'),
551
546
  });
552
547
  export const DiffFilesInputSchema = z.strictObject({
553
548
  original: RequiredPathSchema.describe('Path to original file'),
@@ -570,15 +565,14 @@ export const DiffFilesInputSchema = z.strictObject({
570
565
  .describe('Strip trailing carriage returns before diffing'),
571
566
  });
572
567
  export const DiffFilesOutputSchema = z.strictObject({
573
- ok: z.boolean(),
568
+ ok: SuccessFlagSchema,
574
569
  diff: z.string().optional().describe('Unified diff content'),
575
570
  isIdentical: z.boolean().optional().describe('True if files are identical'),
576
- linesAdded: z.number().optional().describe('Lines added'),
577
- linesRemoved: z.number().optional().describe('Lines removed'),
578
- hunksCount: z.number().optional().describe('Number of diff hunks'),
571
+ linesAdded: NonNegativeIntegerSchema.optional().describe('Lines added'),
572
+ linesRemoved: NonNegativeIntegerSchema.optional().describe('Lines removed'),
573
+ hunksCount: NonNegativeIntegerSchema.optional().describe('Number of diff hunks'),
579
574
  truncated: z.boolean().optional().describe('Diff content truncated?'),
580
575
  resourceUri: z.string().optional().describe('Full diff content URI'),
581
- error: ErrorSchema.optional(),
582
576
  });
583
577
  export const ApplyPatchInputSchema = z.strictObject({
584
578
  path: RequiredPathSchema.describe('Path to file to patch'),
@@ -610,21 +604,20 @@ export const ApplyPatchOutputSchema = z.strictObject({
610
604
  ok: z.boolean(),
611
605
  path: z.string().optional(),
612
606
  applied: z.boolean().optional(),
613
- hunksApplied: z.number().optional().describe('Hunks applied'),
614
- linesAdded: z.number().optional().describe('Lines added'),
615
- linesRemoved: z.number().optional().describe('Lines removed'),
607
+ hunksApplied: NonNegativeIntegerSchema.optional().describe('Hunks applied'),
608
+ linesAdded: NonNegativeIntegerSchema.optional().describe('Lines added'),
609
+ linesRemoved: NonNegativeIntegerSchema.optional().describe('Lines removed'),
616
610
  results: z
617
611
  .array(z.strictObject({
618
612
  path: z.string().describe('File path'),
619
613
  applied: z.boolean().describe('Patch applied successfully'),
620
- hunksApplied: z.number().optional().describe('Hunks applied'),
621
- linesAdded: z.number().optional().describe('Lines added'),
622
- linesRemoved: z.number().optional().describe('Lines removed'),
623
- error: z.string().optional().describe('Error message'),
614
+ hunksApplied: NonNegativeIntegerSchema.optional().describe('Hunks applied'),
615
+ linesAdded: NonNegativeIntegerSchema.optional().describe('Lines added'),
616
+ linesRemoved: NonNegativeIntegerSchema.optional().describe('Lines removed'),
617
+ error: ErrorSchema.optional().describe('Structured error details'),
624
618
  }))
625
619
  .optional()
626
620
  .describe('Per-file results for multi-file patches'),
627
- error: ErrorSchema.optional(),
628
621
  });
629
622
  export const SearchAndReplaceInputSchema = z.strictObject({
630
623
  path: OptionalPathSchema.describe(DESC_PATH_ROOT),
@@ -671,22 +664,22 @@ export const SearchAndReplaceInputSchema = z.strictObject({
671
664
  .describe('Max files to process before stopping'),
672
665
  });
673
666
  export const SearchAndReplaceOutputSchema = z.strictObject({
674
- ok: z.boolean(),
675
- matches: z.number().optional().describe('Total matches found'),
676
- filesChanged: z.number().optional().describe('Files modified'),
677
- processedFiles: z.number().optional().describe('Files processed'),
678
- failedFiles: z.number().optional().describe('Files skipped due to errors'),
667
+ ok: SuccessFlagSchema,
668
+ matches: NonNegativeIntegerSchema.optional().describe('Total matches found'),
669
+ filesChanged: NonNegativeIntegerSchema.optional().describe('Files modified'),
670
+ processedFiles: NonNegativeIntegerSchema.optional().describe('Files processed'),
671
+ failedFiles: NonNegativeIntegerSchema.optional().describe('Files skipped due to errors'),
679
672
  failures: z
680
673
  .array(z.strictObject({
681
674
  path: z.string().describe('File path'),
682
- error: z.string().describe('Error message'),
675
+ error: ErrorSchema.describe('Structured error details'),
683
676
  }))
684
677
  .optional()
685
678
  .describe('Sample of per-file errors'),
686
679
  changedFiles: z
687
680
  .array(z.strictObject({
688
681
  path: z.string().describe('File path'),
689
- matches: z.number().describe('Matches in file'),
682
+ matches: PositiveIntegerSchema.describe('Matches in file'),
690
683
  }))
691
684
  .optional()
692
685
  .describe('Sample of changed files'),
@@ -703,6 +696,4 @@ export const SearchAndReplaceOutputSchema = z.strictObject({
703
696
  .enum(['maxFiles'])
704
697
  .optional()
705
698
  .describe('Why processing stopped early'),
706
- dryRun: z.boolean().optional(),
707
- error: ErrorSchema.optional(),
708
699
  });
@@ -22,7 +22,7 @@ const RootSchema = z.strictObject({
22
22
  uri: z.string(),
23
23
  name: z.string().optional(),
24
24
  });
25
- const RootsResponseSchema = z.object({
25
+ const RootsResponseSchema = z.strictObject({
26
26
  roots: z.array(RootSchema).optional(),
27
27
  });
28
28
  function isRoot(value) {