@j0hanz/filesystem-mcp 1.6.1 → 1.7.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 +19 -17
- package/dist/lib/errors.js +20 -16
- package/dist/lib/file-operations/glob-engine.js +37 -20
- package/dist/lib/fs-helpers.js +7 -8
- package/dist/lib/path-policy.js +30 -14
- package/dist/prompts.js +1 -1
- package/dist/resources/generated-instructions.js +23 -22
- package/dist/resources/tool-catalog.js +2 -2
- package/dist/resources/tool-info.js +1 -1
- package/dist/resources/workflows.js +2 -3
- package/dist/resources.js +3 -3
- package/dist/server/bootstrap.js +1 -6
- package/dist/server/capabilities.js +4 -1
- package/dist/server/roots-manager.d.ts +2 -0
- package/dist/server/roots-manager.js +24 -6
- package/dist/tools/calculate-hash.js +6 -22
- package/dist/tools/read-multiple.js +34 -43
- package/dist/tools/replace-in-files.js +52 -56
- package/dist/tools/search-content.js +6 -27
- package/dist/tools/shared.d.ts +22 -0
- package/dist/tools/shared.js +69 -8
- package/dist/tools/stat-many.js +6 -24
- package/dist/tools/task-support.js +19 -0
- package/package.json +1 -3
|
@@ -9,7 +9,7 @@ import { globEntries } from '../lib/file-operations/glob-engine.js';
|
|
|
9
9
|
import { assertNotAborted, withAbort } from '../lib/fs-helpers.js';
|
|
10
10
|
import { validateExistingPath } from '../lib/path-validation.js';
|
|
11
11
|
import { CalculateHashInputSchema, CalculateHashOutputSchema, } from '../schemas.js';
|
|
12
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
12
|
+
import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
13
13
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
14
14
|
const WINDOWS_PATH_SEPARATOR = /\\/gu;
|
|
15
15
|
export const CALCULATE_HASH_TOOL = {
|
|
@@ -158,17 +158,10 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
158
158
|
context: { path: args.path },
|
|
159
159
|
run: async (signal) => {
|
|
160
160
|
const baseName = path.basename(args.path);
|
|
161
|
-
|
|
162
|
-
notifyProgress(extra, {
|
|
163
|
-
current: 0,
|
|
164
|
-
message: `🕮 calculate_hash: ${baseName}`,
|
|
165
|
-
});
|
|
166
|
-
const baseReporter = createProgressReporter(extra);
|
|
161
|
+
const progress = createToolProgressSession(extra, `🕮 calculate_hash: ${baseName}`);
|
|
167
162
|
const progressWithMessage = ({ current, total, }) => {
|
|
168
|
-
if (current > progressCursor)
|
|
169
|
-
progressCursor = current;
|
|
170
163
|
const fileWord = current === 1 ? 'file' : 'files';
|
|
171
|
-
|
|
164
|
+
progress.update({
|
|
172
165
|
current,
|
|
173
166
|
...(total !== undefined ? { total } : {}),
|
|
174
167
|
message: `🕮 calculate_hash: ${baseName} [${current} ${fileWord} hashed]`,
|
|
@@ -178,7 +171,7 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
178
171
|
const result = await handleCalculateHash(args, signal, progressWithMessage);
|
|
179
172
|
const sc = result.structuredContent;
|
|
180
173
|
const totalFiles = sc.ok ? (sc.fileCount ?? 1) : 1;
|
|
181
|
-
const finalCurrent = Math.max(totalFiles + 1,
|
|
174
|
+
const finalCurrent = Math.max(totalFiles + 1, progress.getCurrent() + 1);
|
|
182
175
|
let suffix;
|
|
183
176
|
if (!sc.ok) {
|
|
184
177
|
suffix = 'failed';
|
|
@@ -189,20 +182,11 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
189
182
|
else {
|
|
190
183
|
suffix = `${(sc.hash ?? '').slice(0, 8)}...`;
|
|
191
184
|
}
|
|
192
|
-
|
|
193
|
-
current: finalCurrent,
|
|
194
|
-
total: finalCurrent,
|
|
195
|
-
message: `🕮 calculate_hash: ${baseName} • ${suffix}`,
|
|
196
|
-
});
|
|
185
|
+
progress.complete(`🕮 calculate_hash: ${baseName} • ${suffix}`, finalCurrent);
|
|
197
186
|
return result;
|
|
198
187
|
}
|
|
199
188
|
catch (error) {
|
|
200
|
-
|
|
201
|
-
notifyProgress(extra, {
|
|
202
|
-
current: finalCurrent,
|
|
203
|
-
total: finalCurrent,
|
|
204
|
-
message: `🕮 calculate_hash: ${baseName} • failed`,
|
|
205
|
-
});
|
|
189
|
+
progress.fail(`🕮 calculate_hash: ${baseName} • failed`);
|
|
206
190
|
throw error;
|
|
207
191
|
}
|
|
208
192
|
},
|
|
@@ -3,7 +3,7 @@ import { DEFAULT_READ_MANY_MAX_TOTAL_SIZE, DEFAULT_SEARCH_TIMEOUT_MS, } from '..
|
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { readMultipleFiles } from '../lib/file-operations/read-multiple-files.js';
|
|
5
5
|
import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
|
|
6
|
-
import { buildResourceLink, buildToolErrorResponse, buildToolResponse,
|
|
6
|
+
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
export const READ_MULTIPLE_FILES_TOOL = {
|
|
9
9
|
name: 'read_many',
|
|
@@ -20,6 +20,33 @@ export const READ_MULTIPLE_FILES_TOOL = {
|
|
|
20
20
|
'Per-file `truncationReason` can be `head`, `range`, or `externalized`.',
|
|
21
21
|
],
|
|
22
22
|
};
|
|
23
|
+
function toStructuredReadManyResult(result) {
|
|
24
|
+
const structured = {
|
|
25
|
+
path: result.path,
|
|
26
|
+
};
|
|
27
|
+
if (result.content !== undefined)
|
|
28
|
+
structured.content = result.content;
|
|
29
|
+
if (result.truncated)
|
|
30
|
+
structured.truncated = result.truncated;
|
|
31
|
+
if (result.resourceUri)
|
|
32
|
+
structured.resourceUri = result.resourceUri;
|
|
33
|
+
if (result.head !== undefined)
|
|
34
|
+
structured.head = result.head;
|
|
35
|
+
if (result.startLine !== undefined)
|
|
36
|
+
structured.startLine = result.startLine;
|
|
37
|
+
if (result.endLine !== undefined)
|
|
38
|
+
structured.endLine = result.endLine;
|
|
39
|
+
if (result.hasMoreLines)
|
|
40
|
+
structured.hasMoreLines = result.hasMoreLines;
|
|
41
|
+
if (result.totalLines !== undefined)
|
|
42
|
+
structured.totalLines = result.totalLines;
|
|
43
|
+
if (result.truncationReason) {
|
|
44
|
+
structured.truncationReason = result.truncationReason;
|
|
45
|
+
}
|
|
46
|
+
if (result.error)
|
|
47
|
+
structured.error = result.error;
|
|
48
|
+
return structured;
|
|
49
|
+
}
|
|
23
50
|
async function handleReadMultipleFiles(args, signal, resourceStore, onReadComplete) {
|
|
24
51
|
const options = {
|
|
25
52
|
...(signal ? { signal } : {}),
|
|
@@ -70,25 +97,7 @@ async function handleReadMultipleFiles(args, signal, resourceStore, onReadComple
|
|
|
70
97
|
}
|
|
71
98
|
const structured = {
|
|
72
99
|
ok: true,
|
|
73
|
-
results: mappedResults.map((result) => (
|
|
74
|
-
path: result.path,
|
|
75
|
-
...(result.content !== undefined ? { content: result.content } : {}),
|
|
76
|
-
...(result.truncated ? { truncated: result.truncated } : {}),
|
|
77
|
-
...(result.resourceUri ? { resourceUri: result.resourceUri } : {}),
|
|
78
|
-
...(result.head !== undefined ? { head: result.head } : {}),
|
|
79
|
-
...(result.startLine !== undefined
|
|
80
|
-
? { startLine: result.startLine }
|
|
81
|
-
: {}),
|
|
82
|
-
...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
|
|
83
|
-
...(result.hasMoreLines ? { hasMoreLines: result.hasMoreLines } : {}),
|
|
84
|
-
...(result.totalLines !== undefined
|
|
85
|
-
? { totalLines: result.totalLines }
|
|
86
|
-
: {}),
|
|
87
|
-
...(result.truncationReason
|
|
88
|
-
? { truncationReason: result.truncationReason }
|
|
89
|
-
: {}),
|
|
90
|
-
...(result.error ? { error: result.error } : {}),
|
|
91
|
-
})),
|
|
100
|
+
results: mappedResults.map((result) => toStructuredReadManyResult(result)),
|
|
92
101
|
summary: {
|
|
93
102
|
total: mappedResults.length,
|
|
94
103
|
succeeded,
|
|
@@ -130,18 +139,9 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
130
139
|
? `, ${path.basename(args.paths[1] ?? '')}${args.paths.length > 2 ? '…' : ''}`
|
|
131
140
|
: '';
|
|
132
141
|
const context = `${args.paths.length} files [${first}${extraPaths}]`;
|
|
133
|
-
|
|
134
|
-
notifyProgress(extra, {
|
|
135
|
-
current: 0,
|
|
136
|
-
message: `🕮 read_many: ${context}`,
|
|
137
|
-
});
|
|
138
|
-
const baseReporter = createProgressReporter(extra);
|
|
142
|
+
const progress = createToolProgressSession(extra, `🕮 read_many: ${context}`);
|
|
139
143
|
const onReadComplete = () => {
|
|
140
|
-
|
|
141
|
-
baseReporter({
|
|
142
|
-
current: progressCursor,
|
|
143
|
-
message: `🕮 read_many: ${context} [${progressCursor}/${args.paths.length} read]`,
|
|
144
|
-
});
|
|
144
|
+
progress.increment((current) => `🕮 read_many: ${context} [${current}/${args.paths.length} read]`);
|
|
145
145
|
};
|
|
146
146
|
try {
|
|
147
147
|
const result = await handleReadMultipleFiles(args, signal, options.resourceStore, onReadComplete);
|
|
@@ -156,21 +156,12 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
156
156
|
else {
|
|
157
157
|
suffix = `${total} files read`;
|
|
158
158
|
}
|
|
159
|
-
const finalCurrent = Math.max(total,
|
|
160
|
-
|
|
161
|
-
current: finalCurrent,
|
|
162
|
-
total: finalCurrent,
|
|
163
|
-
message: `🕮 read_many: ${context} • ${suffix}`,
|
|
164
|
-
});
|
|
159
|
+
const finalCurrent = Math.max(total, progress.getCurrent() + 1);
|
|
160
|
+
progress.complete(`🕮 read_many: ${context} • ${suffix}`, finalCurrent);
|
|
165
161
|
return result;
|
|
166
162
|
}
|
|
167
163
|
catch (error) {
|
|
168
|
-
|
|
169
|
-
notifyProgress(extra, {
|
|
170
|
-
current: finalCurrent,
|
|
171
|
-
total: finalCurrent,
|
|
172
|
-
message: `🕮 read_many: ${context} • failed`,
|
|
173
|
-
});
|
|
164
|
+
progress.fail(`🕮 read_many: ${context} • failed`);
|
|
174
165
|
throw error;
|
|
175
166
|
}
|
|
176
167
|
},
|
|
@@ -9,7 +9,7 @@ import { globEntries } from '../lib/file-operations/glob-engine.js';
|
|
|
9
9
|
import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
|
|
10
10
|
import { validateExistingPath, validatePathForWrite, } from '../lib/path-validation.js';
|
|
11
11
|
import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
|
|
12
|
-
import { buildToolErrorResponse, buildToolResponse,
|
|
12
|
+
import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
13
13
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
14
14
|
export const SEARCH_AND_REPLACE_TOOL = {
|
|
15
15
|
name: 'search_and_replace',
|
|
@@ -56,31 +56,42 @@ function createRegexMatcher(pattern) {
|
|
|
56
56
|
throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid regex pattern: ${formatUnknownErrorMessage(error)}`);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
regex.lastIndex
|
|
59
|
+
function createRegexReplacementMatcher(regex) {
|
|
60
|
+
const count = (content) => {
|
|
61
|
+
regex.lastIndex = 0;
|
|
62
|
+
let matchCount = 0;
|
|
63
|
+
while (regex.exec(content) !== null) {
|
|
64
|
+
matchCount++;
|
|
65
|
+
if (regex.lastIndex === 0) {
|
|
66
|
+
regex.lastIndex++;
|
|
67
|
+
}
|
|
66
68
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
return matchCount;
|
|
70
|
+
};
|
|
71
|
+
const replace = (content, replacement) => {
|
|
72
|
+
regex.lastIndex = 0;
|
|
73
|
+
return content.replace(regex, replacement);
|
|
74
|
+
};
|
|
75
|
+
return { count, replace };
|
|
69
76
|
}
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
function createLiteralReplacementMatcher(searchPattern) {
|
|
78
|
+
const count = (content) => {
|
|
79
|
+
let matchCount = 0;
|
|
80
|
+
let pos = content.indexOf(searchPattern);
|
|
81
|
+
const patternLength = searchPattern.length;
|
|
82
|
+
while (pos !== -1) {
|
|
83
|
+
matchCount++;
|
|
84
|
+
pos = content.indexOf(searchPattern, pos + patternLength);
|
|
85
|
+
}
|
|
86
|
+
return matchCount;
|
|
87
|
+
};
|
|
88
|
+
const replace = (content, replacement) => content.replaceAll(searchPattern, () => replacement);
|
|
89
|
+
return { count, replace };
|
|
79
90
|
}
|
|
80
91
|
function formatFileTooLargeError(filePath, size, maxFileSize) {
|
|
81
92
|
return `File too large: ${filePath} (${size} bytes > ${maxFileSize} bytes)`;
|
|
82
93
|
}
|
|
83
|
-
async function processEntry(entryPath,
|
|
94
|
+
async function processEntry(entryPath, options, replacement, matcher, maxFileSize, signal, summary) {
|
|
84
95
|
let validPath;
|
|
85
96
|
try {
|
|
86
97
|
validPath = await validatePathForWrite(entryPath, signal);
|
|
@@ -107,22 +118,13 @@ async function processEntry(entryPath, args, regex, maxFileSize, signal, summary
|
|
|
107
118
|
encoding: 'utf-8',
|
|
108
119
|
signal,
|
|
109
120
|
});
|
|
110
|
-
const matchCount =
|
|
111
|
-
? countRegexMatches(content, regex)
|
|
112
|
-
: countLiteralMatches(content, args.searchPattern);
|
|
121
|
+
const matchCount = matcher.count(content);
|
|
113
122
|
if (matchCount > 0) {
|
|
114
123
|
summary.totalMatches += matchCount;
|
|
115
124
|
summary.filesChanged++;
|
|
116
125
|
recordChangedFile(summary, validPath, matchCount);
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
regex.lastIndex = 0;
|
|
120
|
-
newContent = content.replace(regex, args.replacement);
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
newContent = content.replaceAll(args.searchPattern, () => args.replacement);
|
|
124
|
-
}
|
|
125
|
-
if ((args.dryRun || args.returnDiff) &&
|
|
126
|
+
const newContent = matcher.replace(content, replacement);
|
|
127
|
+
if ((options.dryRun || options.returnDiff) &&
|
|
126
128
|
summary.diff.length < MAX_DIFF_SIZE) {
|
|
127
129
|
const patch = createTwoFilesPatch(path.basename(validPath), path.basename(validPath), content, newContent, 'Original', 'Modified');
|
|
128
130
|
// Only append if it won't exceed the limit too much
|
|
@@ -130,7 +132,7 @@ async function processEntry(entryPath, args, regex, maxFileSize, signal, summary
|
|
|
130
132
|
summary.diff += patch;
|
|
131
133
|
}
|
|
132
134
|
}
|
|
133
|
-
if (!
|
|
135
|
+
if (!options.dryRun) {
|
|
134
136
|
await atomicWriteFile(validPath, newContent, {
|
|
135
137
|
encoding: 'utf-8',
|
|
136
138
|
signal,
|
|
@@ -195,6 +197,13 @@ function createReplacementRegex(args) {
|
|
|
195
197
|
}
|
|
196
198
|
return createRegexMatcher(args.searchPattern);
|
|
197
199
|
}
|
|
200
|
+
function createReplacementMatcher(args) {
|
|
201
|
+
const regex = createReplacementRegex(args);
|
|
202
|
+
if (regex) {
|
|
203
|
+
return createRegexReplacementMatcher(regex);
|
|
204
|
+
}
|
|
205
|
+
return createLiteralReplacementMatcher(args.searchPattern);
|
|
206
|
+
}
|
|
198
207
|
function reportReplaceProgress(onProgress, current, force = false) {
|
|
199
208
|
if (current === 0)
|
|
200
209
|
return;
|
|
@@ -205,7 +214,7 @@ function reportReplaceProgress(onProgress, current, force = false) {
|
|
|
205
214
|
export async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
206
215
|
const maxFileSize = MAX_TEXT_FILE_SIZE;
|
|
207
216
|
const root = await resolveSearchRoot(args.path, signal);
|
|
208
|
-
const
|
|
217
|
+
const matcher = createReplacementMatcher(args);
|
|
209
218
|
const entries = globEntries({
|
|
210
219
|
cwd: root,
|
|
211
220
|
pattern: args.filePattern,
|
|
@@ -226,7 +235,10 @@ export async function handleSearchAndReplace(args, signal, onProgress = () => {
|
|
|
226
235
|
summary.processedFiles++;
|
|
227
236
|
reportReplaceProgress(onProgress, summary.processedFiles);
|
|
228
237
|
},
|
|
229
|
-
runEntry: async (entryPath) => processEntry(entryPath,
|
|
238
|
+
runEntry: async (entryPath) => processEntry(entryPath, {
|
|
239
|
+
dryRun: args.dryRun,
|
|
240
|
+
returnDiff: args.returnDiff ?? false,
|
|
241
|
+
}, args.replacement, matcher, maxFileSize, signal, summary),
|
|
230
242
|
});
|
|
231
243
|
reportReplaceProgress(onProgress, summary.processedFiles, true);
|
|
232
244
|
const failureSuffix = summary.failedFiles > 0 ? ` (${summary.failedFiles} failed)` : '';
|
|
@@ -256,16 +268,9 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
256
268
|
run: async (signal) => {
|
|
257
269
|
const dryLabel = args.dryRun ? ' [dry run]' : '';
|
|
258
270
|
const context = `"${args.searchPattern}" in ${args.filePattern}${dryLabel}`;
|
|
259
|
-
|
|
260
|
-
notifyProgress(extra, {
|
|
261
|
-
current: 0,
|
|
262
|
-
message: `🛠 search_and_replace: ${context}`,
|
|
263
|
-
});
|
|
264
|
-
const baseReporter = createProgressReporter(extra);
|
|
271
|
+
const progress = createToolProgressSession(extra, `🛠 search_and_replace: ${context}`);
|
|
265
272
|
const progressWithMessage = ({ current, total, }) => {
|
|
266
|
-
|
|
267
|
-
progressCursor = current;
|
|
268
|
-
baseReporter({
|
|
273
|
+
progress.update({
|
|
269
274
|
current,
|
|
270
275
|
...(total !== undefined ? { total } : {}),
|
|
271
276
|
message: `🛠 search_and_replace: ${args.searchPattern} [${current} files processed]`,
|
|
@@ -274,7 +279,7 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
274
279
|
try {
|
|
275
280
|
const result = await handleSearchAndReplace(args, signal, progressWithMessage);
|
|
276
281
|
const sc = result.structuredContent;
|
|
277
|
-
const finalCurrent = Math.max((sc.processedFiles ?? 0) + 1,
|
|
282
|
+
const finalCurrent = Math.max((sc.processedFiles ?? 0) + 1, progress.getCurrent() + 1);
|
|
278
283
|
const matchWord = (sc.matches ?? 0) === 1 ? 'match' : 'matches';
|
|
279
284
|
const fileWord = (sc.filesChanged ?? 0) === 1 ? 'file' : 'files';
|
|
280
285
|
let endSuffix = `${sc.matches ?? 0} ${matchWord} in ${sc.filesChanged ?? 0} ${fileWord}`;
|
|
@@ -282,20 +287,11 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
282
287
|
endSuffix += `, ${sc.failedFiles} failed`;
|
|
283
288
|
if (sc.dryRun)
|
|
284
289
|
endSuffix += ' [dry run]';
|
|
285
|
-
|
|
286
|
-
current: finalCurrent,
|
|
287
|
-
total: finalCurrent,
|
|
288
|
-
message: `🛠 search_and_replace: ${context} • ${endSuffix}`,
|
|
289
|
-
});
|
|
290
|
+
progress.complete(`🛠 search_and_replace: ${context} • ${endSuffix}`, finalCurrent);
|
|
290
291
|
return result;
|
|
291
292
|
}
|
|
292
293
|
catch (error) {
|
|
293
|
-
|
|
294
|
-
notifyProgress(extra, {
|
|
295
|
-
current: finalCurrent,
|
|
296
|
-
total: finalCurrent,
|
|
297
|
-
message: `🛠 search_and_replace: ${context} • failed`,
|
|
298
|
-
});
|
|
294
|
+
progress.fail(`🛠 search_and_replace: ${context} • failed`);
|
|
299
295
|
throw error;
|
|
300
296
|
}
|
|
301
297
|
},
|
|
@@ -5,7 +5,7 @@ import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
|
|
|
5
5
|
import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
6
6
|
import { searchContent } from '../lib/file-operations/search-content.js';
|
|
7
7
|
import { SearchContentInputSchema, SearchContentOutputSchema, } from '../schemas.js';
|
|
8
|
-
import { buildResourceLink, buildToolErrorResponse, buildToolResponse,
|
|
8
|
+
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
9
9
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
10
10
|
const MAX_INLINE_MATCHES = parseInt(process.env['FS_CONTEXT_MAX_INLINE_MATCHES'] ?? '', 10) || 50;
|
|
11
11
|
export const SEARCH_CONTENT_TOOL = {
|
|
@@ -207,28 +207,16 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
207
207
|
run: async (signal) => {
|
|
208
208
|
const scope = args.filePattern;
|
|
209
209
|
const { pattern } = args;
|
|
210
|
-
|
|
211
|
-
notifyProgress(extra, {
|
|
212
|
-
current: 0,
|
|
213
|
-
message: `🔎︎ grep: ${pattern} in ${scope}`,
|
|
214
|
-
});
|
|
215
|
-
const baseReporter = createProgressReporter(extra);
|
|
210
|
+
const progress = createToolProgressSession(extra, `🔎︎ grep: ${pattern} in ${scope}`);
|
|
216
211
|
const progressWithMessage = ({ current, total, }) => {
|
|
217
|
-
if (current > progressCursor)
|
|
218
|
-
progressCursor = current;
|
|
219
212
|
const fileWord = current === 1 ? 'file' : 'files';
|
|
220
|
-
|
|
213
|
+
progress.update({
|
|
221
214
|
current,
|
|
222
215
|
...(total !== undefined ? { total } : {}),
|
|
223
216
|
message: `🔎︎ grep: ${pattern} [${current} ${fileWord} scanned]`,
|
|
224
217
|
});
|
|
225
218
|
};
|
|
226
219
|
try {
|
|
227
|
-
if (signal) {
|
|
228
|
-
signal.addEventListener('abort', () => {
|
|
229
|
-
console.error('searchContent signal aborted!');
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
220
|
const result = await handleSearchContent(args, signal, options.resourceStore, progressWithMessage);
|
|
233
221
|
const sc = result.structuredContent;
|
|
234
222
|
const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
|
|
@@ -254,21 +242,12 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
254
242
|
suffix += ' [truncated — max files]';
|
|
255
243
|
}
|
|
256
244
|
}
|
|
257
|
-
const finalCurrent = Math.max((sc.filesScanned ?? 0) + 1,
|
|
258
|
-
|
|
259
|
-
current: finalCurrent,
|
|
260
|
-
total: finalCurrent,
|
|
261
|
-
message: `🔎︎ grep: ${pattern} • ${suffix}`,
|
|
262
|
-
});
|
|
245
|
+
const finalCurrent = Math.max((sc.filesScanned ?? 0) + 1, progress.getCurrent() + 1);
|
|
246
|
+
progress.complete(`🔎︎ grep: ${pattern} • ${suffix}`, finalCurrent);
|
|
263
247
|
return result;
|
|
264
248
|
}
|
|
265
249
|
catch (error) {
|
|
266
|
-
|
|
267
|
-
notifyProgress(extra, {
|
|
268
|
-
current: finalCurrent,
|
|
269
|
-
total: finalCurrent,
|
|
270
|
-
message: `🔎︎ grep: ${pattern} in ${scope} • failed`,
|
|
271
|
-
});
|
|
250
|
+
progress.fail(`🔎︎ grep: ${pattern} in ${scope} • failed`);
|
|
272
251
|
throw error;
|
|
273
252
|
}
|
|
274
253
|
},
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export type ToolResponse<T> = ReturnType<typeof buildToolResponse<T>> & {
|
|
|
47
47
|
interface ToolErrorResponse extends Record<string, unknown> {
|
|
48
48
|
content: ContentBlock[];
|
|
49
49
|
isError: true;
|
|
50
|
+
errorCode?: string;
|
|
50
51
|
}
|
|
51
52
|
export type ToolResult<T> = ToolResponse<T> | ToolErrorResponse;
|
|
52
53
|
export declare function withValidatedArgs<Args, Result>(schema: z.ZodType<Args>, handler: (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>): (args: unknown, extra: ToolExtra) => Promise<ToolResult<Result>>;
|
|
@@ -111,9 +112,30 @@ export declare function notifyProgress(extra: ToolExtra, progress: {
|
|
|
111
112
|
total?: number;
|
|
112
113
|
message?: string;
|
|
113
114
|
}): void;
|
|
115
|
+
export interface ToolProgressSession {
|
|
116
|
+
update: (progress: {
|
|
117
|
+
current: number;
|
|
118
|
+
total?: number;
|
|
119
|
+
message: string;
|
|
120
|
+
}) => void;
|
|
121
|
+
increment: (messageForCurrent: (current: number) => string) => void;
|
|
122
|
+
complete: (message: string, minimumCurrent?: number) => void;
|
|
123
|
+
fail: (message: string, minimumCurrent?: number) => void;
|
|
124
|
+
getCurrent: () => number;
|
|
125
|
+
}
|
|
126
|
+
export declare function createToolProgressSession(extra: ToolExtra, startMessage: string): ToolProgressSession;
|
|
114
127
|
export declare function wrapToolHandler<Args, Result>(handler: (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>, options: {
|
|
115
128
|
guard?: (() => boolean) | undefined;
|
|
116
129
|
progressMessage?: (args: Args) => string;
|
|
117
130
|
completionMessage?: (args: Args, result: ToolResult<Result>) => string | undefined;
|
|
118
131
|
}): (args: Args, extra?: ToolExtra) => Promise<ToolResult<Result>>;
|
|
132
|
+
/**
|
|
133
|
+
* Returns `pathValue` if non-empty; otherwise resolves to the single allowed
|
|
134
|
+
* directory from module-level state managed by `RootsManager`. Throws when the
|
|
135
|
+
* path is ambiguous (multiple roots) or when no roots are configured.
|
|
136
|
+
*
|
|
137
|
+
* NOTE: Depends on `getAllowedDirectories()` which reads module-level state
|
|
138
|
+
* updated by `RootsManager`. Ensure the server is initialized before calling.
|
|
139
|
+
* See `src/server/roots-manager.ts` for the update lifecycle.
|
|
140
|
+
*/
|
|
119
141
|
export declare function resolvePathOrRoot(pathValue: string | undefined): string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -198,6 +198,7 @@ export function buildToolErrorResponse(error, defaultCode, path) {
|
|
|
198
198
|
return {
|
|
199
199
|
content: [{ type: 'text', text }],
|
|
200
200
|
isError: true,
|
|
201
|
+
errorCode: detailed.code,
|
|
201
202
|
};
|
|
202
203
|
}
|
|
203
204
|
function buildNotInitializedResult() {
|
|
@@ -259,10 +260,11 @@ export function createProgressReporter(extra) {
|
|
|
259
260
|
// out-of-order progress is undefined in the MCP spec.
|
|
260
261
|
if (current <= lastProgress)
|
|
261
262
|
return;
|
|
262
|
-
//
|
|
263
|
-
//
|
|
263
|
+
// Terminal notifications always bypass the rate limit so clients reliably
|
|
264
|
+
// receive the final state even when updates arrive in quick succession.
|
|
265
|
+
const isTerminal = total !== undefined && current >= total;
|
|
264
266
|
const now = Date.now();
|
|
265
|
-
if (now - lastSentMs < PROGRESS_RATE_LIMIT_MS)
|
|
267
|
+
if (!isTerminal && now - lastSentMs < PROGRESS_RATE_LIMIT_MS)
|
|
266
268
|
return;
|
|
267
269
|
lastProgress = current;
|
|
268
270
|
lastSentMs = now;
|
|
@@ -278,16 +280,66 @@ export function notifyProgress(extra, progress) {
|
|
|
278
280
|
return;
|
|
279
281
|
void reportProgress(extra, progress);
|
|
280
282
|
}
|
|
283
|
+
export function createToolProgressSession(extra, startMessage) {
|
|
284
|
+
notifyProgress(extra, {
|
|
285
|
+
current: 0,
|
|
286
|
+
message: startMessage,
|
|
287
|
+
});
|
|
288
|
+
let cursor = 0;
|
|
289
|
+
const baseReporter = createProgressReporter(extra);
|
|
290
|
+
const setCursor = (value) => {
|
|
291
|
+
if (value > cursor)
|
|
292
|
+
cursor = value;
|
|
293
|
+
return cursor;
|
|
294
|
+
};
|
|
295
|
+
return {
|
|
296
|
+
update: ({ current, total, message }) => {
|
|
297
|
+
const normalized = setCursor(current);
|
|
298
|
+
baseReporter({
|
|
299
|
+
current: normalized,
|
|
300
|
+
...(total !== undefined ? { total } : {}),
|
|
301
|
+
message,
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
increment: (messageForCurrent) => {
|
|
305
|
+
const next = setCursor(cursor + 1);
|
|
306
|
+
baseReporter({
|
|
307
|
+
current: next,
|
|
308
|
+
message: messageForCurrent(next),
|
|
309
|
+
});
|
|
310
|
+
},
|
|
311
|
+
complete: (message, minimumCurrent) => {
|
|
312
|
+
const finalCurrent = Math.max(cursor + 1, minimumCurrent ?? 1, 1);
|
|
313
|
+
notifyProgress(extra, {
|
|
314
|
+
current: finalCurrent,
|
|
315
|
+
total: finalCurrent,
|
|
316
|
+
message,
|
|
317
|
+
});
|
|
318
|
+
cursor = finalCurrent;
|
|
319
|
+
},
|
|
320
|
+
fail: (message, minimumCurrent) => {
|
|
321
|
+
const finalCurrent = Math.max(cursor + 1, minimumCurrent ?? 1, 1);
|
|
322
|
+
notifyProgress(extra, {
|
|
323
|
+
current: finalCurrent,
|
|
324
|
+
total: finalCurrent,
|
|
325
|
+
message,
|
|
326
|
+
});
|
|
327
|
+
cursor = finalCurrent;
|
|
328
|
+
},
|
|
329
|
+
getCurrent: () => cursor,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
281
332
|
async function withProgress(message, extra, run, getCompletionMessage) {
|
|
282
333
|
if (!canReportProgress(extra)) {
|
|
283
334
|
return run();
|
|
284
335
|
}
|
|
285
336
|
const total = 1;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
337
|
+
// Emit the start notification only when a progressToken is present; for
|
|
338
|
+
// task-only mode the task status is already 'working' — a zero-progress
|
|
339
|
+
// notification would add unnecessary overhead without client value.
|
|
340
|
+
if (canSendProgress(extra)) {
|
|
341
|
+
await reportProgress(extra, { current: 0, total, message });
|
|
342
|
+
}
|
|
291
343
|
try {
|
|
292
344
|
const result = await run();
|
|
293
345
|
const endMessage = getCompletionMessage?.(result) ?? message;
|
|
@@ -326,6 +378,15 @@ export function wrapToolHandler(handler, options) {
|
|
|
326
378
|
return maybeStripStructuredContentFromResult(result);
|
|
327
379
|
};
|
|
328
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* Returns `pathValue` if non-empty; otherwise resolves to the single allowed
|
|
383
|
+
* directory from module-level state managed by `RootsManager`. Throws when the
|
|
384
|
+
* path is ambiguous (multiple roots) or when no roots are configured.
|
|
385
|
+
*
|
|
386
|
+
* NOTE: Depends on `getAllowedDirectories()` which reads module-level state
|
|
387
|
+
* updated by `RootsManager`. Ensure the server is initialized before calling.
|
|
388
|
+
* See `src/server/roots-manager.ts` for the update lifecycle.
|
|
389
|
+
*/
|
|
329
390
|
export function resolvePathOrRoot(pathValue) {
|
|
330
391
|
if (pathValue && pathValue.trim().length > 0)
|
|
331
392
|
return pathValue;
|
package/dist/tools/stat-many.js
CHANGED
|
@@ -4,7 +4,7 @@ import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
|
4
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
5
5
|
import { getMultipleFileInfo } from '../lib/file-operations/file-info.js';
|
|
6
6
|
import { GetMultipleFileInfoInputSchema, GetMultipleFileInfoOutputSchema, } from '../schemas.js';
|
|
7
|
-
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse,
|
|
7
|
+
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, createToolProgressSession, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
8
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
9
9
|
export const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
10
10
|
name: 'stat_many',
|
|
@@ -79,18 +79,9 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
79
79
|
? `, ${path.basename(args.paths[1] ?? '')}${args.paths.length > 2 ? '…' : ''}`
|
|
80
80
|
: '';
|
|
81
81
|
const context = `${args.paths.length} paths [${first}${extraPaths}]`;
|
|
82
|
-
|
|
83
|
-
notifyProgress(extra, {
|
|
84
|
-
current: 0,
|
|
85
|
-
message: `🕮 stat_many: ${context}`,
|
|
86
|
-
});
|
|
87
|
-
const baseReporter = createProgressReporter(extra);
|
|
82
|
+
const progress = createToolProgressSession(extra, `🕮 stat_many: ${context}`);
|
|
88
83
|
const onProgress = () => {
|
|
89
|
-
|
|
90
|
-
baseReporter({
|
|
91
|
-
current: progressCursor,
|
|
92
|
-
message: `🕮 stat_many: ${context} [${progressCursor}/${args.paths.length} scanned]`,
|
|
93
|
-
});
|
|
84
|
+
progress.increment((current) => `🕮 stat_many: ${context} [${current}/${args.paths.length} scanned]`);
|
|
94
85
|
};
|
|
95
86
|
try {
|
|
96
87
|
const result = await handleGetMultipleFileInfo(args, signal, onProgress);
|
|
@@ -105,21 +96,12 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
105
96
|
else {
|
|
106
97
|
suffix = `${total} OK`;
|
|
107
98
|
}
|
|
108
|
-
const finalCurrent = Math.max(total,
|
|
109
|
-
|
|
110
|
-
current: finalCurrent,
|
|
111
|
-
total: finalCurrent,
|
|
112
|
-
message: `🕮 stat_many: ${context} • ${suffix}`,
|
|
113
|
-
});
|
|
99
|
+
const finalCurrent = Math.max(total, progress.getCurrent() + 1);
|
|
100
|
+
progress.complete(`🕮 stat_many: ${context} • ${suffix}`, finalCurrent);
|
|
114
101
|
return result;
|
|
115
102
|
}
|
|
116
103
|
catch (error) {
|
|
117
|
-
|
|
118
|
-
notifyProgress(extra, {
|
|
119
|
-
current: finalCurrent,
|
|
120
|
-
total: finalCurrent,
|
|
121
|
-
message: `🕮 stat_many: ${context} • failed`,
|
|
122
|
-
});
|
|
104
|
+
progress.fail(`🕮 stat_many: ${context} • failed`);
|
|
123
105
|
throw error;
|
|
124
106
|
}
|
|
125
107
|
},
|