@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.
- package/README.md +162 -145
- package/dist/cli.js +2 -2
- package/dist/completions.js +54 -51
- package/dist/config.d.ts +13 -14
- 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 +64 -41
- 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 +69 -22
- package/dist/lib/file-operations/search.d.ts +0 -1
- package/dist/lib/file-operations/search.js +87 -95
- package/dist/lib/file-operations/traversal.js +13 -15
- package/dist/lib/fs-helpers.d.ts +3 -10
- package/dist/lib/fs-helpers.js +29 -108
- package/dist/lib/globs.d.ts +2 -0
- package/dist/lib/globs.js +19 -0
- 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/lib/zod-codecs.d.ts +2 -0
- package/dist/lib/zod-codecs.js +18 -0
- package/dist/pkg-info.d.ts +1 -0
- package/dist/pkg-info.js +2 -2
- package/dist/prompts.js +3 -3
- package/dist/resources/generated-instructions.js +41 -41
- package/dist/resources/tool-catalog.js +33 -58
- package/dist/resources/tool-info.d.ts +0 -1
- package/dist/resources/tool-info.js +44 -67
- package/dist/resources/workflows.js +47 -19
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +4 -4
- package/dist/schemas.d.ts +185 -465
- package/dist/schemas.js +174 -206
- 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 +9 -7
- package/dist/server/task-store.d.ts +10 -0
- package/dist/server/task-store.js +73 -0
- package/dist/tools/apply-patch.js +39 -20
- package/dist/tools/calculate-hash.js +14 -27
- package/dist/tools/create-directory.js +11 -9
- package/dist/tools/delete-file.js +19 -19
- package/dist/tools/diff-files.js +16 -18
- package/dist/tools/edit-file.js +11 -5
- package/dist/tools/list-directory.js +16 -21
- package/dist/tools/move-file.js +105 -100
- package/dist/tools/read-multiple.js +15 -10
- package/dist/tools/read.js +6 -7
- package/dist/tools/replace-in-files.js +76 -115
- package/dist/tools/roots.js +3 -7
- package/dist/tools/search-content.js +158 -203
- package/dist/tools/search-files.js +59 -50
- package/dist/tools/shared.d.ts +10 -0
- package/dist/tools/shared.js +105 -36
- package/dist/tools/stat-many.js +15 -9
- package/dist/tools/stat.js +6 -6
- package/dist/tools/task-support.d.ts +10 -9
- package/dist/tools/task-support.js +94 -23
- package/dist/tools/tree.js +4 -4
- package/dist/tools/write-file.js +11 -12
- package/package.json +10 -9
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import
|
|
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
|
|
57
|
-
|
|
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
|
-
:
|
|
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:
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
const
|
|
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 =
|
|
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 =
|
|
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
|
|
325
|
-
const
|
|
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.
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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 ?
|
|
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
|
|
133
|
-
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
142
|
+
const { totalMatches = 0, stoppedReason } = sc;
|
|
134
143
|
let suffix;
|
|
135
|
-
if (
|
|
144
|
+
if (totalMatches === 0) {
|
|
136
145
|
suffix = 'No matches';
|
|
137
146
|
}
|
|
138
147
|
else {
|
|
139
|
-
suffix = `${
|
|
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.
|
|
177
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
|
|
169
178
|
});
|
|
170
179
|
const { isInitialized } = options;
|
|
171
180
|
const wrappedHandler = wrapToolHandler(handler, {
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -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>;
|