@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.
- package/LICENSE +21 -0
- package/README.md +143 -71
- package/dist/cli.js +2 -2
- package/dist/completions.js +54 -51
- package/dist/config.d.ts +13 -13
- package/dist/config.js +12 -12
- package/dist/index.js +1 -1
- package/dist/lib/abort.d.ts +7 -0
- package/dist/lib/abort.js +81 -0
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.js +8 -2
- package/dist/lib/errors.d.ts +7 -3
- package/dist/lib/errors.js +59 -39
- package/dist/lib/file-operations/core.d.ts +3 -3
- package/dist/lib/file-operations/core.js +23 -20
- package/dist/lib/file-operations/metadata.d.ts +2 -2
- package/dist/lib/file-operations/metadata.js +60 -19
- package/dist/lib/file-operations/search.js +83 -84
- package/dist/lib/file-operations/traversal.js +13 -15
- package/dist/lib/fs-helpers.d.ts +3 -10
- package/dist/lib/fs-helpers.js +20 -98
- package/dist/lib/globs.js +1 -1
- package/dist/lib/logger.d.ts +28 -0
- package/dist/lib/logger.js +91 -0
- package/dist/lib/observability.d.ts +7 -0
- package/dist/lib/observability.js +19 -9
- package/dist/lib/paths.js +55 -55
- package/dist/lib/resource-store.js +4 -4
- package/dist/lib/utils.d.ts +0 -12
- package/dist/lib/utils.js +0 -13
- package/dist/resources/generated-instructions.js +40 -31
- package/dist/resources/tool-catalog.js +32 -26
- package/dist/resources/tool-info.js +34 -29
- package/dist/resources/workflows.js +39 -18
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +4 -4
- package/dist/schemas.d.ts +66 -66
- package/dist/schemas.js +21 -44
- package/dist/server/bootstrap.d.ts +12 -11
- package/dist/server/bootstrap.js +95 -86
- package/dist/server/roots-manager.d.ts +5 -2
- package/dist/server/roots-manager.js +8 -6
- package/dist/server/task-store.d.ts +10 -0
- package/dist/server/task-store.js +73 -0
- package/dist/tools/apply-patch.js +26 -18
- package/dist/tools/calculate-hash.js +11 -22
- package/dist/tools/create-directory.js +10 -8
- package/dist/tools/delete-file.js +17 -15
- package/dist/tools/diff-files.js +15 -15
- package/dist/tools/edit-file.js +7 -4
- package/dist/tools/list-directory.js +6 -6
- package/dist/tools/move-file.js +98 -81
- package/dist/tools/read-multiple.js +4 -4
- package/dist/tools/read.js +5 -5
- package/dist/tools/replace-in-files.js +20 -23
- package/dist/tools/roots.js +1 -1
- package/dist/tools/search-content.js +32 -41
- package/dist/tools/search-files.js +54 -41
- package/dist/tools/shared.d.ts +3 -0
- package/dist/tools/shared.js +70 -28
- package/dist/tools/stat-many.js +11 -7
- package/dist/tools/stat.js +4 -4
- package/dist/tools/task-support.d.ts +10 -9
- package/dist/tools/task-support.js +94 -23
- package/dist/tools/tree.js +3 -3
- package/dist/tools/write-file.js +10 -7
- package/package.json +9 -9
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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:${
|
|
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.
|
|
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.
|
|
194
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.NOT_FILE, primaryPath),
|
|
195
195
|
});
|
|
196
196
|
};
|
|
197
197
|
const wrappedHandler = wrapToolHandler(handler, {
|
package/dist/tools/read.js
CHANGED
|
@@ -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:${
|
|
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 =
|
|
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 =
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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.
|
|
366
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
|
|
370
367
|
});
|
|
371
368
|
const { isInitialized } = options;
|
|
372
369
|
const wrappedHandler = wrapToolHandler(handler, {
|
package/dist/tools/roots.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
54
|
-
const
|
|
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
|
-
:
|
|
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(
|
|
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
|
|
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
|
|
127
|
-
relativeByFile.set(file,
|
|
128
|
-
return
|
|
116
|
+
const rel = relative(result.basePath, file);
|
|
117
|
+
relativeByFile.set(file, rel);
|
|
118
|
+
return rel;
|
|
129
119
|
};
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
142
|
-
file:
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
232
|
+
const matchPayloads = buildSortedPayloads(result, searchContext);
|
|
242
233
|
const fullStructured = buildSearchStructured(result.summary, matchPayloads);
|
|
243
|
-
const preview = buildSearchPreviewState(
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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 ?
|
|
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.
|
|
177
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
|
|
165
178
|
});
|
|
166
179
|
const { isInitialized } = options;
|
|
167
180
|
const wrappedHandler = wrapToolHandler(handler, {
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -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';
|