@j0hanz/filesystem-mcp 1.13.2 → 1.14.1

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 (74) hide show
  1. package/README.md +162 -145
  2. package/dist/cli.js +2 -2
  3. package/dist/completions.js +54 -51
  4. package/dist/config.d.ts +13 -14
  5. package/dist/config.js +12 -12
  6. package/dist/index.js +1 -1
  7. package/dist/lib/abort.d.ts +7 -0
  8. package/dist/lib/abort.js +81 -0
  9. package/dist/lib/constants.d.ts +3 -1
  10. package/dist/lib/constants.js +8 -2
  11. package/dist/lib/errors.d.ts +7 -3
  12. package/dist/lib/errors.js +64 -41
  13. package/dist/lib/file-operations/core.d.ts +3 -3
  14. package/dist/lib/file-operations/core.js +23 -20
  15. package/dist/lib/file-operations/metadata.d.ts +2 -2
  16. package/dist/lib/file-operations/metadata.js +69 -22
  17. package/dist/lib/file-operations/search.d.ts +0 -1
  18. package/dist/lib/file-operations/search.js +87 -95
  19. package/dist/lib/file-operations/traversal.js +13 -15
  20. package/dist/lib/fs-helpers.d.ts +3 -10
  21. package/dist/lib/fs-helpers.js +29 -108
  22. package/dist/lib/globs.d.ts +2 -0
  23. package/dist/lib/globs.js +19 -0
  24. package/dist/lib/logger.d.ts +28 -0
  25. package/dist/lib/logger.js +91 -0
  26. package/dist/lib/observability.d.ts +7 -0
  27. package/dist/lib/observability.js +19 -9
  28. package/dist/lib/paths.js +55 -55
  29. package/dist/lib/resource-store.js +4 -4
  30. package/dist/lib/utils.d.ts +0 -12
  31. package/dist/lib/utils.js +0 -13
  32. package/dist/lib/zod-codecs.d.ts +2 -0
  33. package/dist/lib/zod-codecs.js +18 -0
  34. package/dist/pkg-info.d.ts +1 -0
  35. package/dist/pkg-info.js +2 -2
  36. package/dist/prompts.js +3 -3
  37. package/dist/resources/generated-instructions.js +41 -41
  38. package/dist/resources/tool-catalog.js +33 -58
  39. package/dist/resources/tool-info.d.ts +0 -1
  40. package/dist/resources/tool-info.js +44 -67
  41. package/dist/resources/workflows.js +47 -19
  42. package/dist/resources.d.ts +1 -1
  43. package/dist/resources.js +4 -4
  44. package/dist/schemas.d.ts +185 -465
  45. package/dist/schemas.js +174 -206
  46. package/dist/server/bootstrap.d.ts +12 -11
  47. package/dist/server/bootstrap.js +95 -86
  48. package/dist/server/roots-manager.d.ts +5 -2
  49. package/dist/server/roots-manager.js +9 -7
  50. package/dist/server/task-store.d.ts +10 -0
  51. package/dist/server/task-store.js +73 -0
  52. package/dist/tools/apply-patch.js +39 -20
  53. package/dist/tools/calculate-hash.js +14 -27
  54. package/dist/tools/create-directory.js +11 -9
  55. package/dist/tools/delete-file.js +19 -19
  56. package/dist/tools/diff-files.js +16 -18
  57. package/dist/tools/edit-file.js +11 -5
  58. package/dist/tools/list-directory.js +16 -21
  59. package/dist/tools/move-file.js +105 -100
  60. package/dist/tools/read-multiple.js +15 -10
  61. package/dist/tools/read.js +6 -7
  62. package/dist/tools/replace-in-files.js +76 -115
  63. package/dist/tools/roots.js +3 -7
  64. package/dist/tools/search-content.js +158 -203
  65. package/dist/tools/search-files.js +59 -50
  66. package/dist/tools/shared.d.ts +10 -0
  67. package/dist/tools/shared.js +105 -36
  68. package/dist/tools/stat-many.js +15 -9
  69. package/dist/tools/stat.js +6 -6
  70. package/dist/tools/task-support.d.ts +10 -9
  71. package/dist/tools/task-support.js +94 -23
  72. package/dist/tools/tree.js +4 -4
  73. package/dist/tools/write-file.js +11 -12
  74. package/package.json +10 -9
@@ -1,9 +1,8 @@
1
- import * as path from 'node:path';
2
- import { performance } from 'node:perf_hooks';
1
+ import { relative } from 'node:path';
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
- import { searchContent } from '../lib/file-operations/search.js';
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
8
  import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, resolvePathOrRoot, truncateProgressPattern, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
@@ -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,34 +50,15 @@ 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
- function compareNormalizedMatches(left, right) {
66
- const fileCompare = left.relativeFile.localeCompare(right.relativeFile);
67
- if (fileCompare !== 0)
68
- return fileCompare;
69
- if (left.line !== right.line)
70
- return left.line - right.line;
71
- return left.index - right.index;
72
- }
73
- function buildSearchPreviewState(matches, payloads) {
74
- const needsExternalize = matches.length > CONFIG.MAX_INLINE_MATCHES;
53
+ function buildSearchPreviewState(payloads) {
54
+ const needsExternalize = payloads.length > CONFIG.MAX_INLINE_MATCHES;
75
55
  const visibleCount = needsExternalize
76
56
  ? CONFIG.MAX_INLINE_MATCHES
77
- : matches.length;
78
- const visibleMatches = matches.slice(0, visibleCount);
57
+ : payloads.length;
79
58
  return {
80
59
  needsExternalize,
81
- visibleMatches,
82
60
  visiblePayloads: payloads.slice(0, visibleCount),
83
- heading: SearchResponseBuilder.buildHeading(matches.length, visibleMatches.length),
61
+ heading: buildHeading(payloads.length, visibleCount),
84
62
  };
85
63
  }
86
64
  export const SEARCH_CONTENT_TOOL = {
@@ -94,188 +72,166 @@ export const SEARCH_CONTENT_TOOL = {
94
72
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
95
73
  nuances: [
96
74
  'Inline results capped at 50 matches; full results via `resourceUri`.',
97
- 'Skips binary and oversized files.',
98
75
  ],
99
76
  gotchas: [
100
77
  'Skips binary/oversized files silently — verify with `stat` if no matches.',
101
78
  ],
102
79
  taskSupport: 'optional',
103
80
  };
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) }
81
+ function buildHeading(totalMatches, visibleMatches) {
82
+ if (visibleMatches >= totalMatches) {
83
+ return `Found ${totalMatches}:`;
84
+ }
85
+ return `Found ${totalMatches} (showing first ${visibleMatches}):`;
86
+ }
87
+ function buildSearchText(heading, matches, summary) {
88
+ if (matches.length === 0)
89
+ return 'No matches';
90
+ const text = buildMatchList(heading, matches);
91
+ if (!summary)
92
+ return text;
93
+ const summaryOpts = {
94
+ truncated: summary.truncated,
95
+ ...(summary.truncated
96
+ ? { truncatedReason: resolveTruncatedReason(summary) }
97
+ : {}),
98
+ };
99
+ return text + formatOperationSummary(summaryOpts);
100
+ }
101
+ function buildSearchStructured(summary, matches) {
102
+ return {
103
+ ok: true,
104
+ matches,
105
+ totalMatches: summary.matches,
106
+ filesScanned: summary.filesScanned,
107
+ ...buildStructuredSummaryFields(summary),
108
+ };
109
+ }
110
+ function buildSortedPayloads(result, context) {
111
+ const relativeByFile = new Map();
112
+ const getRelativeFile = (file) => {
113
+ const cached = relativeByFile.get(file);
114
+ if (cached !== undefined)
115
+ return cached;
116
+ const rel = relative(result.basePath, file);
117
+ relativeByFile.set(file, rel);
118
+ return rel;
119
+ };
120
+ const payloads = result.matches.map((match, originalIndex) => {
121
+ const relFile = getRelativeFile(match.file);
122
+ const column = findColumnOffset(match.content, context);
123
+ const payload = {
124
+ file: relFile,
125
+ line: match.line,
126
+ ...(column !== undefined ? { column } : {}),
127
+ content: match.content,
128
+ matchCount: match.matchCount,
129
+ ...(match.contextBefore
130
+ ? { contextBefore: [...match.contextBefore] }
124
131
  : {}),
132
+ ...(match.contextAfter ? { contextAfter: [...match.contextAfter] } : {}),
125
133
  };
126
- return text + formatOperationSummary(summaryOpts);
127
- },
128
- buildStructured(summary, matches, options) {
129
- 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;
148
- };
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})`;
134
+ return { payload, originalIndex };
135
+ });
136
+ payloads.sort((left, right) => {
137
+ const fileCompare = left.payload.file.localeCompare(right.payload.file);
138
+ if (fileCompare !== 0)
139
+ return fileCompare;
140
+ if (left.payload.line !== right.payload.line)
141
+ return left.payload.line - right.payload.line;
142
+ return left.originalIndex - right.originalIndex;
143
+ });
144
+ return payloads.map((p) => p.payload);
145
+ }
146
+ function buildMatchList(heading, matches) {
147
+ if (matches.length === 0)
148
+ return heading;
149
+ const parts = [heading];
150
+ for (const match of matches) {
151
+ parts.push(`\n ${match.file}:${String(match.line).padStart(4)}: ${match.content}`);
152
+ }
153
+ return parts.join('');
154
+ }
155
+ function resolveTruncatedReason(summary) {
156
+ if (summary.stoppedReason === 'timeout')
157
+ return 'timeout';
158
+ if (summary.stoppedReason === 'maxFiles') {
159
+ return `max files (${summary.filesScanned})`;
160
+ }
161
+ return `max results (${summary.matches})`;
162
+ }
163
+ function findColumnOffset(content, context) {
164
+ try {
165
+ if (context.matcher) {
166
+ context.matcher.lastIndex = 0;
167
+ const match = context.matcher.exec(content);
168
+ return match ? match.index : undefined;
189
169
  }
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 ?? '');
170
+ if (context.caseSensitive) {
171
+ const idx = content.indexOf(context.pattern);
205
172
  return idx >= 0 ? idx : undefined;
206
173
  }
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)}`);
174
+ // Case-insensitive literal search
175
+ const idx = content.toLowerCase().indexOf(context.foldedPattern ?? '');
176
+ return idx >= 0 ? idx : undefined;
177
+ }
178
+ catch {
179
+ return undefined;
180
+ }
181
+ }
182
+ async function executeSearch(args, basePath, signal, onProgress) {
183
+ const excludePatterns = args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS;
184
+ const options = {
185
+ includeHidden: args.includeHidden,
186
+ excludePatterns,
187
+ filePattern: args.filePattern,
188
+ caseSensitive: args.caseSensitive,
189
+ wholeWord: args.wholeWord,
190
+ contextLines: args.contextLines,
191
+ maxResults: args.maxResults,
192
+ isLiteral: !args.isRegex,
193
+ ...(signal ? { signal } : {}),
194
+ ...(onProgress ? { onProgress } : {}),
195
+ };
196
+ try {
197
+ return await searchContent(basePath, args.pattern, options);
198
+ }
199
+ catch (error) {
200
+ if (error instanceof Error && /regular expression/i.test(error.message)) {
201
+ throw new McpError(ErrorCode.INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)} (RE2: no lookahead/lookbehind/backrefs)`);
256
202
  }
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
- };
203
+ throw error;
204
+ }
205
+ }
206
+ function createSearchMatcher(args) {
207
+ if (!args.isRegex)
208
+ return undefined;
209
+ try {
210
+ const flags = args.caseSensitive ? '' : 'i';
211
+ return new RE2(args.pattern, flags);
212
+ }
213
+ catch (error) {
214
+ throw new McpError(ErrorCode.INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)} (RE2: no lookahead/lookbehind/backrefs)`);
215
+ }
216
+ }
217
+ function createSearchContext(args, matcher) {
218
+ return {
219
+ pattern: args.pattern,
220
+ caseSensitive: args.caseSensitive,
221
+ ...(matcher ? { matcher } : {}),
222
+ ...(!args.isRegex && !args.caseSensitive
223
+ ? { foldedPattern: args.pattern.toLowerCase() }
224
+ : {}),
225
+ };
226
+ }
269
227
  async function handleSearchContent(args, signal, resourceStore, onProgress) {
270
228
  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 });
278
- const preview = buildSearchPreviewState(normalizedMatches, matchPayloads);
229
+ const regexMatcher = createSearchMatcher(args);
230
+ const result = await executeSearch(args, basePath, signal, onProgress);
231
+ const searchContext = createSearchContext(args, regexMatcher);
232
+ const matchPayloads = buildSortedPayloads(result, searchContext);
233
+ const fullStructured = buildSearchStructured(result.summary, matchPayloads);
234
+ const preview = buildSearchPreviewState(matchPayloads);
279
235
  if (resourceStore && preview.needsExternalize) {
280
236
  const previewStructured = {
281
237
  ...fullStructured,
@@ -288,7 +244,7 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
288
244
  text: JSON.stringify(fullStructured),
289
245
  });
290
246
  previewStructured.resourceUri = entry.uri;
291
- const text = SearchResponseBuilder.buildText(preview.heading, preview.visibleMatches);
247
+ const text = buildSearchText(preview.heading, preview.visiblePayloads);
292
248
  return buildToolResponse(text, previewStructured, [
293
249
  buildResourceLink({
294
250
  uri: entry.uri,
@@ -299,13 +255,14 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
299
255
  }),
300
256
  ]);
301
257
  }
302
- const text = SearchResponseBuilder.buildText(preview.heading, preview.visibleMatches, result.summary);
258
+ const text = buildSearchText(preview.heading, preview.visiblePayloads, result.summary);
303
259
  return buildToolResponse(text, fullStructured);
304
260
  }
305
261
  export function registerSearchContentTool(server, options = {}) {
306
262
  const handler = (args, extra) => executeToolWithDiagnostics({
307
263
  toolName: 'grep',
308
264
  extra,
265
+ outputSchema: SearchContentOutputSchema,
309
266
  context: { path: args.path ?? '.' },
310
267
  run: async (signal) => {
311
268
  const { pattern, filePattern: scope } = args;
@@ -321,10 +278,8 @@ export function registerSearchContentTool(server, options = {}) {
321
278
  try {
322
279
  const result = await handleSearchContent(args, signal, options.resourceStore, progressWithMessage);
323
280
  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);
281
+ const { totalMatches = 0, filesMatched = 0, stoppedReason } = sc;
282
+ const suffix = buildCompletionSuffix(totalMatches, filesMatched, scope, stoppedReason);
328
283
  const finalCurrent = resolveFinalProgressCurrent(progress, (sc.filesScanned ?? 0) + 1);
329
284
  progress.complete(`${progressLabel} • ${suffix}`, finalCurrent);
330
285
  return result;
@@ -334,7 +289,7 @@ export function registerSearchContentTool(server, options = {}) {
334
289
  throw error;
335
290
  }
336
291
  },
337
- onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path ?? '.'),
292
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path ?? '.'),
338
293
  });
339
294
  const { isInitialized } = options;
340
295
  const wrappedHandler = wrapToolHandler(handler, {
@@ -1,4 +1,4 @@
1
- import * as path from 'node:path';
1
+ import { basename, relative } from 'node:path';
2
2
  import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_TIMEOUT_MS, } from '../lib/constants.js';
3
3
  import { ErrorCode } from '../lib/errors.js';
4
4
  import { searchFiles } from '../lib/file-operations/search.js';
@@ -14,12 +14,45 @@ 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
  };
20
+ function buildTruncatedReason(summary) {
21
+ if (!summary.truncated)
22
+ return undefined;
23
+ if (summary.stoppedReason === 'timeout')
24
+ return 'timeout';
25
+ if (summary.stoppedReason === 'maxFiles')
26
+ return `max files (${summary.filesScanned})`;
27
+ return `max results (${summary.matched})`;
28
+ }
29
+ function buildRelativeResults(basePath, displayResults) {
30
+ const relativeResults = [];
31
+ for (const entry of displayResults) {
32
+ relativeResults.push({
33
+ path: relative(basePath, entry.path),
34
+ size: entry.size,
35
+ modified: entry.modified?.toISOString(),
36
+ });
37
+ }
38
+ return relativeResults;
39
+ }
40
+ function computeNextCursor(summary, displayResultsCount, cursorOffset) {
41
+ if (summary.truncated && displayResultsCount > 0) {
42
+ return encodeOffsetCursor(cursorOffset + displayResultsCount);
43
+ }
44
+ return undefined;
45
+ }
46
+ function applySummaryFields(structured, summary, nextCursor) {
47
+ if (summary.truncated)
48
+ structured.truncated = true;
49
+ if (summary.skippedInaccessible)
50
+ structured.skippedInaccessible = summary.skippedInaccessible;
51
+ if (summary.stoppedReason)
52
+ structured.stoppedReason = summary.stoppedReason;
53
+ if (nextCursor !== undefined)
54
+ structured.nextCursor = nextCursor;
55
+ }
23
56
  async function handleSearchFiles(args, signal, onProgress) {
24
57
  const basePath = resolvePathOrRoot(args.path);
25
58
  const excludePatterns = args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS;
@@ -31,58 +64,34 @@ async function handleSearchFiles(args, signal, onProgress) {
31
64
  includeHidden: args.includeHidden,
32
65
  sortBy: args.sortBy,
33
66
  respectGitignore: !args.includeIgnored,
34
- ...(args.maxDepth !== undefined ? { maxDepth: args.maxDepth } : {}),
35
- ...(onProgress ? { onProgress } : {}),
36
- ...(signal ? { signal } : {}),
37
67
  };
68
+ if (args.maxDepth !== undefined)
69
+ searchOptions.maxDepth = args.maxDepth;
70
+ if (onProgress)
71
+ searchOptions.onProgress = onProgress;
72
+ if (signal)
73
+ searchOptions.signal = signal;
38
74
  const result = await searchFiles(basePath, args.pattern, excludePatterns, searchOptions);
39
75
  const allResults = result.results;
40
- const displayResults = cursorOffset > 0 ? allResults.slice(cursorOffset) : allResults;
41
- const nextCursor = result.summary.truncated && displayResults.length > 0
42
- ? encodeOffsetCursor(cursorOffset + displayResults.length)
43
- : undefined;
44
- const relativeResults = [];
45
- for (const entry of displayResults) {
46
- relativeResults.push({
47
- path: path.relative(result.basePath, entry.path),
48
- size: entry.size,
49
- modified: entry.modified?.toISOString(),
50
- });
51
- }
76
+ let displayResults = allResults;
77
+ if (cursorOffset > 0)
78
+ displayResults = allResults.slice(cursorOffset);
79
+ const nextCursor = computeNextCursor(result.summary, displayResults.length, cursorOffset);
80
+ const relativeResults = buildRelativeResults(result.basePath, displayResults);
52
81
  const structured = {
53
82
  ok: true,
54
83
  root: basePath,
55
- pattern: args.pattern,
56
84
  results: relativeResults,
57
85
  totalMatches: result.summary.matched,
58
86
  filesScanned: result.summary.filesScanned,
59
- ...(result.summary.truncated
60
- ? { truncated: result.summary.truncated }
61
- : {}),
62
- ...(result.summary.skippedInaccessible
63
- ? { skippedInaccessible: result.summary.skippedInaccessible }
64
- : {}),
65
- ...(result.summary.stoppedReason
66
- ? { stoppedReason: result.summary.stoppedReason }
67
- : {}),
68
- ...(nextCursor !== undefined ? { nextCursor } : {}),
69
87
  };
70
- let truncatedReason;
71
- if (result.summary.truncated) {
72
- if (result.summary.stoppedReason === 'timeout') {
73
- truncatedReason = 'timeout';
74
- }
75
- else if (result.summary.stoppedReason === 'maxFiles') {
76
- truncatedReason = `max files (${result.summary.filesScanned})`;
77
- }
78
- else {
79
- truncatedReason = `max results (${result.summary.matched})`;
80
- }
81
- }
88
+ applySummaryFields(structured, result.summary, nextCursor);
89
+ const truncatedReason = buildTruncatedReason(result.summary);
82
90
  const summaryOptions = {
83
91
  truncated: result.summary.truncated,
84
- ...(truncatedReason ? { truncatedReason } : {}),
85
92
  };
93
+ if (truncatedReason)
94
+ summaryOptions.truncatedReason = truncatedReason;
86
95
  const textLines = [];
87
96
  if (relativeResults.length === 0) {
88
97
  textLines.push('No matches');
@@ -103,10 +112,11 @@ export function registerSearchFilesTool(server, options = {}) {
103
112
  const handler = (args, extra) => executeToolWithDiagnostics({
104
113
  toolName: 'find',
105
114
  extra,
115
+ outputSchema: SearchFilesOutputSchema,
106
116
  timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
107
117
  context: { path: args.path ?? '.' },
108
118
  run: async (signal) => {
109
- const rawScopeLabel = args.path ? path.basename(args.path) : '.';
119
+ const rawScopeLabel = args.path ? basename(args.path) : '.';
110
120
  const scopeLabel = rawScopeLabel || '.';
111
121
  const { pattern } = args;
112
122
  const truncatedPattern = truncateProgressPattern(pattern);
@@ -129,14 +139,13 @@ export function registerSearchFilesTool(server, options = {}) {
129
139
  try {
130
140
  const result = await handleSearchFiles(args, signal, progressWithMessage);
131
141
  const sc = result.structuredContent;
132
- const count = sc.ok ? (sc.totalMatches ?? 0) : 0;
133
- const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
142
+ const { totalMatches = 0, stoppedReason } = sc;
134
143
  let suffix;
135
- if (count === 0) {
144
+ if (totalMatches === 0) {
136
145
  suffix = 'No matches';
137
146
  }
138
147
  else {
139
- suffix = `${count} ${count === 1 ? 'match' : 'matches'}`;
148
+ suffix = `${totalMatches} ${totalMatches === 1 ? 'match' : 'matches'}`;
140
149
  if (stoppedReason === 'timeout') {
141
150
  suffix += ' [timeout]';
142
151
  }
@@ -165,7 +174,7 @@ export function registerSearchFilesTool(server, options = {}) {
165
174
  throw error;
166
175
  }
167
176
  },
168
- onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
177
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
169
178
  });
170
179
  const { isInitialized } = options;
171
180
  const wrappedHandler = wrapToolHandler(handler, {
@@ -38,6 +38,12 @@ export declare function buildResourceLink(params: {
38
38
  description?: string;
39
39
  expiresAt?: string;
40
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
+ };
41
47
  export declare function buildToolResponse<T>(text: string, structuredContent: T, extraContent?: ContentBlock[]): {
42
48
  content: ContentBlock[];
43
49
  structuredContent: T;
@@ -57,6 +63,9 @@ export interface ToolExtra {
57
63
  signal?: AbortSignal;
58
64
  _meta?: {
59
65
  progressToken?: ProgressToken | undefined;
66
+ traceparent?: string | undefined;
67
+ tracestate?: string | undefined;
68
+ baggage?: string | undefined;
60
69
  };
61
70
  sendNotification?: (notification: {
62
71
  method: 'notifications/progress';
@@ -94,6 +103,7 @@ export declare function buildFileInfoPayload(info: FileInfo): FileInfoPayload;
94
103
  interface ToolExecutionOptions<T> {
95
104
  toolName: string;
96
105
  extra: ToolExtra;
106
+ outputSchema?: z.ZodType<T>;
97
107
  run: (signal: AbortSignal | undefined) => ToolResponse<T> | Promise<ToolResponse<T>>;
98
108
  onError: (error: unknown) => ToolResult<T>;
99
109
  context?: Record<string, unknown>;