@j0hanz/filesystem-mcp 1.14.0 → 1.15.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -71
  3. package/dist/cli.js +2 -2
  4. package/dist/completions.js +54 -51
  5. package/dist/config.d.ts +13 -13
  6. package/dist/config.js +12 -12
  7. package/dist/index.js +1 -1
  8. package/dist/lib/abort.d.ts +7 -0
  9. package/dist/lib/abort.js +81 -0
  10. package/dist/lib/constants.d.ts +3 -1
  11. package/dist/lib/constants.js +8 -2
  12. package/dist/lib/errors.d.ts +7 -3
  13. package/dist/lib/errors.js +59 -39
  14. package/dist/lib/file-operations/core.d.ts +3 -3
  15. package/dist/lib/file-operations/core.js +23 -20
  16. package/dist/lib/file-operations/metadata.d.ts +2 -2
  17. package/dist/lib/file-operations/metadata.js +60 -19
  18. package/dist/lib/file-operations/search.js +83 -84
  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 +20 -98
  22. package/dist/lib/globs.js +1 -1
  23. package/dist/lib/logger.d.ts +28 -0
  24. package/dist/lib/logger.js +91 -0
  25. package/dist/lib/observability.d.ts +7 -0
  26. package/dist/lib/observability.js +19 -9
  27. package/dist/lib/paths.js +55 -55
  28. package/dist/lib/resource-store.js +4 -4
  29. package/dist/lib/utils.d.ts +0 -12
  30. package/dist/lib/utils.js +0 -13
  31. package/dist/resources/generated-instructions.js +40 -31
  32. package/dist/resources/tool-catalog.js +32 -26
  33. package/dist/resources/tool-info.js +34 -29
  34. package/dist/resources/workflows.js +39 -18
  35. package/dist/resources.d.ts +1 -1
  36. package/dist/resources.js +4 -4
  37. package/dist/schemas.d.ts +66 -66
  38. package/dist/schemas.js +21 -44
  39. package/dist/server/bootstrap.d.ts +12 -11
  40. package/dist/server/bootstrap.js +95 -86
  41. package/dist/server/roots-manager.d.ts +5 -2
  42. package/dist/server/roots-manager.js +8 -6
  43. package/dist/server/task-store.d.ts +10 -0
  44. package/dist/server/task-store.js +73 -0
  45. package/dist/tools/apply-patch.js +26 -18
  46. package/dist/tools/calculate-hash.js +11 -22
  47. package/dist/tools/create-directory.js +10 -8
  48. package/dist/tools/delete-file.js +17 -15
  49. package/dist/tools/diff-files.js +15 -15
  50. package/dist/tools/edit-file.js +7 -4
  51. package/dist/tools/list-directory.js +6 -6
  52. package/dist/tools/move-file.js +98 -81
  53. package/dist/tools/read-multiple.js +4 -4
  54. package/dist/tools/read.js +5 -5
  55. package/dist/tools/replace-in-files.js +20 -23
  56. package/dist/tools/roots.js +1 -1
  57. package/dist/tools/search-content.js +32 -41
  58. package/dist/tools/search-files.js +54 -41
  59. package/dist/tools/shared.d.ts +3 -0
  60. package/dist/tools/shared.js +70 -28
  61. package/dist/tools/stat-many.js +11 -7
  62. package/dist/tools/stat.js +4 -4
  63. package/dist/tools/task-support.d.ts +10 -9
  64. package/dist/tools/task-support.js +94 -23
  65. package/dist/tools/tree.js +3 -3
  66. package/dist/tools/write-file.js +10 -7
  67. package/package.json +9 -9
@@ -1,4 +1,4 @@
1
- import * as path from 'node:path';
1
+ import { basename } 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 { readMultipleFiles } from '../lib/file-operations/metadata.js';
@@ -19,7 +19,7 @@ export const READ_MANY_TOOL = {
19
19
  taskSupport: 'optional',
20
20
  };
21
21
  function buildReadManyResourceName(filePath) {
22
- return `read:${path.basename(filePath)}`;
22
+ return `read:${basename(filePath)}`;
23
23
  }
24
24
  function toStructuredReadManyResult(result) {
25
25
  const structuredResult = {
@@ -72,7 +72,7 @@ function maybeExternalizeReadManyResult(result, resourceStore) {
72
72
  const baseResult = {
73
73
  ...rest,
74
74
  ...(error
75
- ? { error: buildStructuredError(error, ErrorCode.E_UNKNOWN, result.path) }
75
+ ? { error: buildStructuredError(error, ErrorCode.UNKNOWN, result.path) }
76
76
  : {}),
77
77
  ...(truncationReason ? { truncationReason } : {}),
78
78
  };
@@ -191,7 +191,7 @@ export function registerReadMultipleFilesTool(server, options = {}) {
191
191
  throw error;
192
192
  }
193
193
  },
194
- onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, primaryPath),
194
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.NOT_FILE, primaryPath),
195
195
  });
196
196
  };
197
197
  const wrappedHandler = wrapToolHandler(handler, {
@@ -1,5 +1,5 @@
1
- import * as path from 'node:path';
2
1
  import { createHash } from 'node:crypto';
2
+ import { basename } from 'node:path';
3
3
  import { DEFAULT_SEARCH_TIMEOUT_MS, MAX_TEXT_FILE_SIZE, } from '../lib/constants.js';
4
4
  import { ErrorCode } from '../lib/errors.js';
5
5
  import { readFile } from '../lib/fs-helpers.js';
@@ -24,7 +24,7 @@ const READ_TOOL_NAME = 'read';
24
24
  const READ_TOOL_LABEL = '🕮 read';
25
25
  const FULL_FILE_CONTENTS_DESCRIPTION = 'Full file contents';
26
26
  function buildReadResourceName(filePath) {
27
- return `read:${path.basename(filePath)}`;
27
+ return `read:${basename(filePath)}`;
28
28
  }
29
29
  function buildReadOptions(args, signal) {
30
30
  const options = {
@@ -99,7 +99,7 @@ function maybeBuildExternalizedReadResponse(filePath, content, structured, resou
99
99
  ]);
100
100
  }
101
101
  function buildReadProgressMessage(args) {
102
- const name = path.basename(args.path);
102
+ const name = basename(args.path);
103
103
  if (args.startLine !== undefined) {
104
104
  const end = args.endLine ?? '…';
105
105
  return `${READ_TOOL_LABEL}: ${name} [lines ${args.startLine}–${end}]`;
@@ -111,7 +111,7 @@ function buildReadProgressMessage(args) {
111
111
  return `${READ_TOOL_LABEL}: ${name}`;
112
112
  }
113
113
  function buildReadCompletionMessage(args, result) {
114
- const name = path.basename(args.path);
114
+ const name = basename(args.path);
115
115
  if (result.isError)
116
116
  return `${READ_TOOL_LABEL}: ${name} • failed`;
117
117
  const structured = result.structuredContent;
@@ -160,7 +160,7 @@ export function registerReadFileTool(server, options = {}) {
160
160
  timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
161
161
  context: { path: args.path },
162
162
  run: (signal) => handleReadFile(args, signal, options.resourceStore),
163
- onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, args.path),
163
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.NOT_FILE, args.path),
164
164
  });
165
165
  const wrappedHandler = wrapToolHandler(handler, {
166
166
  guard: options.isInitialized,
@@ -1,15 +1,14 @@
1
- import * as fs from 'node:fs/promises';
2
- import * as path from 'node:path';
3
1
  import { Buffer } from 'node:buffer';
2
+ import { open } from 'node:fs/promises';
3
+ import { basename, relative } from 'node:path';
4
4
  import { createTwoFilesPatch } from 'diff';
5
5
  import RE2 from 're2';
6
- import safeRegex from 'safe-regex2';
7
6
  import { DEFAULT_EXCLUDE_PATTERNS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../lib/constants.js';
8
7
  import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
9
8
  import { globEntries } from '../lib/file-operations/traversal.js';
10
9
  import { atomicWriteFile } from '../lib/fs-helpers.js';
10
+ import { Logger } from '../lib/logger.js';
11
11
  import { validateExistingPath, validatePathForWrite } from '../lib/paths.js';
12
- import { reportPeriodicProgress } from '../lib/utils.js';
13
12
  import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
14
13
  import { buildStructuredError, buildToolErrorResponse, buildToolResponse, createToolProgressSession, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, resolveFinalProgressCurrent, resolvePathOrRoot, truncateProgressPattern, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
15
14
  import { registerToolTaskIfAvailable } from './task-support.js';
@@ -36,7 +35,7 @@ function recordFailure(failures, failure) {
36
35
  failures.push(failure);
37
36
  }
38
37
  function recordChangedFile(summary, filePath, matchCount) {
39
- const relativePath = path.relative(summary.root, filePath);
38
+ const relativePath = relative(summary.root, filePath);
40
39
  if (summary.changedFiles.length < MAX_CHANGED_FILES) {
41
40
  summary.changedFiles.push({ path: relativePath, matches: matchCount });
42
41
  return;
@@ -49,11 +48,14 @@ function createRegexMatcher(pattern, caseSensitive) {
49
48
  return new RE2(pattern, flags);
50
49
  }
51
50
  catch (error) {
52
- throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
51
+ throw new McpError(ErrorCode.INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)} (RE2: no lookahead/lookbehind/backrefs)`);
53
52
  }
54
53
  }
55
54
  function createRegexReplacementMatcher(regex) {
56
55
  return {
56
+ testBuffer(buffer) {
57
+ return regex.test(buffer);
58
+ },
57
59
  count(content) {
58
60
  regex.lastIndex = 0;
59
61
  let matchCount = 0;
@@ -120,7 +122,7 @@ async function processEntry(entryPath, ctx) {
120
122
  summary.failedFiles++;
121
123
  recordFailure(summary.failures, {
122
124
  path: entryPath,
123
- error: buildStructuredError(error, ErrorCode.E_UNKNOWN, entryPath),
125
+ error: buildStructuredError(error, ErrorCode.UNKNOWN, entryPath),
124
126
  });
125
127
  return;
126
128
  }
@@ -149,7 +151,7 @@ async function processEntry(entryPath, ctx) {
149
151
  summary.failedFiles++;
150
152
  recordFailure(summary.failures, {
151
153
  path: validPath,
152
- error: buildStructuredError(error, ErrorCode.E_UNKNOWN, validPath),
154
+ error: buildStructuredError(error, ErrorCode.UNKNOWN, validPath),
153
155
  });
154
156
  }
155
157
  }
@@ -157,7 +159,7 @@ async function readReplacementPlan(validPath, ctx) {
157
159
  const { matcher, replacement, maxFileSize, signal } = ctx;
158
160
  let fileHandle;
159
161
  try {
160
- const fd = await fs.open(validPath, 'r');
162
+ const fd = await open(validPath, 'r');
161
163
  fileHandle = fd;
162
164
  const stats = await fileHandle.stat();
163
165
  if (stats.size > maxFileSize) {
@@ -195,7 +197,7 @@ async function maybeAppendPatchDiff(summary, params) {
195
197
  const patch = await new Promise((resolve) => {
196
198
  // Defer to event loop to avoid blocking on large diffs
197
199
  setImmediate(() => {
198
- createTwoFilesPatch(path.basename(params.filePath), path.basename(params.filePath), params.originalContent, params.updatedContent, 'Original', 'Modified', {
200
+ createTwoFilesPatch(basename(params.filePath), basename(params.filePath), params.originalContent, params.updatedContent, 'Original', 'Modified', {
199
201
  callback: (res) => {
200
202
  resolve(res ?? '');
201
203
  },
@@ -262,9 +264,6 @@ async function resolveSearchRoot(pathValue, signal) {
262
264
  function createReplacementRegex(args) {
263
265
  if (!args.isRegex)
264
266
  return undefined;
265
- if (!safeRegex(args.searchPattern)) {
266
- throw new McpError(ErrorCode.E_INVALID_INPUT, `Unsafe regex pattern: ${args.searchPattern}`);
267
- }
268
267
  return createRegexMatcher(args.searchPattern, args.caseSensitive);
269
268
  }
270
269
  function createReplacementMatcher(args) {
@@ -287,7 +286,7 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
287
286
  cwd: root,
288
287
  pattern: args.filePattern,
289
288
  excludePatterns: args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS,
290
- includeHidden: args.includeHidden ?? false,
289
+ includeHidden: args.includeHidden,
291
290
  baseNameMatch: false,
292
291
  caseSensitiveMatch: true, // Default to sensitive for file paths
293
292
  followSymbolicLinks: false,
@@ -300,7 +299,7 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
300
299
  const context = {
301
300
  options: {
302
301
  dryRun: args.dryRun,
303
- returnDiff: args.returnDiff ?? false,
302
+ returnDiff: args.returnDiff,
304
303
  },
305
304
  replacement: args.replacement,
306
305
  matcher,
@@ -314,9 +313,7 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
314
313
  ...(args.maxFiles !== undefined ? { maxEntries: args.maxFiles } : {}),
315
314
  onEntry: () => {
316
315
  summary.processedFiles++;
317
- reportPeriodicProgress(onProgress, summary.processedFiles, {
318
- throttleModulo: 25,
319
- });
316
+ onProgress({ current: summary.processedFiles });
320
317
  },
321
318
  runEntry: (entryPath) => processEntry(entryPath, context),
322
319
  });
@@ -324,10 +321,10 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
324
321
  if (stoppedByLimit) {
325
322
  summary.stoppedReason = 'maxFiles';
326
323
  }
327
- reportPeriodicProgress(onProgress, summary.processedFiles, {
328
- throttleModulo: 25,
329
- force: true,
330
- });
324
+ onProgress({ current: summary.processedFiles });
325
+ if (!args.dryRun && summary.totalMatches > 0) {
326
+ Logger.info(`search_and_replace: ${summary.filesChanged} file(s), ${summary.totalMatches} match(es)`);
327
+ }
331
328
  return buildToolResponse(buildSearchAndReplaceText(summary, args.dryRun), buildSearchAndReplaceStructuredResult(summary, args));
332
329
  }
333
330
  export function registerSearchAndReplaceTool(server, options = {}) {
@@ -366,7 +363,7 @@ export function registerSearchAndReplaceTool(server, options = {}) {
366
363
  throw error;
367
364
  }
368
365
  },
369
- onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
366
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
370
367
  });
371
368
  const { isInitialized } = options;
372
369
  const wrappedHandler = wrapToolHandler(handler, {
@@ -36,7 +36,7 @@ export function registerListAllowedDirectoriesTool(server, options = {}) {
36
36
  extra,
37
37
  outputSchema: ListAllowedDirectoriesOutputSchema,
38
38
  run: () => handleListAllowedDirectories(),
39
- onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN),
39
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN),
40
40
  });
41
41
  const wrappedHandler = wrapToolHandler(handler, {
42
42
  guard: options.isInitialized,
@@ -1,8 +1,8 @@
1
- import * as path from 'node:path';
1
+ import { relative } from 'node:path';
2
2
  import RE2 from 're2';
3
3
  import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
4
4
  import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
5
- import { searchContent } from '../lib/file-operations/search.js';
5
+ import { searchContent, } from '../lib/file-operations/search.js';
6
6
  import { formatOperationSummary } from '../config.js';
7
7
  import { SearchContentInputSchema, SearchContentOutputSchema, } from '../schemas.js';
8
8
  import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, resolvePathOrRoot, truncateProgressPattern, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
@@ -50,25 +50,15 @@ function buildCompletionSuffix(count, filesMatched, scope, stoppedReason) {
50
50
  : '';
51
51
  return `${count} ${matchWord} in ${filesMatched} ${fileWord}${reasonSuffix}`;
52
52
  }
53
- function compareNormalizedMatches(left, right) {
54
- const fileCompare = left.relativeFile.localeCompare(right.relativeFile);
55
- if (fileCompare !== 0)
56
- return fileCompare;
57
- if (left.line !== right.line)
58
- return left.line - right.line;
59
- return left.index - right.index;
60
- }
61
- function buildSearchPreviewState(matches, payloads) {
62
- const needsExternalize = matches.length > CONFIG.MAX_INLINE_MATCHES;
53
+ function buildSearchPreviewState(payloads) {
54
+ const needsExternalize = payloads.length > CONFIG.MAX_INLINE_MATCHES;
63
55
  const visibleCount = needsExternalize
64
56
  ? CONFIG.MAX_INLINE_MATCHES
65
- : matches.length;
66
- const visibleMatches = matches.slice(0, visibleCount);
57
+ : payloads.length;
67
58
  return {
68
59
  needsExternalize,
69
- visibleMatches,
70
60
  visiblePayloads: payloads.slice(0, visibleCount),
71
- heading: buildHeading(matches.length, visibleMatches.length),
61
+ heading: buildHeading(payloads.length, visibleCount),
72
62
  };
73
63
  }
74
64
  export const SEARCH_CONTENT_TOOL = {
@@ -117,29 +107,21 @@ function buildSearchStructured(summary, matches) {
117
107
  ...buildStructuredSummaryFields(summary),
118
108
  };
119
109
  }
120
- function normalizeMatches(result) {
110
+ function buildSortedPayloads(result, context) {
121
111
  const relativeByFile = new Map();
122
112
  const getRelativeFile = (file) => {
123
113
  const cached = relativeByFile.get(file);
124
114
  if (cached !== undefined)
125
115
  return cached;
126
- const relative = path.relative(result.basePath, file);
127
- relativeByFile.set(file, relative);
128
- return relative;
116
+ const rel = relative(result.basePath, file);
117
+ relativeByFile.set(file, rel);
118
+ return rel;
129
119
  };
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) => {
120
+ const payloads = result.matches.map((match, originalIndex) => {
121
+ const relFile = getRelativeFile(match.file);
140
122
  const column = findColumnOffset(match.content, context);
141
- return {
142
- file: match.relativeFile,
123
+ const payload = {
124
+ file: relFile,
143
125
  line: match.line,
144
126
  ...(column !== undefined ? { column } : {}),
145
127
  content: match.content,
@@ -149,14 +131,24 @@ function buildMatchPayloads(matches, context) {
149
131
  : {}),
150
132
  ...(match.contextAfter ? { contextAfter: [...match.contextAfter] } : {}),
151
133
  };
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;
152
143
  });
144
+ return payloads.map((p) => p.payload);
153
145
  }
154
146
  function buildMatchList(heading, matches) {
155
147
  if (matches.length === 0)
156
148
  return heading;
157
149
  const parts = [heading];
158
150
  for (const match of matches) {
159
- parts.push(`\n ${match.relativeFile}:${String(match.line).padStart(4)}: ${match.content}`);
151
+ parts.push(`\n ${match.file}:${String(match.line).padStart(4)}: ${match.content}`);
160
152
  }
161
153
  return parts.join('');
162
154
  }
@@ -206,7 +198,7 @@ async function executeSearch(args, basePath, signal, onProgress) {
206
198
  }
207
199
  catch (error) {
208
200
  if (error instanceof Error && /regular expression/i.test(error.message)) {
209
- throw new McpError(ErrorCode.E_INVALID_PATTERN, error.message);
201
+ throw new McpError(ErrorCode.INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)} (RE2: no lookahead/lookbehind/backrefs)`);
210
202
  }
211
203
  throw error;
212
204
  }
@@ -219,7 +211,7 @@ function createSearchMatcher(args) {
219
211
  return new RE2(args.pattern, flags);
220
212
  }
221
213
  catch (error) {
222
- throw new McpError(ErrorCode.E_INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
214
+ throw new McpError(ErrorCode.INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)} (RE2: no lookahead/lookbehind/backrefs)`);
223
215
  }
224
216
  }
225
217
  function createSearchContext(args, matcher) {
@@ -236,11 +228,10 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
236
228
  const basePath = resolvePathOrRoot(args.path);
237
229
  const regexMatcher = createSearchMatcher(args);
238
230
  const result = await executeSearch(args, basePath, signal, onProgress);
239
- const normalizedMatches = normalizeMatches(result);
240
231
  const searchContext = createSearchContext(args, regexMatcher);
241
- const matchPayloads = buildMatchPayloads(normalizedMatches, searchContext);
232
+ const matchPayloads = buildSortedPayloads(result, searchContext);
242
233
  const fullStructured = buildSearchStructured(result.summary, matchPayloads);
243
- const preview = buildSearchPreviewState(normalizedMatches, matchPayloads);
234
+ const preview = buildSearchPreviewState(matchPayloads);
244
235
  if (resourceStore && preview.needsExternalize) {
245
236
  const previewStructured = {
246
237
  ...fullStructured,
@@ -253,7 +244,7 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
253
244
  text: JSON.stringify(fullStructured),
254
245
  });
255
246
  previewStructured.resourceUri = entry.uri;
256
- const text = buildSearchText(preview.heading, preview.visibleMatches);
247
+ const text = buildSearchText(preview.heading, preview.visiblePayloads);
257
248
  return buildToolResponse(text, previewStructured, [
258
249
  buildResourceLink({
259
250
  uri: entry.uri,
@@ -264,7 +255,7 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
264
255
  }),
265
256
  ]);
266
257
  }
267
- const text = buildSearchText(preview.heading, preview.visibleMatches, result.summary);
258
+ const text = buildSearchText(preview.heading, preview.visiblePayloads, result.summary);
268
259
  return buildToolResponse(text, fullStructured);
269
260
  }
270
261
  export function registerSearchContentTool(server, options = {}) {
@@ -298,7 +289,7 @@ export function registerSearchContentTool(server, options = {}) {
298
289
  throw error;
299
290
  }
300
291
  },
301
- onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path ?? '.'),
292
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path ?? '.'),
302
293
  });
303
294
  const { isInitialized } = options;
304
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';
@@ -17,6 +17,42 @@ export const SEARCH_FILES_TOOL = {
17
17
  nuances: ['Respects `.gitignore` unless `includeIgnored=true`.'],
18
18
  taskSupport: 'optional',
19
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
+ }
20
56
  async function handleSearchFiles(args, signal, onProgress) {
21
57
  const basePath = resolvePathOrRoot(args.path);
22
58
  const excludePatterns = args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS;
@@ -28,57 +64,34 @@ async function handleSearchFiles(args, signal, onProgress) {
28
64
  includeHidden: args.includeHidden,
29
65
  sortBy: args.sortBy,
30
66
  respectGitignore: !args.includeIgnored,
31
- ...(args.maxDepth !== undefined ? { maxDepth: args.maxDepth } : {}),
32
- ...(onProgress ? { onProgress } : {}),
33
- ...(signal ? { signal } : {}),
34
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;
35
74
  const result = await searchFiles(basePath, args.pattern, excludePatterns, searchOptions);
36
75
  const allResults = result.results;
37
- const displayResults = cursorOffset > 0 ? allResults.slice(cursorOffset) : allResults;
38
- const nextCursor = result.summary.truncated && displayResults.length > 0
39
- ? encodeOffsetCursor(cursorOffset + displayResults.length)
40
- : undefined;
41
- const relativeResults = [];
42
- for (const entry of displayResults) {
43
- relativeResults.push({
44
- path: path.relative(result.basePath, entry.path),
45
- size: entry.size,
46
- modified: entry.modified?.toISOString(),
47
- });
48
- }
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);
49
81
  const structured = {
50
82
  ok: true,
51
83
  root: basePath,
52
84
  results: relativeResults,
53
85
  totalMatches: result.summary.matched,
54
86
  filesScanned: result.summary.filesScanned,
55
- ...(result.summary.truncated
56
- ? { truncated: result.summary.truncated }
57
- : {}),
58
- ...(result.summary.skippedInaccessible
59
- ? { skippedInaccessible: result.summary.skippedInaccessible }
60
- : {}),
61
- ...(result.summary.stoppedReason
62
- ? { stoppedReason: result.summary.stoppedReason }
63
- : {}),
64
- ...(nextCursor !== undefined ? { nextCursor } : {}),
65
87
  };
66
- let truncatedReason;
67
- if (result.summary.truncated) {
68
- if (result.summary.stoppedReason === 'timeout') {
69
- truncatedReason = 'timeout';
70
- }
71
- else if (result.summary.stoppedReason === 'maxFiles') {
72
- truncatedReason = `max files (${result.summary.filesScanned})`;
73
- }
74
- else {
75
- truncatedReason = `max results (${result.summary.matched})`;
76
- }
77
- }
88
+ applySummaryFields(structured, result.summary, nextCursor);
89
+ const truncatedReason = buildTruncatedReason(result.summary);
78
90
  const summaryOptions = {
79
91
  truncated: result.summary.truncated,
80
- ...(truncatedReason ? { truncatedReason } : {}),
81
92
  };
93
+ if (truncatedReason)
94
+ summaryOptions.truncatedReason = truncatedReason;
82
95
  const textLines = [];
83
96
  if (relativeResults.length === 0) {
84
97
  textLines.push('No matches');
@@ -103,7 +116,7 @@ export function registerSearchFilesTool(server, options = {}) {
103
116
  timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
104
117
  context: { path: args.path ?? '.' },
105
118
  run: async (signal) => {
106
- const rawScopeLabel = args.path ? path.basename(args.path) : '.';
119
+ const rawScopeLabel = args.path ? basename(args.path) : '.';
107
120
  const scopeLabel = rawScopeLabel || '.';
108
121
  const { pattern } = args;
109
122
  const truncatedPattern = truncateProgressPattern(pattern);
@@ -161,7 +174,7 @@ export function registerSearchFilesTool(server, options = {}) {
161
174
  throw error;
162
175
  }
163
176
  },
164
- onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
177
+ onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
165
178
  });
166
179
  const { isInitialized } = options;
167
180
  const wrappedHandler = wrapToolHandler(handler, {
@@ -63,6 +63,9 @@ export interface ToolExtra {
63
63
  signal?: AbortSignal;
64
64
  _meta?: {
65
65
  progressToken?: ProgressToken | undefined;
66
+ traceparent?: string | undefined;
67
+ tracestate?: string | undefined;
68
+ baggage?: string | undefined;
66
69
  };
67
70
  sendNotification?: (notification: {
68
71
  method: 'notifications/progress';