@j0hanz/filesystem-mcp 1.11.1 → 1.12.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/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/tools/apply-patch.js +24 -30
- 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 +163 -112
- 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 +245 -216
- 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
|
@@ -7,12 +7,66 @@ import { formatOperationSummary, joinLines } 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,196 @@ export const SEARCH_CONTENT_TOOL = {
|
|
|
31
85
|
],
|
|
32
86
|
taskSupport: 'optional',
|
|
33
87
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (caseSensitive) {
|
|
42
|
-
const idx = content.indexOf(pattern);
|
|
43
|
-
return idx >= 0 ? idx : undefined;
|
|
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}:`;
|
|
44
95
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return text;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
},
|
|
159
|
+
buildMatchList(heading, matches) {
|
|
160
|
+
const lines = [heading];
|
|
161
|
+
for (const match of matches) {
|
|
162
|
+
const lineNum = String(match.line).padStart(4);
|
|
163
|
+
lines.push(` ${match.relativeFile}:${lineNum}: ${match.content}`);
|
|
164
|
+
}
|
|
165
|
+
return joinLines(lines);
|
|
166
|
+
},
|
|
167
|
+
resolveTruncatedReason(summary) {
|
|
168
|
+
if (summary.stoppedReason === 'timeout')
|
|
169
|
+
return 'timeout';
|
|
170
|
+
if (summary.stoppedReason === 'maxFiles') {
|
|
171
|
+
return `max files (${summary.filesScanned})`;
|
|
172
|
+
}
|
|
173
|
+
return `max results (${summary.matches})`;
|
|
174
|
+
},
|
|
175
|
+
findColumnOffset(content, context) {
|
|
176
|
+
try {
|
|
177
|
+
if (context.matcher) {
|
|
178
|
+
context.matcher.lastIndex = 0;
|
|
179
|
+
const match = context.matcher.exec(content);
|
|
180
|
+
return match ? match.index : undefined;
|
|
181
|
+
}
|
|
182
|
+
if (context.caseSensitive) {
|
|
183
|
+
const idx = content.indexOf(context.pattern);
|
|
184
|
+
return idx >= 0 ? idx : undefined;
|
|
185
|
+
}
|
|
186
|
+
// Case-insensitive literal search
|
|
187
|
+
const idx = content.toLowerCase().indexOf(context.foldedPattern ?? '');
|
|
188
|
+
return idx >= 0 ? idx : undefined;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
const SearchExecutor = {
|
|
196
|
+
async run(args, basePath, signal, onProgress) {
|
|
197
|
+
const excludePatterns = args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS;
|
|
198
|
+
const options = {
|
|
199
|
+
includeHidden: args.includeHidden,
|
|
200
|
+
excludePatterns,
|
|
201
|
+
filePattern: args.filePattern,
|
|
202
|
+
caseSensitive: args.caseSensitive,
|
|
203
|
+
wholeWord: args.wholeWord,
|
|
204
|
+
contextLines: args.contextLines,
|
|
205
|
+
maxResults: args.maxResults,
|
|
206
|
+
isLiteral: !args.isRegex,
|
|
207
|
+
multiline: args.multiline,
|
|
208
|
+
...(signal ? { signal } : {}),
|
|
209
|
+
...(onProgress ? { onProgress } : {}),
|
|
210
|
+
};
|
|
211
|
+
try {
|
|
212
|
+
return await searchContent(basePath, args.pattern, options);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (error instanceof Error && /regular expression/i.test(error.message)) {
|
|
216
|
+
throw new McpError(ErrorCode.E_INVALID_PATTERN, error.message);
|
|
217
|
+
}
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
createMatcher(args) {
|
|
222
|
+
if (!args.isRegex)
|
|
223
|
+
return undefined;
|
|
159
224
|
try {
|
|
160
225
|
const flags = (args.caseSensitive ? '' : 'i') + (args.multiline ? 'm' : '');
|
|
161
|
-
|
|
226
|
+
return new RE2(args.pattern, flags);
|
|
162
227
|
}
|
|
163
228
|
catch (error) {
|
|
164
229
|
throw new McpError(ErrorCode.E_INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
|
|
165
230
|
}
|
|
231
|
+
},
|
|
232
|
+
createSearchContext(args, matcher) {
|
|
233
|
+
return {
|
|
234
|
+
pattern: args.pattern,
|
|
235
|
+
caseSensitive: args.caseSensitive,
|
|
236
|
+
...(matcher ? { matcher } : {}),
|
|
237
|
+
...(!args.isRegex && !args.caseSensitive
|
|
238
|
+
? { foldedPattern: args.pattern.toLowerCase() }
|
|
239
|
+
: {}),
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
244
|
+
const basePath = resolvePathOrRoot(args.path);
|
|
245
|
+
const patternType = args.isRegex ? 'regex' : 'literal';
|
|
246
|
+
const regexMatcher = SearchExecutor.createMatcher(args);
|
|
247
|
+
const result = await SearchExecutor.run(args, basePath, signal, onProgress);
|
|
248
|
+
const normalizedMatches = SearchResponseBuilder.normalizeMatches(result);
|
|
249
|
+
const searchContext = SearchExecutor.createSearchContext(args, regexMatcher);
|
|
250
|
+
const matchPayloads = SearchResponseBuilder.buildMatchPayloads(normalizedMatches, searchContext);
|
|
251
|
+
const fullStructured = SearchResponseBuilder.buildStructured(result.summary, matchPayloads, { patternType, caseSensitive: args.caseSensitive });
|
|
252
|
+
const preview = buildSearchPreviewState(normalizedMatches, matchPayloads);
|
|
253
|
+
if (resourceStore && preview.needsExternalize) {
|
|
254
|
+
const previewStructured = {
|
|
255
|
+
...fullStructured,
|
|
256
|
+
matches: preview.visiblePayloads,
|
|
257
|
+
truncated: true,
|
|
258
|
+
};
|
|
259
|
+
const entry = resourceStore.putText({
|
|
260
|
+
name: 'grep:matches',
|
|
261
|
+
mimeType: 'application/json',
|
|
262
|
+
text: JSON.stringify(fullStructured),
|
|
263
|
+
});
|
|
264
|
+
previewStructured.resourceUri = entry.uri;
|
|
265
|
+
const text = SearchResponseBuilder.buildText(preview.heading, preview.visibleMatches);
|
|
266
|
+
return buildToolResponse(text, previewStructured, [
|
|
267
|
+
buildResourceLink({
|
|
268
|
+
uri: entry.uri,
|
|
269
|
+
name: entry.name,
|
|
270
|
+
mimeType: entry.mimeType,
|
|
271
|
+
description: 'Full grep results as JSON (structuredContent)',
|
|
272
|
+
expiresAt: entry.expiresAt,
|
|
273
|
+
}),
|
|
274
|
+
]);
|
|
166
275
|
}
|
|
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
|
-
]);
|
|
276
|
+
const text = SearchResponseBuilder.buildText(preview.heading, preview.visibleMatches, result.summary);
|
|
277
|
+
return buildToolResponse(text, fullStructured);
|
|
233
278
|
}
|
|
234
279
|
export function registerSearchContentTool(server, options = {}) {
|
|
235
280
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
@@ -237,14 +282,14 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
237
282
|
extra,
|
|
238
283
|
context: { path: args.path ?? '.' },
|
|
239
284
|
run: async (signal) => {
|
|
240
|
-
const scope = args
|
|
241
|
-
const
|
|
242
|
-
const progress = createToolProgressSession(extra,
|
|
285
|
+
const { pattern, filePattern: scope } = args;
|
|
286
|
+
const progressLabel = `🔎︎ grep: ${pattern}`;
|
|
287
|
+
const progress = createToolProgressSession(extra, progressLabel);
|
|
243
288
|
const progressWithMessage = ({ current, total, }) => {
|
|
244
289
|
progress.update({
|
|
245
290
|
current,
|
|
246
291
|
...(total !== undefined ? { total } : {}),
|
|
247
|
-
message:
|
|
292
|
+
message: `${progressLabel} [${current} files]`,
|
|
248
293
|
});
|
|
249
294
|
};
|
|
250
295
|
try {
|
|
@@ -253,18 +298,13 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
253
298
|
const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
|
|
254
299
|
const filesMatched = sc.ok ? (sc.filesMatched ?? 0) : 0;
|
|
255
300
|
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
256
|
-
const suffix = buildCompletionSuffix(
|
|
257
|
-
count,
|
|
258
|
-
filesMatched,
|
|
259
|
-
scope,
|
|
260
|
-
stoppedReason,
|
|
261
|
-
});
|
|
301
|
+
const suffix = buildCompletionSuffix(count, filesMatched, scope, stoppedReason);
|
|
262
302
|
const finalCurrent = resolveFinalProgressCurrent(progress, (sc.filesScanned ?? 0) + 1);
|
|
263
|
-
progress.complete(
|
|
303
|
+
progress.complete(`${progressLabel} • ${suffix}`, finalCurrent);
|
|
264
304
|
return result;
|
|
265
305
|
}
|
|
266
306
|
catch (error) {
|
|
267
|
-
progress.fail(
|
|
307
|
+
progress.fail(`${progressLabel} • failed`);
|
|
268
308
|
throw error;
|
|
269
309
|
}
|
|
270
310
|
},
|
|
@@ -279,14 +319,3 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
279
319
|
return;
|
|
280
320
|
server.registerTool('grep', withDefaultIcons({ ...SEARCH_CONTENT_TOOL }, options.iconInfo), validatedHandler);
|
|
281
321
|
}
|
|
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;
|