@j0hanz/filesystem-mcp 1.13.2 → 1.14.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/README.md +117 -100
- package/dist/config.d.ts +0 -1
- package/dist/lib/errors.js +5 -2
- package/dist/lib/file-operations/metadata.js +9 -3
- package/dist/lib/file-operations/search.d.ts +0 -1
- package/dist/lib/file-operations/search.js +5 -12
- package/dist/lib/fs-helpers.js +10 -11
- package/dist/lib/globs.d.ts +2 -0
- package/dist/lib/globs.js +19 -0
- 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 +3 -12
- package/dist/resources/tool-catalog.js +10 -41
- package/dist/resources/tool-info.d.ts +0 -1
- package/dist/resources/tool-info.js +11 -39
- package/dist/resources/workflows.js +8 -1
- package/dist/schemas.d.ts +179 -459
- package/dist/schemas.js +156 -165
- package/dist/server/roots-manager.js +1 -1
- package/dist/tools/apply-patch.js +19 -8
- package/dist/tools/calculate-hash.js +3 -5
- package/dist/tools/create-directory.js +1 -1
- package/dist/tools/delete-file.js +2 -4
- package/dist/tools/diff-files.js +1 -3
- package/dist/tools/edit-file.js +5 -2
- package/dist/tools/list-directory.js +10 -15
- package/dist/tools/move-file.js +14 -26
- package/dist/tools/read-multiple.js +12 -7
- package/dist/tools/read.js +1 -2
- package/dist/tools/replace-in-files.js +58 -94
- package/dist/tools/roots.js +2 -6
- package/dist/tools/search-content.js +150 -186
- package/dist/tools/search-files.js +5 -9
- package/dist/tools/shared.d.ts +7 -0
- package/dist/tools/shared.js +38 -11
- package/dist/tools/stat-many.js +6 -4
- package/dist/tools/stat.js +2 -2
- package/dist/tools/tree.js +1 -1
- package/dist/tools/write-file.js +1 -5
- package/package.json +2 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
|
-
import { performance } from 'node:perf_hooks';
|
|
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';
|
|
@@ -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,15 +50,6 @@ function buildCompletionSuffix(count, filesMatched, scope, stoppedReason) {
|
|
|
53
50
|
: '';
|
|
54
51
|
return `${count} ${matchWord} in ${filesMatched} ${fileWord}${reasonSuffix}`;
|
|
55
52
|
}
|
|
56
|
-
function createSearchMetricNames() {
|
|
57
|
-
searchMetricSequence += 1;
|
|
58
|
-
const metricSuffix = `${Date.now()}_${searchMetricSequence}`;
|
|
59
|
-
return {
|
|
60
|
-
timerStartName: `searchContentStart_${metricSuffix}`,
|
|
61
|
-
timerEndName: `searchContentEnd_${metricSuffix}`,
|
|
62
|
-
metricName: `searchContent_${metricSuffix}`,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
53
|
function compareNormalizedMatches(left, right) {
|
|
66
54
|
const fileCompare = left.relativeFile.localeCompare(right.relativeFile);
|
|
67
55
|
if (fileCompare !== 0)
|
|
@@ -80,7 +68,7 @@ function buildSearchPreviewState(matches, payloads) {
|
|
|
80
68
|
needsExternalize,
|
|
81
69
|
visibleMatches,
|
|
82
70
|
visiblePayloads: payloads.slice(0, visibleCount),
|
|
83
|
-
heading:
|
|
71
|
+
heading: buildHeading(matches.length, visibleMatches.length),
|
|
84
72
|
};
|
|
85
73
|
}
|
|
86
74
|
export const SEARCH_CONTENT_TOOL = {
|
|
@@ -94,187 +82,164 @@ export const SEARCH_CONTENT_TOOL = {
|
|
|
94
82
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
95
83
|
nuances: [
|
|
96
84
|
'Inline results capped at 50 matches; full results via `resourceUri`.',
|
|
97
|
-
'Skips binary and oversized files.',
|
|
98
85
|
],
|
|
99
86
|
gotchas: [
|
|
100
87
|
'Skips binary/oversized files silently — verify with `stat` if no matches.',
|
|
101
88
|
],
|
|
102
89
|
taskSupport: 'optional',
|
|
103
90
|
};
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
91
|
+
function buildHeading(totalMatches, visibleMatches) {
|
|
92
|
+
if (visibleMatches >= totalMatches) {
|
|
93
|
+
return `Found ${totalMatches}:`;
|
|
94
|
+
}
|
|
95
|
+
return `Found ${totalMatches} (showing first ${visibleMatches}):`;
|
|
96
|
+
}
|
|
97
|
+
function buildSearchText(heading, matches, summary) {
|
|
98
|
+
if (matches.length === 0)
|
|
99
|
+
return 'No matches';
|
|
100
|
+
const text = buildMatchList(heading, matches);
|
|
101
|
+
if (!summary)
|
|
102
|
+
return text;
|
|
103
|
+
const summaryOpts = {
|
|
104
|
+
truncated: summary.truncated,
|
|
105
|
+
...(summary.truncated
|
|
106
|
+
? { truncatedReason: resolveTruncatedReason(summary) }
|
|
107
|
+
: {}),
|
|
108
|
+
};
|
|
109
|
+
return text + formatOperationSummary(summaryOpts);
|
|
110
|
+
}
|
|
111
|
+
function buildSearchStructured(summary, matches) {
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
matches,
|
|
115
|
+
totalMatches: summary.matches,
|
|
116
|
+
filesScanned: summary.filesScanned,
|
|
117
|
+
...buildStructuredSummaryFields(summary),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function normalizeMatches(result) {
|
|
121
|
+
const relativeByFile = new Map();
|
|
122
|
+
const getRelativeFile = (file) => {
|
|
123
|
+
const cached = relativeByFile.get(file);
|
|
124
|
+
if (cached !== undefined)
|
|
125
|
+
return cached;
|
|
126
|
+
const relative = path.relative(result.basePath, file);
|
|
127
|
+
relativeByFile.set(file, relative);
|
|
128
|
+
return relative;
|
|
129
|
+
};
|
|
130
|
+
return result.matches
|
|
131
|
+
.map((match, index) => ({
|
|
132
|
+
...match,
|
|
133
|
+
relativeFile: getRelativeFile(match.file),
|
|
134
|
+
index,
|
|
135
|
+
}))
|
|
136
|
+
.sort(compareNormalizedMatches);
|
|
137
|
+
}
|
|
138
|
+
function buildMatchPayloads(matches, context) {
|
|
139
|
+
return matches.map((match) => {
|
|
140
|
+
const column = findColumnOffset(match.content, context);
|
|
129
141
|
return {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
normalizeMatches(result) {
|
|
140
|
-
const relativeByFile = new Map();
|
|
141
|
-
const getRelativeFile = (file) => {
|
|
142
|
-
const cached = relativeByFile.get(file);
|
|
143
|
-
if (cached !== undefined)
|
|
144
|
-
return cached;
|
|
145
|
-
const relative = path.relative(result.basePath, file);
|
|
146
|
-
relativeByFile.set(file, relative);
|
|
147
|
-
return relative;
|
|
142
|
+
file: match.relativeFile,
|
|
143
|
+
line: match.line,
|
|
144
|
+
...(column !== undefined ? { column } : {}),
|
|
145
|
+
content: match.content,
|
|
146
|
+
matchCount: match.matchCount,
|
|
147
|
+
...(match.contextBefore
|
|
148
|
+
? { contextBefore: [...match.contextBefore] }
|
|
149
|
+
: {}),
|
|
150
|
+
...(match.contextAfter ? { contextAfter: [...match.contextAfter] } : {}),
|
|
148
151
|
};
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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})`;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function buildMatchList(heading, matches) {
|
|
155
|
+
if (matches.length === 0)
|
|
156
|
+
return heading;
|
|
157
|
+
const parts = [heading];
|
|
158
|
+
for (const match of matches) {
|
|
159
|
+
parts.push(`\n ${match.relativeFile}:${String(match.line).padStart(4)}: ${match.content}`);
|
|
160
|
+
}
|
|
161
|
+
return parts.join('');
|
|
162
|
+
}
|
|
163
|
+
function resolveTruncatedReason(summary) {
|
|
164
|
+
if (summary.stoppedReason === 'timeout')
|
|
165
|
+
return 'timeout';
|
|
166
|
+
if (summary.stoppedReason === 'maxFiles') {
|
|
167
|
+
return `max files (${summary.filesScanned})`;
|
|
168
|
+
}
|
|
169
|
+
return `max results (${summary.matches})`;
|
|
170
|
+
}
|
|
171
|
+
function findColumnOffset(content, context) {
|
|
172
|
+
try {
|
|
173
|
+
if (context.matcher) {
|
|
174
|
+
context.matcher.lastIndex = 0;
|
|
175
|
+
const match = context.matcher.exec(content);
|
|
176
|
+
return match ? match.index : undefined;
|
|
189
177
|
}
|
|
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 ?? '');
|
|
178
|
+
if (context.caseSensitive) {
|
|
179
|
+
const idx = content.indexOf(context.pattern);
|
|
205
180
|
return idx >= 0 ? idx : undefined;
|
|
206
181
|
}
|
|
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)}`);
|
|
182
|
+
// Case-insensitive literal search
|
|
183
|
+
const idx = content.toLowerCase().indexOf(context.foldedPattern ?? '');
|
|
184
|
+
return idx >= 0 ? idx : undefined;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function executeSearch(args, basePath, signal, onProgress) {
|
|
191
|
+
const excludePatterns = args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS;
|
|
192
|
+
const options = {
|
|
193
|
+
includeHidden: args.includeHidden,
|
|
194
|
+
excludePatterns,
|
|
195
|
+
filePattern: args.filePattern,
|
|
196
|
+
caseSensitive: args.caseSensitive,
|
|
197
|
+
wholeWord: args.wholeWord,
|
|
198
|
+
contextLines: args.contextLines,
|
|
199
|
+
maxResults: args.maxResults,
|
|
200
|
+
isLiteral: !args.isRegex,
|
|
201
|
+
...(signal ? { signal } : {}),
|
|
202
|
+
...(onProgress ? { onProgress } : {}),
|
|
203
|
+
};
|
|
204
|
+
try {
|
|
205
|
+
return await searchContent(basePath, args.pattern, options);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error instanceof Error && /regular expression/i.test(error.message)) {
|
|
209
|
+
throw new McpError(ErrorCode.E_INVALID_PATTERN, error.message);
|
|
256
210
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
};
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function createSearchMatcher(args) {
|
|
215
|
+
if (!args.isRegex)
|
|
216
|
+
return undefined;
|
|
217
|
+
try {
|
|
218
|
+
const flags = args.caseSensitive ? '' : 'i';
|
|
219
|
+
return new RE2(args.pattern, flags);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
throw new McpError(ErrorCode.E_INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function createSearchContext(args, matcher) {
|
|
226
|
+
return {
|
|
227
|
+
pattern: args.pattern,
|
|
228
|
+
caseSensitive: args.caseSensitive,
|
|
229
|
+
...(matcher ? { matcher } : {}),
|
|
230
|
+
...(!args.isRegex && !args.caseSensitive
|
|
231
|
+
? { foldedPattern: args.pattern.toLowerCase() }
|
|
232
|
+
: {}),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
269
235
|
async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
270
236
|
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 });
|
|
237
|
+
const regexMatcher = createSearchMatcher(args);
|
|
238
|
+
const result = await executeSearch(args, basePath, signal, onProgress);
|
|
239
|
+
const normalizedMatches = normalizeMatches(result);
|
|
240
|
+
const searchContext = createSearchContext(args, regexMatcher);
|
|
241
|
+
const matchPayloads = buildMatchPayloads(normalizedMatches, searchContext);
|
|
242
|
+
const fullStructured = buildSearchStructured(result.summary, matchPayloads);
|
|
278
243
|
const preview = buildSearchPreviewState(normalizedMatches, matchPayloads);
|
|
279
244
|
if (resourceStore && preview.needsExternalize) {
|
|
280
245
|
const previewStructured = {
|
|
@@ -288,7 +253,7 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
288
253
|
text: JSON.stringify(fullStructured),
|
|
289
254
|
});
|
|
290
255
|
previewStructured.resourceUri = entry.uri;
|
|
291
|
-
const text =
|
|
256
|
+
const text = buildSearchText(preview.heading, preview.visibleMatches);
|
|
292
257
|
return buildToolResponse(text, previewStructured, [
|
|
293
258
|
buildResourceLink({
|
|
294
259
|
uri: entry.uri,
|
|
@@ -299,13 +264,14 @@ async function handleSearchContent(args, signal, resourceStore, onProgress) {
|
|
|
299
264
|
}),
|
|
300
265
|
]);
|
|
301
266
|
}
|
|
302
|
-
const text =
|
|
267
|
+
const text = buildSearchText(preview.heading, preview.visibleMatches, result.summary);
|
|
303
268
|
return buildToolResponse(text, fullStructured);
|
|
304
269
|
}
|
|
305
270
|
export function registerSearchContentTool(server, options = {}) {
|
|
306
271
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
307
272
|
toolName: 'grep',
|
|
308
273
|
extra,
|
|
274
|
+
outputSchema: SearchContentOutputSchema,
|
|
309
275
|
context: { path: args.path ?? '.' },
|
|
310
276
|
run: async (signal) => {
|
|
311
277
|
const { pattern, filePattern: scope } = args;
|
|
@@ -321,10 +287,8 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
321
287
|
try {
|
|
322
288
|
const result = await handleSearchContent(args, signal, options.resourceStore, progressWithMessage);
|
|
323
289
|
const sc = result.structuredContent;
|
|
324
|
-
const
|
|
325
|
-
const
|
|
326
|
-
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
327
|
-
const suffix = buildCompletionSuffix(count, filesMatched, scope, stoppedReason);
|
|
290
|
+
const { totalMatches = 0, filesMatched = 0, stoppedReason } = sc;
|
|
291
|
+
const suffix = buildCompletionSuffix(totalMatches, filesMatched, scope, stoppedReason);
|
|
328
292
|
const finalCurrent = resolveFinalProgressCurrent(progress, (sc.filesScanned ?? 0) + 1);
|
|
329
293
|
progress.complete(`${progressLabel} • ${suffix}`, finalCurrent);
|
|
330
294
|
return result;
|
|
@@ -14,10 +14,7 @@ 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
|
};
|
|
23
20
|
async function handleSearchFiles(args, signal, onProgress) {
|
|
@@ -52,7 +49,6 @@ async function handleSearchFiles(args, signal, onProgress) {
|
|
|
52
49
|
const structured = {
|
|
53
50
|
ok: true,
|
|
54
51
|
root: basePath,
|
|
55
|
-
pattern: args.pattern,
|
|
56
52
|
results: relativeResults,
|
|
57
53
|
totalMatches: result.summary.matched,
|
|
58
54
|
filesScanned: result.summary.filesScanned,
|
|
@@ -103,6 +99,7 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
103
99
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
104
100
|
toolName: 'find',
|
|
105
101
|
extra,
|
|
102
|
+
outputSchema: SearchFilesOutputSchema,
|
|
106
103
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
107
104
|
context: { path: args.path ?? '.' },
|
|
108
105
|
run: async (signal) => {
|
|
@@ -129,14 +126,13 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
129
126
|
try {
|
|
130
127
|
const result = await handleSearchFiles(args, signal, progressWithMessage);
|
|
131
128
|
const sc = result.structuredContent;
|
|
132
|
-
const
|
|
133
|
-
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
129
|
+
const { totalMatches = 0, stoppedReason } = sc;
|
|
134
130
|
let suffix;
|
|
135
|
-
if (
|
|
131
|
+
if (totalMatches === 0) {
|
|
136
132
|
suffix = 'No matches';
|
|
137
133
|
}
|
|
138
134
|
else {
|
|
139
|
-
suffix = `${
|
|
135
|
+
suffix = `${totalMatches} ${totalMatches === 1 ? 'match' : 'matches'}`;
|
|
140
136
|
if (stoppedReason === 'timeout') {
|
|
141
137
|
suffix += ' [timeout]';
|
|
142
138
|
}
|
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;
|
|
@@ -94,6 +100,7 @@ export declare function buildFileInfoPayload(info: FileInfo): FileInfoPayload;
|
|
|
94
100
|
interface ToolExecutionOptions<T> {
|
|
95
101
|
toolName: string;
|
|
96
102
|
extra: ToolExtra;
|
|
103
|
+
outputSchema?: z.ZodType<T>;
|
|
97
104
|
run: (signal: AbortSignal | undefined) => ToolResponse<T> | Promise<ToolResponse<T>>;
|
|
98
105
|
onError: (error: unknown) => ToolResult<T>;
|
|
99
106
|
context?: Record<string, unknown>;
|
package/dist/tools/shared.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createDetailedError, ErrorCode, formatDetailedError, getSuggestion, Mcp
|
|
|
6
6
|
import { createTimedAbortSignal } from '../lib/fs-helpers.js';
|
|
7
7
|
import { withToolDiagnostics } from '../lib/observability.js';
|
|
8
8
|
import { getAllowedDirectories } from '../lib/paths.js';
|
|
9
|
+
import { createBase64JsonCodec } from '../lib/zod-codecs.js';
|
|
9
10
|
export {} from './contract.js';
|
|
10
11
|
const MAX_INLINE_CONTENT_CHARS = parseInt(process.env['FS_CONTEXT_MAX_INLINE_CHARS'] ?? '', 10) || 20_000;
|
|
11
12
|
const MAX_INLINE_PREVIEW_CHARS = 4_000;
|
|
@@ -133,19 +134,48 @@ function resolveDetailedError(error, defaultCode, path) {
|
|
|
133
134
|
}
|
|
134
135
|
return detailed;
|
|
135
136
|
}
|
|
137
|
+
export function buildStructuredError(error, defaultCode, path) {
|
|
138
|
+
const detailed = resolveDetailedError(error, defaultCode, path);
|
|
139
|
+
return {
|
|
140
|
+
code: detailed.code,
|
|
141
|
+
message: detailed.message,
|
|
142
|
+
...(detailed.path !== undefined ? { path: detailed.path } : {}),
|
|
143
|
+
...(detailed.suggestion !== undefined
|
|
144
|
+
? { suggestion: detailed.suggestion }
|
|
145
|
+
: {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
136
148
|
export function buildToolResponse(text, structuredContent, extraContent = []) {
|
|
137
149
|
return {
|
|
138
150
|
content: [{ type: 'text', text }, ...extraContent],
|
|
139
151
|
structuredContent,
|
|
140
152
|
};
|
|
141
153
|
}
|
|
154
|
+
function validateStructuredContent(toolName, outputSchema, structuredContent) {
|
|
155
|
+
const parsed = outputSchema.safeParse(structuredContent);
|
|
156
|
+
if (parsed.success) {
|
|
157
|
+
return parsed.data;
|
|
158
|
+
}
|
|
159
|
+
throw new McpError(ErrorCode.E_UNKNOWN, `Tool "${toolName}" returned invalid structuredContent.`, undefined, { errors: z.treeifyError(parsed.error) });
|
|
160
|
+
}
|
|
161
|
+
function validateToolResponse(toolName, result, outputSchema) {
|
|
162
|
+
if (!outputSchema)
|
|
163
|
+
return result;
|
|
164
|
+
if (!Object.hasOwn(result, 'structuredContent')) {
|
|
165
|
+
throw new McpError(ErrorCode.E_UNKNOWN, `Tool "${toolName}" returned success without structuredContent.`);
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
...result,
|
|
169
|
+
structuredContent: validateStructuredContent(toolName, outputSchema, result.structuredContent),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
142
172
|
function parseToolArgs(schema, args) {
|
|
143
173
|
const candidate = args === undefined ? {} : args;
|
|
144
174
|
const parsed = schema.safeParse(candidate);
|
|
145
175
|
if (parsed.success) {
|
|
146
176
|
return parsed.data;
|
|
147
177
|
}
|
|
148
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid tool arguments
|
|
178
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid tool arguments:\n${z.prettifyError(parsed.error)}`, undefined, { errors: z.treeifyError(parsed.error) });
|
|
149
179
|
}
|
|
150
180
|
export function withValidatedArgs(schema, handler) {
|
|
151
181
|
return async (args, extra) => {
|
|
@@ -222,7 +252,7 @@ export async function executeToolWithDiagnostics(options) {
|
|
|
222
252
|
return withToolDiagnostics(options.toolName, () => withToolErrorHandling(async () => {
|
|
223
253
|
const { signal, cleanup } = getToolSignal(options.extra.signal, options.timedSignal);
|
|
224
254
|
try {
|
|
225
|
-
return await options.run(signal);
|
|
255
|
+
return validateToolResponse(options.toolName, await options.run(signal), options.outputSchema);
|
|
226
256
|
}
|
|
227
257
|
finally {
|
|
228
258
|
cleanup();
|
|
@@ -461,19 +491,16 @@ export function resolvePathOrRoot(pathValue) {
|
|
|
461
491
|
}
|
|
462
492
|
return root;
|
|
463
493
|
}
|
|
494
|
+
const OffsetCursorSchema = z.strictObject({
|
|
495
|
+
offset: z.int().min(0),
|
|
496
|
+
});
|
|
497
|
+
const OffsetCursorCodec = createBase64JsonCodec(OffsetCursorSchema);
|
|
464
498
|
export function encodeOffsetCursor(offset) {
|
|
465
|
-
return
|
|
499
|
+
return z.encode(OffsetCursorCodec, { offset });
|
|
466
500
|
}
|
|
467
501
|
export function decodeOffsetCursor(cursor) {
|
|
468
502
|
try {
|
|
469
|
-
|
|
470
|
-
if (typeof parsed === 'object' &&
|
|
471
|
-
parsed !== null &&
|
|
472
|
-
typeof parsed.offset === 'number') {
|
|
473
|
-
const { offset } = parsed;
|
|
474
|
-
if (Number.isInteger(offset) && offset >= 0)
|
|
475
|
-
return offset;
|
|
476
|
-
}
|
|
503
|
+
return OffsetCursorCodec.parse(cursor).offset;
|
|
477
504
|
}
|
|
478
505
|
catch {
|
|
479
506
|
// fall through to throw
|
package/dist/tools/stat-many.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ErrorCode } from '../lib/errors.js';
|
|
|
3
3
|
import { getMultipleFileInfo } from '../lib/file-operations/metadata.js';
|
|
4
4
|
import { formatBytes, joinLines } from '../config.js';
|
|
5
5
|
import { GetMultipleFileInfoInputSchema, GetMultipleFileInfoOutputSchema, } from '../schemas.js';
|
|
6
|
-
import { buildBatchCompletionSuffix, buildBatchPathContext, buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
6
|
+
import { buildBatchCompletionSuffix, buildBatchPathContext, buildFileInfoPayload, buildStructuredError, buildToolErrorResponse, buildToolResponse, createBatchProgressCallbacks, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolveFinalProgressCurrent, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
export const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
9
9
|
name: 'stat_many',
|
|
@@ -14,7 +14,6 @@ export const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
|
14
14
|
outputSchema: GetMultipleFileInfoOutputSchema,
|
|
15
15
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
16
16
|
taskSupport: 'optional',
|
|
17
|
-
nuances: ['Use before read/search when file size/type uncertainty exists.'],
|
|
18
17
|
};
|
|
19
18
|
function formatFileInfoDetail(info) {
|
|
20
19
|
const lines = [
|
|
@@ -38,11 +37,13 @@ async function handleGetMultipleFileInfo(args, signal, onProgress) {
|
|
|
38
37
|
const structuredResults = result.results.map((entry) => ({
|
|
39
38
|
path: entry.path,
|
|
40
39
|
info: entry.info ? buildFileInfoPayload(entry.info) : undefined,
|
|
41
|
-
error: entry.error
|
|
40
|
+
error: entry.error
|
|
41
|
+
? buildStructuredError(entry.error, ErrorCode.E_NOT_FOUND, entry.path)
|
|
42
|
+
: undefined,
|
|
42
43
|
}));
|
|
43
44
|
const text = result.results
|
|
44
45
|
.map((entry) => entry.error
|
|
45
|
-
? `${entry.path}: ${entry.error}`
|
|
46
|
+
? `${entry.path}: ${buildStructuredError(entry.error, ErrorCode.E_NOT_FOUND, entry.path).message}`
|
|
46
47
|
: entry.info
|
|
47
48
|
? formatFileInfoDetail(entry.info)
|
|
48
49
|
: entry.path)
|
|
@@ -64,6 +65,7 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
64
65
|
return executeToolWithDiagnostics({
|
|
65
66
|
toolName: 'stat_many',
|
|
66
67
|
extra,
|
|
68
|
+
outputSchema: GetMultipleFileInfoOutputSchema,
|
|
67
69
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
68
70
|
context: { path: primaryPath },
|
|
69
71
|
run: async (signal) => {
|
package/dist/tools/stat.js
CHANGED
|
@@ -15,7 +15,6 @@ export const GET_FILE_INFO_TOOL = {
|
|
|
15
15
|
outputSchema: GetFileInfoOutputSchema,
|
|
16
16
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
17
17
|
taskSupport: 'forbidden',
|
|
18
|
-
nuances: ['Use before read/search when file size/type uncertainty exists.'],
|
|
19
18
|
};
|
|
20
19
|
function formatFileInfoDetails(info) {
|
|
21
20
|
const lines = [
|
|
@@ -45,6 +44,7 @@ export function registerGetFileInfoTool(server, options = {}) {
|
|
|
45
44
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
46
45
|
toolName: 'stat',
|
|
47
46
|
extra,
|
|
47
|
+
outputSchema: GetFileInfoOutputSchema,
|
|
48
48
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
49
49
|
context: { path: args.path },
|
|
50
50
|
run: (signal) => handleGetFileInfo(args, signal),
|
|
@@ -58,7 +58,7 @@ export function registerGetFileInfoTool(server, options = {}) {
|
|
|
58
58
|
if (result.isError)
|
|
59
59
|
return `🕮 stat: ${name} • failed`;
|
|
60
60
|
const sc = result.structuredContent;
|
|
61
|
-
if (!sc.
|
|
61
|
+
if (!sc.info)
|
|
62
62
|
return `🕮 stat: ${name} • failed`;
|
|
63
63
|
return `🕮 stat: ${sc.info.name} • ${sc.info.type}, ${formatBytes(sc.info.size)}`;
|
|
64
64
|
},
|
package/dist/tools/tree.js
CHANGED
|
@@ -14,7 +14,6 @@ export const TREE_TOOL = {
|
|
|
14
14
|
outputSchema: TreeOutputSchema,
|
|
15
15
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
16
16
|
taskSupport: 'optional',
|
|
17
|
-
gotchas: ['`maxDepth=0` returns only the root node.'],
|
|
18
17
|
};
|
|
19
18
|
async function handleTree(args, signal, onProgress) {
|
|
20
19
|
const basePath = resolvePathOrRoot(args.path);
|
|
@@ -45,6 +44,7 @@ export function registerTreeTool(server, options = {}) {
|
|
|
45
44
|
return executeToolWithDiagnostics({
|
|
46
45
|
toolName: 'tree',
|
|
47
46
|
extra,
|
|
47
|
+
outputSchema: TreeOutputSchema,
|
|
48
48
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
49
49
|
context: { path: targetPath },
|
|
50
50
|
run: async (signal) => {
|