@j0hanz/filesystem-mcp 1.13.1 → 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 (65) hide show
  1. package/README.md +117 -100
  2. package/dist/completions.d.ts +0 -11
  3. package/dist/completions.js +1 -1
  4. package/dist/config.d.ts +0 -1
  5. package/dist/lib/constants.d.ts +0 -1
  6. package/dist/lib/constants.js +0 -1
  7. package/dist/lib/errors.d.ts +0 -1
  8. package/dist/lib/errors.js +5 -9
  9. package/dist/lib/file-operations/core.d.ts +3 -2
  10. package/dist/lib/file-operations/metadata.js +9 -3
  11. package/dist/lib/file-operations/search.d.ts +1 -59
  12. package/dist/lib/file-operations/search.js +9 -22
  13. package/dist/lib/file-operations/traversal.d.ts +2 -2
  14. package/dist/lib/fs-helpers.d.ts +1 -2
  15. package/dist/lib/fs-helpers.js +10 -12
  16. package/dist/lib/globs.d.ts +2 -0
  17. package/dist/lib/globs.js +19 -0
  18. package/dist/lib/paths.d.ts +0 -3
  19. package/dist/lib/paths.js +1 -4
  20. package/dist/lib/utils.d.ts +4 -4
  21. package/dist/lib/utils.js +1 -10
  22. package/dist/lib/zod-codecs.d.ts +2 -0
  23. package/dist/lib/zod-codecs.js +18 -0
  24. package/dist/pkg-info.d.ts +1 -0
  25. package/dist/pkg-info.js +2 -2
  26. package/dist/prompts.js +3 -3
  27. package/dist/resources/generated-instructions.js +3 -12
  28. package/dist/resources/tool-catalog.js +10 -41
  29. package/dist/resources/tool-info.d.ts +0 -1
  30. package/dist/resources/tool-info.js +24 -39
  31. package/dist/resources/workflows.js +8 -1
  32. package/dist/schemas.d.ts +179 -481
  33. package/dist/schemas.js +156 -169
  34. package/dist/server/bootstrap.d.ts +0 -10
  35. package/dist/server/bootstrap.js +3 -3
  36. package/dist/server/roots-manager.js +1 -1
  37. package/dist/tools/apply-patch.js +19 -8
  38. package/dist/tools/calculate-hash.js +3 -5
  39. package/dist/tools/create-directory.d.ts +1 -4
  40. package/dist/tools/create-directory.js +2 -2
  41. package/dist/tools/delete-file.js +2 -4
  42. package/dist/tools/diff-files.js +1 -3
  43. package/dist/tools/edit-file.d.ts +1 -7
  44. package/dist/tools/edit-file.js +6 -3
  45. package/dist/tools/list-directory.js +10 -15
  46. package/dist/tools/move-file.d.ts +1 -4
  47. package/dist/tools/move-file.js +15 -27
  48. package/dist/tools/read-multiple.js +12 -7
  49. package/dist/tools/read.js +1 -2
  50. package/dist/tools/replace-in-files.d.ts +1 -10
  51. package/dist/tools/replace-in-files.js +62 -97
  52. package/dist/tools/roots.js +2 -6
  53. package/dist/tools/search-content.js +152 -188
  54. package/dist/tools/search-files.js +10 -13
  55. package/dist/tools/shared.d.ts +10 -3
  56. package/dist/tools/shared.js +53 -12
  57. package/dist/tools/stat-many.js +6 -4
  58. package/dist/tools/stat.js +2 -2
  59. package/dist/tools/task-support.d.ts +0 -7
  60. package/dist/tools/task-support.js +7 -6
  61. package/dist/tools/tree.js +1 -1
  62. package/dist/tools/write-file.js +1 -5
  63. package/dist/tools.d.ts +0 -1
  64. package/dist/tools.js +0 -1
  65. package/package.json +5 -5
@@ -1,12 +1,11 @@
1
1
  import * as path from 'node:path';
2
- import { performance } from 'node:perf_hooks';
3
2
  import RE2 from 're2';
4
3
  import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
5
4
  import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
6
5
  import { searchContent } from '../lib/file-operations/search.js';
7
6
  import { formatOperationSummary } from '../config.js';
8
7
  import { SearchContentInputSchema, SearchContentOutputSchema, } from '../schemas.js';
9
- import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
+ import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, resolvePathOrRoot, truncateProgressPattern, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
10
9
  import { registerToolTaskIfAvailable } from './task-support.js';
11
10
  /**
12
11
  * Configuration constants for the Search Content tool.
@@ -19,13 +18,11 @@ const CONFIG = {
19
18
  maxFiles: 'max files',
20
19
  },
21
20
  };
22
- let searchMetricSequence = 0;
23
21
  const TRUTHY_SUMMARY_FIELDS = [
24
22
  'filesMatched',
25
23
  'skippedTooLarge',
26
24
  'skippedBinary',
27
25
  'skippedInaccessible',
28
- 'linesSkippedDueToRegexTimeout',
29
26
  ];
30
27
  function buildStructuredSummaryFields(summary) {
31
28
  const result = {};
@@ -53,15 +50,6 @@ function buildCompletionSuffix(count, filesMatched, scope, stoppedReason) {
53
50
  : '';
54
51
  return `${count} ${matchWord} in ${filesMatched} ${fileWord}${reasonSuffix}`;
55
52
  }
56
- function createSearchMetricNames() {
57
- searchMetricSequence += 1;
58
- const metricSuffix = `${Date.now()}_${searchMetricSequence}`;
59
- return {
60
- timerStartName: `searchContentStart_${metricSuffix}`,
61
- timerEndName: `searchContentEnd_${metricSuffix}`,
62
- metricName: `searchContent_${metricSuffix}`,
63
- };
64
- }
65
53
  function compareNormalizedMatches(left, right) {
66
54
  const fileCompare = left.relativeFile.localeCompare(right.relativeFile);
67
55
  if (fileCompare !== 0)
@@ -80,7 +68,7 @@ function buildSearchPreviewState(matches, payloads) {
80
68
  needsExternalize,
81
69
  visibleMatches,
82
70
  visiblePayloads: payloads.slice(0, visibleCount),
83
- heading: SearchResponseBuilder.buildHeading(matches.length, visibleMatches.length),
71
+ heading: buildHeading(matches.length, visibleMatches.length),
84
72
  };
85
73
  }
86
74
  export const SEARCH_CONTENT_TOOL = {
@@ -94,187 +82,164 @@ export const SEARCH_CONTENT_TOOL = {
94
82
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
95
83
  nuances: [
96
84
  'Inline results capped at 50 matches; full results via `resourceUri`.',
97
- 'Skips binary and oversized files.',
98
85
  ],
99
86
  gotchas: [
100
87
  'Skips binary/oversized files silently — verify with `stat` if no matches.',
101
88
  ],
102
89
  taskSupport: 'optional',
103
90
  };
104
- /**
105
- * Handles formatting and building the search response.
106
- */
107
- const SearchResponseBuilder = {
108
- buildHeading(totalMatches, visibleMatches) {
109
- if (visibleMatches >= totalMatches) {
110
- return `Found ${totalMatches}:`;
111
- }
112
- return `Found ${totalMatches} (showing first ${visibleMatches}):`;
113
- },
114
- buildText(heading, matches, summary) {
115
- if (matches.length === 0)
116
- return 'No matches';
117
- const text = this.buildMatchList(heading, matches);
118
- if (!summary)
119
- return text;
120
- const summaryOpts = {
121
- truncated: summary.truncated,
122
- ...(summary.truncated
123
- ? { truncatedReason: this.resolveTruncatedReason(summary) }
124
- : {}),
125
- };
126
- return text + formatOperationSummary(summaryOpts);
127
- },
128
- buildStructured(summary, matches, options) {
91
+ function buildHeading(totalMatches, visibleMatches) {
92
+ if (visibleMatches >= totalMatches) {
93
+ return `Found ${totalMatches}:`;
94
+ }
95
+ return `Found ${totalMatches} (showing first ${visibleMatches}):`;
96
+ }
97
+ function buildSearchText(heading, matches, summary) {
98
+ if (matches.length === 0)
99
+ return 'No matches';
100
+ const text = buildMatchList(heading, matches);
101
+ if (!summary)
102
+ return text;
103
+ const summaryOpts = {
104
+ truncated: summary.truncated,
105
+ ...(summary.truncated
106
+ ? { truncatedReason: resolveTruncatedReason(summary) }
107
+ : {}),
108
+ };
109
+ return text + formatOperationSummary(summaryOpts);
110
+ }
111
+ function buildSearchStructured(summary, matches) {
112
+ return {
113
+ ok: true,
114
+ matches,
115
+ totalMatches: summary.matches,
116
+ filesScanned: summary.filesScanned,
117
+ ...buildStructuredSummaryFields(summary),
118
+ };
119
+ }
120
+ function normalizeMatches(result) {
121
+ const relativeByFile = new Map();
122
+ const getRelativeFile = (file) => {
123
+ const cached = relativeByFile.get(file);
124
+ if (cached !== undefined)
125
+ return cached;
126
+ const relative = path.relative(result.basePath, file);
127
+ relativeByFile.set(file, relative);
128
+ return relative;
129
+ };
130
+ return result.matches
131
+ .map((match, index) => ({
132
+ ...match,
133
+ relativeFile: getRelativeFile(match.file),
134
+ index,
135
+ }))
136
+ .sort(compareNormalizedMatches);
137
+ }
138
+ function buildMatchPayloads(matches, context) {
139
+ return matches.map((match) => {
140
+ const column = findColumnOffset(match.content, context);
129
141
  return {
130
- ok: true,
131
- patternType: options.patternType,
132
- caseSensitive: options.caseSensitive,
133
- matches,
134
- totalMatches: summary.matches,
135
- filesScanned: summary.filesScanned,
136
- ...buildStructuredSummaryFields(summary),
137
- };
138
- },
139
- normalizeMatches(result) {
140
- const relativeByFile = new Map();
141
- const getRelativeFile = (file) => {
142
- const cached = relativeByFile.get(file);
143
- if (cached !== undefined)
144
- return cached;
145
- const relative = path.relative(result.basePath, file);
146
- relativeByFile.set(file, relative);
147
- return relative;
142
+ file: match.relativeFile,
143
+ line: match.line,
144
+ ...(column !== undefined ? { column } : {}),
145
+ content: match.content,
146
+ matchCount: match.matchCount,
147
+ ...(match.contextBefore
148
+ ? { contextBefore: [...match.contextBefore] }
149
+ : {}),
150
+ ...(match.contextAfter ? { contextAfter: [...match.contextAfter] } : {}),
148
151
  };
149
- return result.matches
150
- .map((match, index) => ({
151
- ...match,
152
- relativeFile: getRelativeFile(match.file),
153
- index,
154
- }))
155
- .sort(compareNormalizedMatches);
156
- },
157
- buildMatchPayloads(matches, context) {
158
- return matches.map((match) => {
159
- const column = this.findColumnOffset(match.content, context);
160
- return {
161
- file: match.relativeFile,
162
- line: match.line,
163
- ...(column !== undefined ? { column } : {}),
164
- content: match.content,
165
- matchCount: match.matchCount,
166
- ...(match.contextBefore
167
- ? { contextBefore: [...match.contextBefore] }
168
- : {}),
169
- ...(match.contextAfter
170
- ? { contextAfter: [...match.contextAfter] }
171
- : {}),
172
- };
173
- });
174
- },
175
- buildMatchList(heading, matches) {
176
- if (matches.length === 0)
177
- return heading;
178
- const parts = [heading];
179
- for (const match of matches) {
180
- parts.push(`\n ${match.relativeFile}:${String(match.line).padStart(4)}: ${match.content}`);
181
- }
182
- return parts.join('');
183
- },
184
- resolveTruncatedReason(summary) {
185
- if (summary.stoppedReason === 'timeout')
186
- return 'timeout';
187
- if (summary.stoppedReason === 'maxFiles') {
188
- return `max files (${summary.filesScanned})`;
152
+ });
153
+ }
154
+ function buildMatchList(heading, matches) {
155
+ if (matches.length === 0)
156
+ return heading;
157
+ const parts = [heading];
158
+ for (const match of matches) {
159
+ parts.push(`\n ${match.relativeFile}:${String(match.line).padStart(4)}: ${match.content}`);
160
+ }
161
+ return parts.join('');
162
+ }
163
+ function resolveTruncatedReason(summary) {
164
+ if (summary.stoppedReason === 'timeout')
165
+ return 'timeout';
166
+ if (summary.stoppedReason === 'maxFiles') {
167
+ return `max files (${summary.filesScanned})`;
168
+ }
169
+ return `max results (${summary.matches})`;
170
+ }
171
+ function findColumnOffset(content, context) {
172
+ try {
173
+ if (context.matcher) {
174
+ context.matcher.lastIndex = 0;
175
+ const match = context.matcher.exec(content);
176
+ return match ? match.index : undefined;
189
177
  }
190
- return `max results (${summary.matches})`;
191
- },
192
- findColumnOffset(content, context) {
193
- try {
194
- if (context.matcher) {
195
- context.matcher.lastIndex = 0;
196
- const match = context.matcher.exec(content);
197
- return match ? match.index : undefined;
198
- }
199
- if (context.caseSensitive) {
200
- const idx = content.indexOf(context.pattern);
201
- return idx >= 0 ? idx : undefined;
202
- }
203
- // Case-insensitive literal search
204
- const idx = content.toLowerCase().indexOf(context.foldedPattern ?? '');
178
+ if (context.caseSensitive) {
179
+ const idx = content.indexOf(context.pattern);
205
180
  return idx >= 0 ? idx : undefined;
206
181
  }
207
- catch {
208
- return undefined;
209
- }
210
- },
211
- };
212
- const SearchExecutor = {
213
- async run(args, basePath, signal, onProgress) {
214
- const excludePatterns = args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS;
215
- const { timerStartName, timerEndName, metricName } = createSearchMetricNames();
216
- const options = {
217
- includeHidden: args.includeHidden,
218
- excludePatterns,
219
- filePattern: args.filePattern,
220
- caseSensitive: args.caseSensitive,
221
- wholeWord: args.wholeWord,
222
- contextLines: args.contextLines,
223
- maxResults: args.maxResults,
224
- isLiteral: !args.isRegex,
225
- multiline: args.multiline,
226
- ...(signal ? { signal } : {}),
227
- ...(onProgress ? { onProgress } : {}),
228
- };
229
- performance.mark(timerStartName);
230
- try {
231
- return await searchContent(basePath, args.pattern, options);
232
- }
233
- catch (error) {
234
- if (error instanceof Error && /regular expression/i.test(error.message)) {
235
- throw new McpError(ErrorCode.E_INVALID_PATTERN, error.message);
236
- }
237
- throw error;
238
- }
239
- finally {
240
- performance.mark(timerEndName);
241
- performance.measure(metricName, timerStartName, timerEndName);
242
- performance.clearMarks(timerStartName);
243
- performance.clearMarks(timerEndName);
244
- performance.clearMeasures(metricName);
245
- }
246
- },
247
- createMatcher(args) {
248
- if (!args.isRegex)
249
- return undefined;
250
- try {
251
- const flags = (args.caseSensitive ? '' : 'i') + (args.multiline ? 'm' : '');
252
- return new RE2(args.pattern, flags);
253
- }
254
- catch (error) {
255
- throw new McpError(ErrorCode.E_INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
182
+ // Case-insensitive literal search
183
+ const idx = content.toLowerCase().indexOf(context.foldedPattern ?? '');
184
+ return idx >= 0 ? idx : undefined;
185
+ }
186
+ catch {
187
+ return undefined;
188
+ }
189
+ }
190
+ async function executeSearch(args, basePath, signal, onProgress) {
191
+ const excludePatterns = args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS;
192
+ const options = {
193
+ includeHidden: args.includeHidden,
194
+ excludePatterns,
195
+ filePattern: args.filePattern,
196
+ caseSensitive: args.caseSensitive,
197
+ wholeWord: args.wholeWord,
198
+ contextLines: args.contextLines,
199
+ maxResults: args.maxResults,
200
+ isLiteral: !args.isRegex,
201
+ ...(signal ? { signal } : {}),
202
+ ...(onProgress ? { onProgress } : {}),
203
+ };
204
+ try {
205
+ return await searchContent(basePath, args.pattern, options);
206
+ }
207
+ catch (error) {
208
+ if (error instanceof Error && /regular expression/i.test(error.message)) {
209
+ throw new McpError(ErrorCode.E_INVALID_PATTERN, error.message);
256
210
  }
257
- },
258
- createSearchContext(args, matcher) {
259
- return {
260
- pattern: args.pattern,
261
- caseSensitive: args.caseSensitive,
262
- ...(matcher ? { matcher } : {}),
263
- ...(!args.isRegex && !args.caseSensitive
264
- ? { foldedPattern: args.pattern.toLowerCase() }
265
- : {}),
266
- };
267
- },
268
- };
211
+ throw error;
212
+ }
213
+ }
214
+ function createSearchMatcher(args) {
215
+ if (!args.isRegex)
216
+ return undefined;
217
+ try {
218
+ const flags = args.caseSensitive ? '' : 'i';
219
+ return new RE2(args.pattern, flags);
220
+ }
221
+ catch (error) {
222
+ throw new McpError(ErrorCode.E_INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
223
+ }
224
+ }
225
+ function createSearchContext(args, matcher) {
226
+ return {
227
+ pattern: args.pattern,
228
+ caseSensitive: args.caseSensitive,
229
+ ...(matcher ? { matcher } : {}),
230
+ ...(!args.isRegex && !args.caseSensitive
231
+ ? { foldedPattern: args.pattern.toLowerCase() }
232
+ : {}),
233
+ };
234
+ }
269
235
  async function handleSearchContent(args, signal, resourceStore, onProgress) {
270
236
  const basePath = resolvePathOrRoot(args.path);
271
- const patternType = args.isRegex ? 'regex' : 'literal';
272
- const regexMatcher = SearchExecutor.createMatcher(args);
273
- const result = await SearchExecutor.run(args, basePath, signal, onProgress);
274
- const normalizedMatches = SearchResponseBuilder.normalizeMatches(result);
275
- const searchContext = SearchExecutor.createSearchContext(args, regexMatcher);
276
- const matchPayloads = SearchResponseBuilder.buildMatchPayloads(normalizedMatches, searchContext);
277
- const fullStructured = SearchResponseBuilder.buildStructured(result.summary, matchPayloads, { patternType, caseSensitive: args.caseSensitive });
237
+ const regexMatcher = createSearchMatcher(args);
238
+ const result = await executeSearch(args, basePath, signal, onProgress);
239
+ const normalizedMatches = normalizeMatches(result);
240
+ const searchContext = createSearchContext(args, regexMatcher);
241
+ const matchPayloads = buildMatchPayloads(normalizedMatches, searchContext);
242
+ const fullStructured = buildSearchStructured(result.summary, matchPayloads);
278
243
  const preview = buildSearchPreviewState(normalizedMatches, matchPayloads);
279
244
  if (resourceStore && preview.needsExternalize) {
280
245
  const previewStructured = {
@@ -288,7 +253,7 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
288
253
  text: JSON.stringify(fullStructured),
289
254
  });
290
255
  previewStructured.resourceUri = entry.uri;
291
- const text = SearchResponseBuilder.buildText(preview.heading, preview.visibleMatches);
256
+ const text = buildSearchText(preview.heading, preview.visibleMatches);
292
257
  return buildToolResponse(text, previewStructured, [
293
258
  buildResourceLink({
294
259
  uri: entry.uri,
@@ -299,17 +264,18 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
299
264
  }),
300
265
  ]);
301
266
  }
302
- const text = SearchResponseBuilder.buildText(preview.heading, preview.visibleMatches, result.summary);
267
+ const text = buildSearchText(preview.heading, preview.visibleMatches, result.summary);
303
268
  return buildToolResponse(text, fullStructured);
304
269
  }
305
270
  export function registerSearchContentTool(server, options = {}) {
306
271
  const handler = (args, extra) => executeToolWithDiagnostics({
307
272
  toolName: 'grep',
308
273
  extra,
274
+ outputSchema: SearchContentOutputSchema,
309
275
  context: { path: args.path ?? '.' },
310
276
  run: async (signal) => {
311
277
  const { pattern, filePattern: scope } = args;
312
- const progressLabel = `🔎︎ grep: ${pattern}`;
278
+ const progressLabel = `🔎︎ grep: ${truncateProgressPattern(pattern)}`;
313
279
  const progress = createToolProgressSession(extra, progressLabel);
314
280
  const progressWithMessage = ({ current, total, }) => {
315
281
  progress.update({
@@ -321,10 +287,8 @@ export function registerSearchContentTool(server, options = {}) {
321
287
  try {
322
288
  const result = await handleSearchContent(args, signal, options.resourceStore, progressWithMessage);
323
289
  const sc = result.structuredContent;
324
- const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
325
- const filesMatched = sc.ok ? (sc.filesMatched ?? 0) : 0;
326
- const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
327
- const suffix = buildCompletionSuffix(count, filesMatched, scope, stoppedReason);
290
+ const { totalMatches = 0, filesMatched = 0, stoppedReason } = sc;
291
+ const suffix = buildCompletionSuffix(totalMatches, filesMatched, scope, stoppedReason);
328
292
  const finalCurrent = resolveFinalProgressCurrent(progress, (sc.filesScanned ?? 0) + 1);
329
293
  progress.complete(`${progressLabel} • ${suffix}`, finalCurrent);
330
294
  return result;
@@ -4,7 +4,7 @@ import { ErrorCode } from '../lib/errors.js';
4
4
  import { searchFiles } from '../lib/file-operations/search.js';
5
5
  import { formatOperationSummary, joinLines } from '../config.js';
6
6
  import { SearchFilesInputSchema, SearchFilesOutputSchema } from '../schemas.js';
7
- import { buildToolErrorResponse, buildToolResponse, createProgressReporter, decodeOffsetCursor, encodeOffsetCursor, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
7
+ import { buildToolErrorResponse, buildToolResponse, createProgressReporter, decodeOffsetCursor, encodeOffsetCursor, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, truncateProgressPattern, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  import { registerToolTaskIfAvailable } from './task-support.js';
9
9
  export const SEARCH_FILES_TOOL = {
10
10
  name: 'find',
@@ -14,10 +14,7 @@ export const SEARCH_FILES_TOOL = {
14
14
  inputSchema: SearchFilesInputSchema,
15
15
  outputSchema: SearchFilesOutputSchema,
16
16
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
17
- nuances: [
18
- 'Respects `.gitignore` unless `includeIgnored=true`.',
19
- 'Returns relative paths plus metadata; may truncate.',
20
- ],
17
+ nuances: ['Respects `.gitignore` unless `includeIgnored=true`.'],
21
18
  taskSupport: 'optional',
22
19
  };
23
20
  async function handleSearchFiles(args, signal, onProgress) {
@@ -52,7 +49,6 @@ async function handleSearchFiles(args, signal, onProgress) {
52
49
  const structured = {
53
50
  ok: true,
54
51
  root: basePath,
55
- pattern: args.pattern,
56
52
  results: relativeResults,
57
53
  totalMatches: result.summary.matched,
58
54
  filesScanned: result.summary.filesScanned,
@@ -103,17 +99,19 @@ export function registerSearchFilesTool(server, options = {}) {
103
99
  const handler = (args, extra) => executeToolWithDiagnostics({
104
100
  toolName: 'find',
105
101
  extra,
102
+ outputSchema: SearchFilesOutputSchema,
106
103
  timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
107
104
  context: { path: args.path ?? '.' },
108
105
  run: async (signal) => {
109
106
  const rawScopeLabel = args.path ? path.basename(args.path) : '.';
110
107
  const scopeLabel = rawScopeLabel || '.';
111
108
  const { pattern } = args;
112
- const context = `${pattern} in ${scopeLabel}`;
109
+ const truncatedPattern = truncateProgressPattern(pattern);
110
+ const context = `${truncatedPattern} in ${scopeLabel}`;
113
111
  let progressCursor = 0;
114
112
  notifyProgress(extra, {
115
113
  current: 0,
116
- message: `🔎︎ find: ${pattern}`,
114
+ message: `🔎︎ find: ${truncatedPattern}`,
117
115
  });
118
116
  const baseReporter = createProgressReporter(extra);
119
117
  const progressWithMessage = ({ current, total, }) => {
@@ -122,20 +120,19 @@ export function registerSearchFilesTool(server, options = {}) {
122
120
  baseReporter({
123
121
  current,
124
122
  ...(total !== undefined ? { total } : {}),
125
- message: `🔎︎ find: ${pattern} [${current} files]`,
123
+ message: `🔎︎ find: ${truncatedPattern} [${current} files]`,
126
124
  });
127
125
  };
128
126
  try {
129
127
  const result = await handleSearchFiles(args, signal, progressWithMessage);
130
128
  const sc = result.structuredContent;
131
- const count = sc.ok ? (sc.totalMatches ?? 0) : 0;
132
- const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
129
+ const { totalMatches = 0, stoppedReason } = sc;
133
130
  let suffix;
134
- if (count === 0) {
131
+ if (totalMatches === 0) {
135
132
  suffix = 'No matches';
136
133
  }
137
134
  else {
138
- suffix = `${count} ${count === 1 ? 'match' : 'matches'}`;
135
+ suffix = `${totalMatches} ${totalMatches === 1 ? 'match' : 'matches'}`;
139
136
  if (stoppedReason === 'timeout') {
140
137
  suffix += ' [timeout]';
141
138
  }
@@ -22,7 +22,6 @@ export declare const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS: {
22
22
  readonly destructiveHint: false;
23
23
  readonly openWorldHint: false;
24
24
  };
25
- export declare function shouldStripStructuredOutput(): boolean;
26
25
  export declare function maybeStripStructuredContentFromResult<T extends object>(result: T): T;
27
26
  type ResourceEntry = ReturnType<ResourceStore['putText']>;
28
27
  export declare function maybeExternalizeTextContent(resourceStore: ResourceStore | undefined, content: string, params: {
@@ -39,6 +38,12 @@ export declare function buildResourceLink(params: {
39
38
  description?: string;
40
39
  expiresAt?: string;
41
40
  }): ContentBlock;
41
+ export declare function buildStructuredError(error: unknown, defaultCode: ErrorCode, path?: string): {
42
+ code: ErrorCode;
43
+ message: string;
44
+ path?: string;
45
+ suggestion?: string;
46
+ };
42
47
  export declare function buildToolResponse<T>(text: string, structuredContent: T, extraContent?: ContentBlock[]): {
43
48
  content: ContentBlock[];
44
49
  structuredContent: T;
@@ -95,6 +100,7 @@ export declare function buildFileInfoPayload(info: FileInfo): FileInfoPayload;
95
100
  interface ToolExecutionOptions<T> {
96
101
  toolName: string;
97
102
  extra: ToolExtra;
103
+ outputSchema?: z.ZodType<T>;
98
104
  run: (signal: AbortSignal | undefined) => ToolResponse<T> | Promise<ToolResponse<T>>;
99
105
  onError: (error: unknown) => ToolResult<T>;
100
106
  context?: Record<string, unknown>;
@@ -114,7 +120,7 @@ export declare function notifyProgress(extra: ToolExtra, progress: {
114
120
  total?: number;
115
121
  message?: string;
116
122
  }): void;
117
- export interface ToolProgressSession {
123
+ interface ToolProgressSession {
118
124
  update: (progress: {
119
125
  current: number;
120
126
  total?: number;
@@ -125,7 +131,7 @@ export interface ToolProgressSession {
125
131
  fail: (message: string, minimumCurrent?: number) => void;
126
132
  getCurrent: () => number;
127
133
  }
128
- export interface BatchProgressCallbacks {
134
+ interface BatchProgressCallbacks {
129
135
  progress: ToolProgressSession;
130
136
  onItemComplete: () => void;
131
137
  }
@@ -155,6 +161,7 @@ export declare function resolvePathOrRoot(pathValue: string | undefined): string
155
161
  export declare function encodeOffsetCursor(offset: number): string;
156
162
  export declare function decodeOffsetCursor(cursor: string): number;
157
163
  export declare function buildBatchPathContext(paths: readonly string[], unitLabel?: string): string;
164
+ export declare function truncateProgressPattern(pattern: string, maxLength?: number): string;
158
165
  export declare function buildBatchCompletionSuffix(summary: {
159
166
  total?: number;
160
167
  failed?: number;