@j0hanz/filesystem-mcp 1.11.0 → 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/completions.js +25 -0
- 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/prompts.d.ts +1 -0
- package/dist/prompts.js +58 -0
- package/dist/resources/generated-instructions.js +11 -3
- package/dist/resources/tool-catalog.js +49 -3
- package/dist/resources/tool-info.d.ts +1 -0
- package/dist/resources/tool-info.js +139 -8
- package/dist/resources.js +2 -0
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.js +10 -4
- package/dist/server/bootstrap.js +41 -5
- package/dist/tools/apply-patch.js +30 -33
- package/dist/tools/calculate-hash.js +21 -24
- package/dist/tools/contract.d.ts +1 -1
- package/dist/tools/create-directory.js +3 -3
- package/dist/tools/delete-file.js +2 -2
- package/dist/tools/diff-files.js +32 -20
- package/dist/tools/edit-file.d.ts +4 -1
- package/dist/tools/edit-file.js +172 -104
- package/dist/tools/list-directory.js +105 -10
- package/dist/tools/move-file.js +3 -3
- package/dist/tools/read-multiple.d.ts +1 -1
- package/dist/tools/read-multiple.js +123 -99
- package/dist/tools/read.js +104 -76
- package/dist/tools/replace-in-files.d.ts +5 -2
- package/dist/tools/replace-in-files.js +142 -76
- package/dist/tools/roots.js +1 -1
- package/dist/tools/search-content.js +249 -218
- package/dist/tools/shared.js +39 -7
- package/dist/tools/stat-many.js +12 -19
- package/dist/tools/stat.js +1 -1
- package/dist/tools/task-support.d.ts +7 -0
- package/dist/tools/task-support.js +140 -101
- package/dist/tools/write-file.js +4 -4
- package/dist/tools.js +2 -2
- package/package.json +1 -1
|
@@ -7,7 +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
|
-
|
|
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
|
+
},
|
|
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
|
+
}
|
|
11
70
|
export const SEARCH_CONTENT_TOOL = {
|
|
12
71
|
name: 'grep',
|
|
13
72
|
title: 'Search Content',
|
|
@@ -26,203 +85,196 @@ export const SEARCH_CONTENT_TOOL = {
|
|
|
26
85
|
],
|
|
27
86
|
taskSupport: 'optional',
|
|
28
87
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
function findColumnOffset(content, pattern, isRegex, caseSensitive) {
|
|
38
|
-
try {
|
|
39
|
-
if (isRegex) {
|
|
40
|
-
const flags = caseSensitive ? '' : 'i';
|
|
41
|
-
const regex = new RE2(pattern, flags);
|
|
42
|
-
const match = regex.exec(content);
|
|
43
|
-
return match ? match.index : 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
|
-
const
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
return joinLines(lines);
|
|
86
|
-
}
|
|
87
|
-
function buildSearchMatchPayload(match, context) {
|
|
88
|
-
const column = findColumnOffset(match.content, context.pattern, context.isRegex, context.caseSensitive);
|
|
89
|
-
return {
|
|
90
|
-
file: match.relativeFile,
|
|
91
|
-
line: match.line,
|
|
92
|
-
...(column !== undefined ? { column } : {}),
|
|
93
|
-
content: match.content,
|
|
94
|
-
matchCount: match.matchCount,
|
|
95
|
-
...(match.contextBefore ? { contextBefore: [...match.contextBefore] } : {}),
|
|
96
|
-
...(match.contextAfter ? { contextAfter: [...match.contextAfter] } : {}),
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
function formatSearchMatchLine(match) {
|
|
100
|
-
const lineNum = String(match.line).padStart(4);
|
|
101
|
-
return ` ${match.relativeFile}:${lineNum}: ${match.content}`;
|
|
102
|
-
}
|
|
103
|
-
function buildStructuredSearchResult(result, normalizedMatches, options, context) {
|
|
104
|
-
const { summary } = result;
|
|
105
|
-
const matches = normalizedMatches.map((match) => buildSearchMatchPayload(match, context));
|
|
106
|
-
return {
|
|
107
|
-
ok: true,
|
|
108
|
-
patternType: options.patternType,
|
|
109
|
-
caseSensitive: options.caseSensitive,
|
|
110
|
-
matches,
|
|
111
|
-
totalMatches: summary.matches,
|
|
112
|
-
filesScanned: summary.filesScanned,
|
|
113
|
-
...(summary.truncated ? { truncated: summary.truncated } : {}),
|
|
114
|
-
...(summary.filesMatched ? { filesMatched: summary.filesMatched } : {}),
|
|
115
|
-
...(summary.skippedTooLarge
|
|
116
|
-
? { skippedTooLarge: summary.skippedTooLarge }
|
|
117
|
-
: {}),
|
|
118
|
-
...(summary.skippedBinary ? { skippedBinary: summary.skippedBinary } : {}),
|
|
119
|
-
...(summary.skippedInaccessible
|
|
120
|
-
? { skippedInaccessible: summary.skippedInaccessible }
|
|
121
|
-
: {}),
|
|
122
|
-
...(summary.linesSkippedDueToRegexTimeout
|
|
123
|
-
? { linesSkippedDueToRegexTimeout: summary.linesSkippedDueToRegexTimeout }
|
|
124
|
-
: {}),
|
|
125
|
-
...(summary.stoppedReason ? { stoppedReason: summary.stoppedReason } : {}),
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
function normalizeSearchMatches(result) {
|
|
129
|
-
const relativeByFile = new Map();
|
|
130
|
-
const normalized = [];
|
|
131
|
-
let index = 0;
|
|
132
|
-
for (const match of result.matches) {
|
|
133
|
-
const cached = relativeByFile.get(match.file);
|
|
134
|
-
const relative = cached ?? path.relative(result.basePath, match.file);
|
|
135
|
-
if (!cached)
|
|
136
|
-
relativeByFile.set(match.file, relative);
|
|
137
|
-
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) => ({
|
|
138
135
|
...match,
|
|
139
|
-
relativeFile:
|
|
136
|
+
relativeFile: getRelativeFile(match.file),
|
|
140
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
|
+
};
|
|
141
157
|
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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;
|
|
224
|
+
try {
|
|
225
|
+
const flags = (args.caseSensitive ? '' : 'i') + (args.multiline ? 'm' : '');
|
|
226
|
+
return new RE2(args.pattern, flags);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
throw new McpError(ErrorCode.E_INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
|
|
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
|
+
};
|
|
154
243
|
async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
155
244
|
const basePath = resolvePathOrRoot(args.path);
|
|
156
|
-
const excludePatterns = args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS;
|
|
157
245
|
const patternType = args.isRegex ? 'regex' : 'literal';
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
const normalizedMatches = normalizeSearchMatches(result);
|
|
189
|
-
const searchContext = {
|
|
190
|
-
pattern: args.pattern,
|
|
191
|
-
isRegex: args.isRegex,
|
|
192
|
-
caseSensitive: args.caseSensitive,
|
|
193
|
-
};
|
|
194
|
-
const structuredFull = buildStructuredSearchResult(result, normalizedMatches, {
|
|
195
|
-
patternType,
|
|
196
|
-
caseSensitive: args.caseSensitive,
|
|
197
|
-
}, searchContext);
|
|
198
|
-
const needsExternalize = normalizedMatches.length > MAX_INLINE_MATCHES;
|
|
199
|
-
if (!resourceStore || !needsExternalize) {
|
|
200
|
-
return buildToolResponse(buildSearchTextResult(result, normalizedMatches), structuredFull);
|
|
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
|
+
]);
|
|
201
275
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
const previewStructured = {
|
|
205
|
-
...structuredFull,
|
|
206
|
-
matches: previewPayload,
|
|
207
|
-
truncated: true,
|
|
208
|
-
resourceUri: undefined,
|
|
209
|
-
};
|
|
210
|
-
const entry = resourceStore.putText({
|
|
211
|
-
name: 'grep:matches',
|
|
212
|
-
mimeType: 'application/json',
|
|
213
|
-
text: JSON.stringify(structuredFull),
|
|
214
|
-
});
|
|
215
|
-
previewStructured.resourceUri = entry.uri;
|
|
216
|
-
const text = buildMatchListText(`Found ${normalizedMatches.length} (showing first ${MAX_INLINE_MATCHES}):`, previewMatches);
|
|
217
|
-
return buildToolResponse(text, previewStructured, [
|
|
218
|
-
buildResourceLink({
|
|
219
|
-
uri: entry.uri,
|
|
220
|
-
name: entry.name,
|
|
221
|
-
mimeType: entry.mimeType,
|
|
222
|
-
description: 'Full grep results as JSON (structuredContent)',
|
|
223
|
-
expiresAt: entry.expiresAt,
|
|
224
|
-
}),
|
|
225
|
-
]);
|
|
276
|
+
const text = SearchResponseBuilder.buildText(preview.heading, preview.visibleMatches, result.summary);
|
|
277
|
+
return buildToolResponse(text, fullStructured);
|
|
226
278
|
}
|
|
227
279
|
export function registerSearchContentTool(server, options = {}) {
|
|
228
280
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
@@ -230,14 +282,14 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
230
282
|
extra,
|
|
231
283
|
context: { path: args.path ?? '.' },
|
|
232
284
|
run: async (signal) => {
|
|
233
|
-
const scope = args
|
|
234
|
-
const
|
|
235
|
-
const progress = createToolProgressSession(extra,
|
|
285
|
+
const { pattern, filePattern: scope } = args;
|
|
286
|
+
const progressLabel = `🔎︎ grep: ${pattern}`;
|
|
287
|
+
const progress = createToolProgressSession(extra, progressLabel);
|
|
236
288
|
const progressWithMessage = ({ current, total, }) => {
|
|
237
289
|
progress.update({
|
|
238
290
|
current,
|
|
239
291
|
...(total !== undefined ? { total } : {}),
|
|
240
|
-
message:
|
|
292
|
+
message: `${progressLabel} [${current} files]`,
|
|
241
293
|
});
|
|
242
294
|
};
|
|
243
295
|
try {
|
|
@@ -246,39 +298,18 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
246
298
|
const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
|
|
247
299
|
const filesMatched = sc.ok ? (sc.filesMatched ?? 0) : 0;
|
|
248
300
|
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
249
|
-
const suffix = buildCompletionSuffix(
|
|
250
|
-
count,
|
|
251
|
-
filesMatched,
|
|
252
|
-
scope,
|
|
253
|
-
stoppedReason,
|
|
254
|
-
});
|
|
301
|
+
const suffix = buildCompletionSuffix(count, filesMatched, scope, stoppedReason);
|
|
255
302
|
const finalCurrent = resolveFinalProgressCurrent(progress, (sc.filesScanned ?? 0) + 1);
|
|
256
|
-
progress.complete(
|
|
303
|
+
progress.complete(`${progressLabel} • ${suffix}`, finalCurrent);
|
|
257
304
|
return result;
|
|
258
305
|
}
|
|
259
306
|
catch (error) {
|
|
260
|
-
progress.fail(
|
|
307
|
+
progress.fail(`${progressLabel} • failed`);
|
|
261
308
|
throw error;
|
|
262
309
|
}
|
|
263
310
|
},
|
|
264
311
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path ?? '.'),
|
|
265
312
|
});
|
|
266
|
-
function buildCompletionSuffix(params) {
|
|
267
|
-
if (params.count === 0) {
|
|
268
|
-
return `No matches in ${params.scope}`;
|
|
269
|
-
}
|
|
270
|
-
const matchWord = params.count === 1 ? 'match' : 'matches';
|
|
271
|
-
const fileWord = params.filesMatched === 1 ? 'file' : 'files';
|
|
272
|
-
const reasonLabels = {
|
|
273
|
-
timeout: 'timeout',
|
|
274
|
-
maxResults: 'max results',
|
|
275
|
-
maxFiles: 'max files',
|
|
276
|
-
};
|
|
277
|
-
const reasonSuffix = params.stoppedReason !== undefined
|
|
278
|
-
? ` [${reasonLabels[params.stoppedReason]}]`
|
|
279
|
-
: '';
|
|
280
|
-
return `${params.count} ${matchWord} in ${params.filesMatched} ${fileWord}${reasonSuffix}`;
|
|
281
|
-
}
|
|
282
313
|
const { isInitialized } = options;
|
|
283
314
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
284
315
|
guard: isInitialized,
|
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,
|
|
@@ -440,13 +471,14 @@ export function decodeOffsetCursor(cursor) {
|
|
|
440
471
|
parsed !== null &&
|
|
441
472
|
typeof parsed.offset === 'number') {
|
|
442
473
|
const { offset } = parsed;
|
|
443
|
-
|
|
474
|
+
if (Number.isInteger(offset) && offset >= 0)
|
|
475
|
+
return offset;
|
|
444
476
|
}
|
|
445
477
|
}
|
|
446
478
|
catch {
|
|
447
|
-
//
|
|
479
|
+
// fall through to throw
|
|
448
480
|
}
|
|
449
|
-
|
|
481
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid cursor: the cursor value is malformed or corrupted. Request the first page without a cursor.`);
|
|
450
482
|
}
|
|
451
483
|
export function buildBatchPathContext(paths, unitLabel = 'paths') {
|
|
452
484
|
const normalizedLabel = paths.length === 1 ? unitLabel.replace(/s$/i, '') : unitLabel;
|
package/dist/tools/stat-many.js
CHANGED
|
@@ -35,24 +35,18 @@ async function handleGetMultipleFileInfo(args, signal, onProgress) {
|
|
|
35
35
|
...(signal ? { signal } : {}),
|
|
36
36
|
...(onProgress ? { onProgress } : {}),
|
|
37
37
|
});
|
|
38
|
-
const structuredResults =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
textBlocks.push(formatFileInfoDetail(entry.info));
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
textBlocks.push(entry.path);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
38
|
+
const structuredResults = result.results.map((entry) => ({
|
|
39
|
+
path: entry.path,
|
|
40
|
+
info: entry.info ? buildFileInfoPayload(entry.info) : undefined,
|
|
41
|
+
error: entry.error,
|
|
42
|
+
}));
|
|
43
|
+
const text = result.results
|
|
44
|
+
.map((entry) => entry.error
|
|
45
|
+
? `${entry.path}: ${entry.error}`
|
|
46
|
+
: entry.info
|
|
47
|
+
? formatFileInfoDetail(entry.info)
|
|
48
|
+
: entry.path)
|
|
49
|
+
.join('\n\n');
|
|
56
50
|
const structured = {
|
|
57
51
|
ok: true,
|
|
58
52
|
results: structuredResults,
|
|
@@ -62,7 +56,6 @@ async function handleGetMultipleFileInfo(args, signal, onProgress) {
|
|
|
62
56
|
failed: result.summary.failed,
|
|
63
57
|
},
|
|
64
58
|
};
|
|
65
|
-
const text = textBlocks.join('\n\n');
|
|
66
59
|
return buildToolResponse(text, structured);
|
|
67
60
|
}
|
|
68
61
|
export function registerGetMultipleFileInfoTool(server, options = {}) {
|
package/dist/tools/stat.js
CHANGED
|
@@ -14,7 +14,7 @@ export const GET_FILE_INFO_TOOL = {
|
|
|
14
14
|
inputSchema: GetFileInfoInputSchema,
|
|
15
15
|
outputSchema: GetFileInfoOutputSchema,
|
|
16
16
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
17
|
-
taskSupport: '
|
|
17
|
+
taskSupport: 'forbidden',
|
|
18
18
|
nuances: ['Use before read/search when file size/type uncertainty exists.'],
|
|
19
19
|
};
|
|
20
20
|
function formatFileInfoDetails(info) {
|
|
@@ -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;
|