@j0hanz/filesystem-mcp 1.11.1 → 1.13.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/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/file-operations/metadata.js +71 -142
- package/dist/lib/file-operations/search.js +119 -19
- package/dist/lib/file-operations/traversal.js +45 -102
- package/dist/lib/fs-helpers.js +175 -158
- package/dist/lib/utils.d.ts +4 -0
- package/dist/lib/utils.js +26 -0
- package/dist/resources/tool-info.js +23 -19
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +8 -4
- package/dist/server/bootstrap.js +39 -4
- package/dist/server/roots-manager.d.ts +1 -1
- package/dist/server/roots-manager.js +8 -13
- package/dist/tools/apply-patch.js +34 -38
- package/dist/tools/calculate-hash.js +21 -24
- package/dist/tools/diff-files.js +26 -18
- package/dist/tools/edit-file.d.ts +4 -1
- package/dist/tools/edit-file.js +183 -115
- package/dist/tools/list-directory.js +105 -10
- package/dist/tools/read-multiple.d.ts +1 -1
- package/dist/tools/read-multiple.js +101 -65
- package/dist/tools/read.js +103 -75
- package/dist/tools/replace-in-files.js +85 -38
- package/dist/tools/search-content.js +266 -217
- package/dist/tools/shared.js +35 -4
- package/dist/tools/task-support.d.ts +7 -0
- package/dist/tools/task-support.js +137 -100
- package/dist/tools.js +2 -2
- package/package.json +1 -1
|
@@ -3,16 +3,70 @@ 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
5
|
import { searchContent } from '../lib/file-operations/search.js';
|
|
6
|
-
import { formatOperationSummary
|
|
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, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
9
9
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Configuration constants for the Search Content tool.
|
|
12
|
+
*/
|
|
13
|
+
const CONFIG = {
|
|
14
|
+
MAX_INLINE_MATCHES: parseInt(process.env['FS_CONTEXT_MAX_INLINE_MATCHES'] ?? '', 10) || 50,
|
|
15
|
+
COMPLETION_LABELS: {
|
|
16
|
+
timeout: 'timeout',
|
|
17
|
+
maxResults: 'max results',
|
|
18
|
+
maxFiles: 'max files',
|
|
19
|
+
},
|
|
15
20
|
};
|
|
21
|
+
const TRUTHY_SUMMARY_FIELDS = [
|
|
22
|
+
'filesMatched',
|
|
23
|
+
'skippedTooLarge',
|
|
24
|
+
'skippedBinary',
|
|
25
|
+
'skippedInaccessible',
|
|
26
|
+
'linesSkippedDueToRegexTimeout',
|
|
27
|
+
];
|
|
28
|
+
function buildStructuredSummaryFields(summary) {
|
|
29
|
+
const truthyFields = Object.fromEntries(TRUTHY_SUMMARY_FIELDS.flatMap((key) => {
|
|
30
|
+
const value = summary[key];
|
|
31
|
+
return value ? [[key, value]] : [];
|
|
32
|
+
}));
|
|
33
|
+
return {
|
|
34
|
+
...truthyFields,
|
|
35
|
+
...(summary.truncated ? { truncated: true } : {}),
|
|
36
|
+
...(summary.stoppedReason ? { stoppedReason: summary.stoppedReason } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function buildCompletionSuffix(count, filesMatched, scope, stoppedReason) {
|
|
40
|
+
if (count === 0)
|
|
41
|
+
return `No matches in ${scope}`;
|
|
42
|
+
const matchWord = count === 1 ? 'match' : 'matches';
|
|
43
|
+
const fileWord = filesMatched === 1 ? 'file' : 'files';
|
|
44
|
+
const reasonSuffix = stoppedReason !== undefined
|
|
45
|
+
? ` [${CONFIG.COMPLETION_LABELS[stoppedReason]}]`
|
|
46
|
+
: '';
|
|
47
|
+
return `${count} ${matchWord} in ${filesMatched} ${fileWord}${reasonSuffix}`;
|
|
48
|
+
}
|
|
49
|
+
function compareNormalizedMatches(left, right) {
|
|
50
|
+
const fileCompare = left.relativeFile.localeCompare(right.relativeFile);
|
|
51
|
+
if (fileCompare !== 0)
|
|
52
|
+
return fileCompare;
|
|
53
|
+
if (left.line !== right.line)
|
|
54
|
+
return left.line - right.line;
|
|
55
|
+
return left.index - right.index;
|
|
56
|
+
}
|
|
57
|
+
function buildSearchPreviewState(matches, payloads) {
|
|
58
|
+
const needsExternalize = matches.length > CONFIG.MAX_INLINE_MATCHES;
|
|
59
|
+
const visibleCount = needsExternalize
|
|
60
|
+
? CONFIG.MAX_INLINE_MATCHES
|
|
61
|
+
: matches.length;
|
|
62
|
+
const visibleMatches = matches.slice(0, visibleCount);
|
|
63
|
+
return {
|
|
64
|
+
needsExternalize,
|
|
65
|
+
visibleMatches,
|
|
66
|
+
visiblePayloads: payloads.slice(0, visibleCount),
|
|
67
|
+
heading: SearchResponseBuilder.buildHeading(matches.length, visibleMatches.length),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
16
70
|
export const SEARCH_CONTENT_TOOL = {
|
|
17
71
|
name: 'grep',
|
|
18
72
|
title: 'Search Content',
|
|
@@ -31,205 +85,216 @@ export const SEARCH_CONTENT_TOOL = {
|
|
|
31
85
|
],
|
|
32
86
|
taskSupport: 'optional',
|
|
33
87
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Handles formatting and building the search response.
|
|
90
|
+
*/
|
|
91
|
+
const SearchResponseBuilder = {
|
|
92
|
+
buildHeading(totalMatches, visibleMatches) {
|
|
93
|
+
if (visibleMatches >= totalMatches) {
|
|
94
|
+
return `Found ${totalMatches}:`;
|
|
40
95
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
for (const match of matches) {
|
|
81
|
-
lines.push(formatSearchMatchLine(match));
|
|
82
|
-
}
|
|
83
|
-
return joinLines(lines);
|
|
84
|
-
}
|
|
85
|
-
function buildSearchMatchPayload(match, context) {
|
|
86
|
-
const column = findColumnOffset(match.content, context.pattern, context.matcher, context.caseSensitive);
|
|
87
|
-
return {
|
|
88
|
-
file: match.relativeFile,
|
|
89
|
-
line: match.line,
|
|
90
|
-
...(column !== undefined ? { column } : {}),
|
|
91
|
-
content: match.content,
|
|
92
|
-
matchCount: match.matchCount,
|
|
93
|
-
...(match.contextBefore ? { contextBefore: [...match.contextBefore] } : {}),
|
|
94
|
-
...(match.contextAfter ? { contextAfter: [...match.contextAfter] } : {}),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
function formatSearchMatchLine(match) {
|
|
98
|
-
const lineNum = String(match.line).padStart(4);
|
|
99
|
-
return ` ${match.relativeFile}:${lineNum}: ${match.content}`;
|
|
100
|
-
}
|
|
101
|
-
function buildSearchMatchPayloads(matches, context) {
|
|
102
|
-
return matches.map((match) => buildSearchMatchPayload(match, context));
|
|
103
|
-
}
|
|
104
|
-
function buildStructuredSearchResult(summary, matches, options) {
|
|
105
|
-
return {
|
|
106
|
-
ok: true,
|
|
107
|
-
patternType: options.patternType,
|
|
108
|
-
caseSensitive: options.caseSensitive,
|
|
109
|
-
matches,
|
|
110
|
-
totalMatches: summary.matches,
|
|
111
|
-
filesScanned: summary.filesScanned,
|
|
112
|
-
...(summary.truncated ? { truncated: summary.truncated } : {}),
|
|
113
|
-
...(summary.filesMatched ? { filesMatched: summary.filesMatched } : {}),
|
|
114
|
-
...(summary.skippedTooLarge
|
|
115
|
-
? { skippedTooLarge: summary.skippedTooLarge }
|
|
116
|
-
: {}),
|
|
117
|
-
...(summary.skippedBinary ? { skippedBinary: summary.skippedBinary } : {}),
|
|
118
|
-
...(summary.skippedInaccessible
|
|
119
|
-
? { skippedInaccessible: summary.skippedInaccessible }
|
|
120
|
-
: {}),
|
|
121
|
-
...(summary.linesSkippedDueToRegexTimeout
|
|
122
|
-
? { linesSkippedDueToRegexTimeout: summary.linesSkippedDueToRegexTimeout }
|
|
123
|
-
: {}),
|
|
124
|
-
...(summary.stoppedReason ? { stoppedReason: summary.stoppedReason } : {}),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
function normalizeSearchMatches(result) {
|
|
128
|
-
const relativeByFile = new Map();
|
|
129
|
-
const normalized = [];
|
|
130
|
-
let index = 0;
|
|
131
|
-
for (const match of result.matches) {
|
|
132
|
-
const cached = relativeByFile.get(match.file);
|
|
133
|
-
const relative = cached ?? path.relative(result.basePath, match.file);
|
|
134
|
-
if (!cached)
|
|
135
|
-
relativeByFile.set(match.file, relative);
|
|
136
|
-
normalized.push({
|
|
96
|
+
return `Found ${totalMatches} (showing first ${visibleMatches}):`;
|
|
97
|
+
},
|
|
98
|
+
buildText(heading, matches, summary) {
|
|
99
|
+
if (matches.length === 0)
|
|
100
|
+
return 'No matches';
|
|
101
|
+
const text = this.buildMatchList(heading, matches);
|
|
102
|
+
if (!summary)
|
|
103
|
+
return text;
|
|
104
|
+
const summaryOpts = {
|
|
105
|
+
truncated: summary.truncated,
|
|
106
|
+
...(summary.truncated
|
|
107
|
+
? { truncatedReason: this.resolveTruncatedReason(summary) }
|
|
108
|
+
: {}),
|
|
109
|
+
};
|
|
110
|
+
return text + formatOperationSummary(summaryOpts);
|
|
111
|
+
},
|
|
112
|
+
buildStructured(summary, matches, options) {
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
patternType: options.patternType,
|
|
116
|
+
caseSensitive: options.caseSensitive,
|
|
117
|
+
matches,
|
|
118
|
+
totalMatches: summary.matches,
|
|
119
|
+
filesScanned: summary.filesScanned,
|
|
120
|
+
...buildStructuredSummaryFields(summary),
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
normalizeMatches(result) {
|
|
124
|
+
const relativeByFile = new Map();
|
|
125
|
+
const getRelativeFile = (file) => {
|
|
126
|
+
const cached = relativeByFile.get(file);
|
|
127
|
+
if (cached !== undefined)
|
|
128
|
+
return cached;
|
|
129
|
+
const relative = path.relative(result.basePath, file);
|
|
130
|
+
relativeByFile.set(file, relative);
|
|
131
|
+
return relative;
|
|
132
|
+
};
|
|
133
|
+
return result.matches
|
|
134
|
+
.map((match, index) => ({
|
|
137
135
|
...match,
|
|
138
|
-
relativeFile:
|
|
136
|
+
relativeFile: getRelativeFile(match.file),
|
|
139
137
|
index,
|
|
138
|
+
}))
|
|
139
|
+
.sort(compareNormalizedMatches);
|
|
140
|
+
},
|
|
141
|
+
buildMatchPayloads(matches, context) {
|
|
142
|
+
return matches.map((match) => {
|
|
143
|
+
const column = this.findColumnOffset(match.content, context);
|
|
144
|
+
return {
|
|
145
|
+
file: match.relativeFile,
|
|
146
|
+
line: match.line,
|
|
147
|
+
...(column !== undefined ? { column } : {}),
|
|
148
|
+
content: match.content,
|
|
149
|
+
matchCount: match.matchCount,
|
|
150
|
+
...(match.contextBefore
|
|
151
|
+
? { contextBefore: [...match.contextBefore] }
|
|
152
|
+
: {}),
|
|
153
|
+
...(match.contextAfter
|
|
154
|
+
? { contextAfter: [...match.contextAfter] }
|
|
155
|
+
: {}),
|
|
156
|
+
};
|
|
140
157
|
});
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
},
|
|
159
|
+
buildMatchList(heading, matches) {
|
|
160
|
+
if (matches.length === 0)
|
|
161
|
+
return heading;
|
|
162
|
+
// Fast path: calculate exact byte length to avoid arrays and string concatenation in V8
|
|
163
|
+
// +1 for newline after heading. Each match gets: " " (2) + relativeFile + ":" + line + ": " (2) + content + "\n"
|
|
164
|
+
let totalBytes = Buffer.byteLength(heading, 'utf8');
|
|
165
|
+
for (const match of matches) {
|
|
166
|
+
totalBytes +=
|
|
167
|
+
1 + // \n
|
|
168
|
+
2 + // " "
|
|
169
|
+
Buffer.byteLength(match.relativeFile, 'utf8') +
|
|
170
|
+
1 + // ":"
|
|
171
|
+
Math.max(4, String(match.line).length) +
|
|
172
|
+
2 + // ": "
|
|
173
|
+
Buffer.byteLength(match.content, 'utf8');
|
|
174
|
+
}
|
|
175
|
+
const buf = Buffer.allocUnsafe(totalBytes);
|
|
176
|
+
let offset = buf.write(heading, 0, 'utf8');
|
|
177
|
+
for (const match of matches) {
|
|
178
|
+
offset += buf.write('\n ', offset, 'utf8');
|
|
179
|
+
offset += buf.write(match.relativeFile, offset, 'utf8');
|
|
180
|
+
offset += buf.write(':', offset, 'utf8');
|
|
181
|
+
offset += buf.write(String(match.line).padStart(4), offset, 'utf8');
|
|
182
|
+
offset += buf.write(': ', offset, 'utf8');
|
|
183
|
+
offset += buf.write(match.content, offset, 'utf8');
|
|
184
|
+
}
|
|
185
|
+
return buf.toString('utf8', 0, offset);
|
|
186
|
+
},
|
|
187
|
+
resolveTruncatedReason(summary) {
|
|
188
|
+
if (summary.stoppedReason === 'timeout')
|
|
189
|
+
return 'timeout';
|
|
190
|
+
if (summary.stoppedReason === 'maxFiles') {
|
|
191
|
+
return `max files (${summary.filesScanned})`;
|
|
192
|
+
}
|
|
193
|
+
return `max results (${summary.matches})`;
|
|
194
|
+
},
|
|
195
|
+
findColumnOffset(content, context) {
|
|
196
|
+
try {
|
|
197
|
+
if (context.matcher) {
|
|
198
|
+
context.matcher.lastIndex = 0;
|
|
199
|
+
const match = context.matcher.exec(content);
|
|
200
|
+
return match ? match.index : undefined;
|
|
201
|
+
}
|
|
202
|
+
if (context.caseSensitive) {
|
|
203
|
+
const idx = content.indexOf(context.pattern);
|
|
204
|
+
return idx >= 0 ? idx : undefined;
|
|
205
|
+
}
|
|
206
|
+
// Case-insensitive literal search
|
|
207
|
+
const idx = content.toLowerCase().indexOf(context.foldedPattern ?? '');
|
|
208
|
+
return idx >= 0 ? idx : undefined;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
const SearchExecutor = {
|
|
216
|
+
async run(args, basePath, signal, onProgress) {
|
|
217
|
+
const excludePatterns = args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS;
|
|
218
|
+
const options = {
|
|
219
|
+
includeHidden: args.includeHidden,
|
|
220
|
+
excludePatterns,
|
|
221
|
+
filePattern: args.filePattern,
|
|
222
|
+
caseSensitive: args.caseSensitive,
|
|
223
|
+
wholeWord: args.wholeWord,
|
|
224
|
+
contextLines: args.contextLines,
|
|
225
|
+
maxResults: args.maxResults,
|
|
226
|
+
isLiteral: !args.isRegex,
|
|
227
|
+
multiline: args.multiline,
|
|
228
|
+
...(signal ? { signal } : {}),
|
|
229
|
+
...(onProgress ? { onProgress } : {}),
|
|
230
|
+
};
|
|
231
|
+
try {
|
|
232
|
+
return await searchContent(basePath, args.pattern, options);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (error instanceof Error && /regular expression/i.test(error.message)) {
|
|
236
|
+
throw new McpError(ErrorCode.E_INVALID_PATTERN, error.message);
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
createMatcher(args) {
|
|
242
|
+
if (!args.isRegex)
|
|
243
|
+
return undefined;
|
|
159
244
|
try {
|
|
160
245
|
const flags = (args.caseSensitive ? '' : 'i') + (args.multiline ? 'm' : '');
|
|
161
|
-
|
|
246
|
+
return new RE2(args.pattern, flags);
|
|
162
247
|
}
|
|
163
248
|
catch (error) {
|
|
164
249
|
throw new McpError(ErrorCode.E_INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
|
|
165
250
|
}
|
|
251
|
+
},
|
|
252
|
+
createSearchContext(args, matcher) {
|
|
253
|
+
return {
|
|
254
|
+
pattern: args.pattern,
|
|
255
|
+
caseSensitive: args.caseSensitive,
|
|
256
|
+
...(matcher ? { matcher } : {}),
|
|
257
|
+
...(!args.isRegex && !args.caseSensitive
|
|
258
|
+
? { foldedPattern: args.pattern.toLowerCase() }
|
|
259
|
+
: {}),
|
|
260
|
+
};
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
264
|
+
const basePath = resolvePathOrRoot(args.path);
|
|
265
|
+
const patternType = args.isRegex ? 'regex' : 'literal';
|
|
266
|
+
const regexMatcher = SearchExecutor.createMatcher(args);
|
|
267
|
+
const result = await SearchExecutor.run(args, basePath, signal, onProgress);
|
|
268
|
+
const normalizedMatches = SearchResponseBuilder.normalizeMatches(result);
|
|
269
|
+
const searchContext = SearchExecutor.createSearchContext(args, regexMatcher);
|
|
270
|
+
const matchPayloads = SearchResponseBuilder.buildMatchPayloads(normalizedMatches, searchContext);
|
|
271
|
+
const fullStructured = SearchResponseBuilder.buildStructured(result.summary, matchPayloads, { patternType, caseSensitive: args.caseSensitive });
|
|
272
|
+
const preview = buildSearchPreviewState(normalizedMatches, matchPayloads);
|
|
273
|
+
if (resourceStore && preview.needsExternalize) {
|
|
274
|
+
const previewStructured = {
|
|
275
|
+
...fullStructured,
|
|
276
|
+
matches: preview.visiblePayloads,
|
|
277
|
+
truncated: true,
|
|
278
|
+
};
|
|
279
|
+
const entry = resourceStore.putText({
|
|
280
|
+
name: 'grep:matches',
|
|
281
|
+
mimeType: 'application/json',
|
|
282
|
+
text: JSON.stringify(fullStructured),
|
|
283
|
+
});
|
|
284
|
+
previewStructured.resourceUri = entry.uri;
|
|
285
|
+
const text = SearchResponseBuilder.buildText(preview.heading, preview.visibleMatches);
|
|
286
|
+
return buildToolResponse(text, previewStructured, [
|
|
287
|
+
buildResourceLink({
|
|
288
|
+
uri: entry.uri,
|
|
289
|
+
name: entry.name,
|
|
290
|
+
mimeType: entry.mimeType,
|
|
291
|
+
description: 'Full grep results as JSON (structuredContent)',
|
|
292
|
+
expiresAt: entry.expiresAt,
|
|
293
|
+
}),
|
|
294
|
+
]);
|
|
166
295
|
}
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
excludePatterns,
|
|
170
|
-
filePattern: args.filePattern,
|
|
171
|
-
caseSensitive: args.caseSensitive,
|
|
172
|
-
wholeWord: args.wholeWord,
|
|
173
|
-
contextLines: args.contextLines,
|
|
174
|
-
maxResults: args.maxResults,
|
|
175
|
-
isLiteral: !args.isRegex,
|
|
176
|
-
multiline: args.multiline,
|
|
177
|
-
};
|
|
178
|
-
if (signal) {
|
|
179
|
-
options.signal = signal;
|
|
180
|
-
}
|
|
181
|
-
if (onProgress) {
|
|
182
|
-
options.onProgress = onProgress;
|
|
183
|
-
}
|
|
184
|
-
let result;
|
|
185
|
-
try {
|
|
186
|
-
result = await searchContent(basePath, args.pattern, options);
|
|
187
|
-
}
|
|
188
|
-
catch (error) {
|
|
189
|
-
if (error instanceof Error && /regular expression/i.test(error.message)) {
|
|
190
|
-
throw new McpError(ErrorCode.E_INVALID_PATTERN, error.message);
|
|
191
|
-
}
|
|
192
|
-
throw error;
|
|
193
|
-
}
|
|
194
|
-
const normalizedMatches = normalizeSearchMatches(result);
|
|
195
|
-
const searchContext = {
|
|
196
|
-
pattern: args.pattern,
|
|
197
|
-
matcher: regexMatcher,
|
|
198
|
-
caseSensitive: args.caseSensitive,
|
|
199
|
-
};
|
|
200
|
-
const matchPayloads = buildSearchMatchPayloads(normalizedMatches, searchContext);
|
|
201
|
-
const structuredFull = buildStructuredSearchResult(result.summary, matchPayloads, {
|
|
202
|
-
patternType,
|
|
203
|
-
caseSensitive: args.caseSensitive,
|
|
204
|
-
});
|
|
205
|
-
const needsExternalize = normalizedMatches.length > MAX_INLINE_MATCHES;
|
|
206
|
-
if (!resourceStore || !needsExternalize) {
|
|
207
|
-
return buildToolResponse(buildSearchTextResult(`Found ${normalizedMatches.length}:`, normalizedMatches, result.summary), structuredFull);
|
|
208
|
-
}
|
|
209
|
-
const previewMatches = normalizedMatches.slice(0, MAX_INLINE_MATCHES);
|
|
210
|
-
const previewPayload = buildSearchMatchPayloads(previewMatches, searchContext);
|
|
211
|
-
const previewStructured = {
|
|
212
|
-
...structuredFull,
|
|
213
|
-
matches: previewPayload,
|
|
214
|
-
truncated: true,
|
|
215
|
-
resourceUri: undefined,
|
|
216
|
-
};
|
|
217
|
-
const entry = resourceStore.putText({
|
|
218
|
-
name: 'grep:matches',
|
|
219
|
-
mimeType: 'application/json',
|
|
220
|
-
text: JSON.stringify(structuredFull),
|
|
221
|
-
});
|
|
222
|
-
previewStructured.resourceUri = entry.uri;
|
|
223
|
-
const text = buildSearchTextResult(`Found ${normalizedMatches.length} (showing first ${MAX_INLINE_MATCHES}):`, previewMatches);
|
|
224
|
-
return buildToolResponse(text, previewStructured, [
|
|
225
|
-
buildResourceLink({
|
|
226
|
-
uri: entry.uri,
|
|
227
|
-
name: entry.name,
|
|
228
|
-
mimeType: entry.mimeType,
|
|
229
|
-
description: 'Full grep results as JSON (structuredContent)',
|
|
230
|
-
expiresAt: entry.expiresAt,
|
|
231
|
-
}),
|
|
232
|
-
]);
|
|
296
|
+
const text = SearchResponseBuilder.buildText(preview.heading, preview.visibleMatches, result.summary);
|
|
297
|
+
return buildToolResponse(text, fullStructured);
|
|
233
298
|
}
|
|
234
299
|
export function registerSearchContentTool(server, options = {}) {
|
|
235
300
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
@@ -237,14 +302,14 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
237
302
|
extra,
|
|
238
303
|
context: { path: args.path ?? '.' },
|
|
239
304
|
run: async (signal) => {
|
|
240
|
-
const scope = args
|
|
241
|
-
const
|
|
242
|
-
const progress = createToolProgressSession(extra,
|
|
305
|
+
const { pattern, filePattern: scope } = args;
|
|
306
|
+
const progressLabel = `🔎︎ grep: ${pattern}`;
|
|
307
|
+
const progress = createToolProgressSession(extra, progressLabel);
|
|
243
308
|
const progressWithMessage = ({ current, total, }) => {
|
|
244
309
|
progress.update({
|
|
245
310
|
current,
|
|
246
311
|
...(total !== undefined ? { total } : {}),
|
|
247
|
-
message:
|
|
312
|
+
message: `${progressLabel} [${current} files]`,
|
|
248
313
|
});
|
|
249
314
|
};
|
|
250
315
|
try {
|
|
@@ -253,18 +318,13 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
253
318
|
const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
|
|
254
319
|
const filesMatched = sc.ok ? (sc.filesMatched ?? 0) : 0;
|
|
255
320
|
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
256
|
-
const suffix = buildCompletionSuffix(
|
|
257
|
-
count,
|
|
258
|
-
filesMatched,
|
|
259
|
-
scope,
|
|
260
|
-
stoppedReason,
|
|
261
|
-
});
|
|
321
|
+
const suffix = buildCompletionSuffix(count, filesMatched, scope, stoppedReason);
|
|
262
322
|
const finalCurrent = resolveFinalProgressCurrent(progress, (sc.filesScanned ?? 0) + 1);
|
|
263
|
-
progress.complete(
|
|
323
|
+
progress.complete(`${progressLabel} • ${suffix}`, finalCurrent);
|
|
264
324
|
return result;
|
|
265
325
|
}
|
|
266
326
|
catch (error) {
|
|
267
|
-
progress.fail(
|
|
327
|
+
progress.fail(`${progressLabel} • failed`);
|
|
268
328
|
throw error;
|
|
269
329
|
}
|
|
270
330
|
},
|
|
@@ -279,14 +339,3 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
279
339
|
return;
|
|
280
340
|
server.registerTool('grep', withDefaultIcons({ ...SEARCH_CONTENT_TOOL }, options.iconInfo), validatedHandler);
|
|
281
341
|
}
|
|
282
|
-
function buildCompletionSuffix(params) {
|
|
283
|
-
if (params.count === 0) {
|
|
284
|
-
return `No matches in ${params.scope}`;
|
|
285
|
-
}
|
|
286
|
-
const matchWord = params.count === 1 ? 'match' : 'matches';
|
|
287
|
-
const fileWord = params.filesMatched === 1 ? 'file' : 'files';
|
|
288
|
-
const reasonSuffix = params.stoppedReason !== undefined
|
|
289
|
-
? ` [${SEARCH_COMPLETION_REASON_LABELS[params.stoppedReason]}]`
|
|
290
|
-
: '';
|
|
291
|
-
return `${params.count} ${matchWord} in ${params.filesMatched} ${fileWord}${reasonSuffix}`;
|
|
292
|
-
}
|
package/dist/tools/shared.js
CHANGED
|
@@ -34,6 +34,36 @@ export const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS = {
|
|
|
34
34
|
destructiveHint: false,
|
|
35
35
|
openWorldHint: false,
|
|
36
36
|
};
|
|
37
|
+
function isTaskSupportLevel(value) {
|
|
38
|
+
return value === 'optional' || value === 'required' || value === 'forbidden';
|
|
39
|
+
}
|
|
40
|
+
function normalizeToolExecution(tool) {
|
|
41
|
+
const candidate = tool;
|
|
42
|
+
const topLevelTaskSupport = candidate['taskSupport'];
|
|
43
|
+
const existingExecution = candidate['execution'] && typeof candidate['execution'] === 'object'
|
|
44
|
+
? candidate['execution']
|
|
45
|
+
: undefined;
|
|
46
|
+
const executionTaskSupport = existingExecution?.['taskSupport'];
|
|
47
|
+
const resolvedTaskSupport = isTaskSupportLevel(topLevelTaskSupport)
|
|
48
|
+
? topLevelTaskSupport
|
|
49
|
+
: isTaskSupportLevel(executionTaskSupport)
|
|
50
|
+
? executionTaskSupport
|
|
51
|
+
: undefined;
|
|
52
|
+
if (resolvedTaskSupport === undefined && topLevelTaskSupport === undefined) {
|
|
53
|
+
return tool;
|
|
54
|
+
}
|
|
55
|
+
const normalized = { ...candidate };
|
|
56
|
+
delete normalized['taskSupport'];
|
|
57
|
+
if (resolvedTaskSupport !== undefined || existingExecution !== undefined) {
|
|
58
|
+
normalized['execution'] = {
|
|
59
|
+
...(existingExecution ?? {}),
|
|
60
|
+
...(resolvedTaskSupport !== undefined
|
|
61
|
+
? { taskSupport: resolvedTaskSupport }
|
|
62
|
+
: {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
37
67
|
export function shouldStripStructuredOutput() {
|
|
38
68
|
return parseTrueEnvFlag(process.env['FS_CONTEXT_STRIP_STRUCTURED']);
|
|
39
69
|
}
|
|
@@ -133,15 +163,16 @@ function canReportProgress(extra) {
|
|
|
133
163
|
return canSendProgress(extra) || hasTask;
|
|
134
164
|
}
|
|
135
165
|
export function withDefaultIcons(tool, iconInfo) {
|
|
166
|
+
const normalizedTool = normalizeToolExecution(tool);
|
|
136
167
|
if (!iconInfo) {
|
|
137
|
-
return maybeStripOutputSchema(
|
|
168
|
+
return maybeStripOutputSchema(normalizedTool);
|
|
138
169
|
}
|
|
139
|
-
const existingIcons =
|
|
170
|
+
const existingIcons = normalizedTool.icons;
|
|
140
171
|
if (existingIcons && existingIcons.length > 0) {
|
|
141
|
-
return maybeStripOutputSchema(
|
|
172
|
+
return maybeStripOutputSchema(normalizedTool);
|
|
142
173
|
}
|
|
143
174
|
const withIcons = {
|
|
144
|
-
...
|
|
175
|
+
...normalizedTool,
|
|
145
176
|
icons: [
|
|
146
177
|
{
|
|
147
178
|
src: iconInfo.src,
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
1
2
|
import type { ToolTaskHandler } from '@modelcontextprotocol/sdk/experimental/tasks/interfaces.js';
|
|
2
3
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
4
|
import type { AnySchema, SchemaOutput, ShapeOutput, ZodRawShapeCompat } from '@modelcontextprotocol/sdk/server/zod-compat.js';
|
|
4
5
|
import type { RequestTaskStore } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
5
6
|
import type { IconInfo, ToolExtra, ToolResult } from './shared.js';
|
|
7
|
+
export interface TaskContext {
|
|
8
|
+
taskId: string;
|
|
9
|
+
toolName?: string | undefined;
|
|
10
|
+
startTime: number;
|
|
11
|
+
}
|
|
12
|
+
export declare const taskContext: AsyncLocalStorage<TaskContext>;
|
|
6
13
|
type TaskToolExtra = ToolExtra & {
|
|
7
14
|
taskId?: string;
|
|
8
15
|
taskStore?: RequestTaskStore;
|