@j0hanz/filesystem-mcp 1.13.2 → 1.14.1
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 +162 -145
- package/dist/cli.js +2 -2
- package/dist/completions.js +54 -51
- package/dist/config.d.ts +13 -14
- package/dist/config.js +12 -12
- package/dist/index.js +1 -1
- package/dist/lib/abort.d.ts +7 -0
- package/dist/lib/abort.js +81 -0
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.js +8 -2
- package/dist/lib/errors.d.ts +7 -3
- package/dist/lib/errors.js +64 -41
- package/dist/lib/file-operations/core.d.ts +3 -3
- package/dist/lib/file-operations/core.js +23 -20
- package/dist/lib/file-operations/metadata.d.ts +2 -2
- package/dist/lib/file-operations/metadata.js +69 -22
- package/dist/lib/file-operations/search.d.ts +0 -1
- package/dist/lib/file-operations/search.js +87 -95
- package/dist/lib/file-operations/traversal.js +13 -15
- package/dist/lib/fs-helpers.d.ts +3 -10
- package/dist/lib/fs-helpers.js +29 -108
- package/dist/lib/globs.d.ts +2 -0
- package/dist/lib/globs.js +19 -0
- package/dist/lib/logger.d.ts +28 -0
- package/dist/lib/logger.js +91 -0
- package/dist/lib/observability.d.ts +7 -0
- package/dist/lib/observability.js +19 -9
- package/dist/lib/paths.js +55 -55
- package/dist/lib/resource-store.js +4 -4
- package/dist/lib/utils.d.ts +0 -12
- package/dist/lib/utils.js +0 -13
- 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 +41 -41
- package/dist/resources/tool-catalog.js +33 -58
- package/dist/resources/tool-info.d.ts +0 -1
- package/dist/resources/tool-info.js +44 -67
- package/dist/resources/workflows.js +47 -19
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +4 -4
- package/dist/schemas.d.ts +185 -465
- package/dist/schemas.js +174 -206
- package/dist/server/bootstrap.d.ts +12 -11
- package/dist/server/bootstrap.js +95 -86
- package/dist/server/roots-manager.d.ts +5 -2
- package/dist/server/roots-manager.js +9 -7
- package/dist/server/task-store.d.ts +10 -0
- package/dist/server/task-store.js +73 -0
- package/dist/tools/apply-patch.js +39 -20
- package/dist/tools/calculate-hash.js +14 -27
- package/dist/tools/create-directory.js +11 -9
- package/dist/tools/delete-file.js +19 -19
- package/dist/tools/diff-files.js +16 -18
- package/dist/tools/edit-file.js +11 -5
- package/dist/tools/list-directory.js +16 -21
- package/dist/tools/move-file.js +105 -100
- package/dist/tools/read-multiple.js +15 -10
- package/dist/tools/read.js +6 -7
- package/dist/tools/replace-in-files.js +76 -115
- package/dist/tools/roots.js +3 -7
- package/dist/tools/search-content.js +158 -203
- package/dist/tools/search-files.js +59 -50
- package/dist/tools/shared.d.ts +10 -0
- package/dist/tools/shared.js +105 -36
- package/dist/tools/stat-many.js +15 -9
- package/dist/tools/stat.js +6 -6
- package/dist/tools/task-support.d.ts +10 -9
- package/dist/tools/task-support.js +94 -23
- package/dist/tools/tree.js +4 -4
- package/dist/tools/write-file.js +11 -12
- package/package.json +10 -9
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
1
|
import { Buffer } from 'node:buffer';
|
|
5
|
-
import {
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
import { basename, relative } from 'node:path';
|
|
6
4
|
import { createTwoFilesPatch } from 'diff';
|
|
7
5
|
import RE2 from 're2';
|
|
8
|
-
import safeRegex from 'safe-regex2';
|
|
9
6
|
import { DEFAULT_EXCLUDE_PATTERNS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../lib/constants.js';
|
|
10
7
|
import { ErrorCode, formatUnknownErrorMessage, McpError, } from '../lib/errors.js';
|
|
11
8
|
import { globEntries } from '../lib/file-operations/traversal.js';
|
|
12
9
|
import { atomicWriteFile } from '../lib/fs-helpers.js';
|
|
10
|
+
import { Logger } from '../lib/logger.js';
|
|
13
11
|
import { validateExistingPath, validatePathForWrite } from '../lib/paths.js';
|
|
14
|
-
import { reportPeriodicProgress } from '../lib/utils.js';
|
|
15
12
|
import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
|
|
16
|
-
import { buildToolErrorResponse, buildToolResponse, createToolProgressSession, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, resolveFinalProgressCurrent, resolvePathOrRoot, truncateProgressPattern, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
13
|
+
import { buildStructuredError, buildToolErrorResponse, buildToolResponse, createToolProgressSession, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, resolveFinalProgressCurrent, resolvePathOrRoot, truncateProgressPattern, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
17
14
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
18
15
|
export const SEARCH_AND_REPLACE_TOOL = {
|
|
19
16
|
name: 'search_and_replace',
|
|
@@ -26,12 +23,6 @@ export const SEARCH_AND_REPLACE_TOOL = {
|
|
|
26
23
|
outputSchema: SearchAndReplaceOutputSchema,
|
|
27
24
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
28
25
|
taskSupport: 'optional',
|
|
29
|
-
gotchas: [
|
|
30
|
-
'Replaces ALL occurrences — not just the first. Use `edit` for single replacements.',
|
|
31
|
-
],
|
|
32
|
-
nuances: [
|
|
33
|
-
'Changed-file sample and failure sample are capped/truncated in output.',
|
|
34
|
-
],
|
|
35
26
|
};
|
|
36
27
|
const MAX_FAILURES = 20;
|
|
37
28
|
const REPLACE_CONCURRENCY = Math.min(PARALLEL_CONCURRENCY, 8);
|
|
@@ -44,7 +35,7 @@ function recordFailure(failures, failure) {
|
|
|
44
35
|
failures.push(failure);
|
|
45
36
|
}
|
|
46
37
|
function recordChangedFile(summary, filePath, matchCount) {
|
|
47
|
-
const relativePath =
|
|
38
|
+
const relativePath = relative(summary.root, filePath);
|
|
48
39
|
if (summary.changedFiles.length < MAX_CHANGED_FILES) {
|
|
49
40
|
summary.changedFiles.push({ path: relativePath, matches: matchCount });
|
|
50
41
|
return;
|
|
@@ -57,67 +48,55 @@ function createRegexMatcher(pattern, caseSensitive) {
|
|
|
57
48
|
return new RE2(pattern, flags);
|
|
58
49
|
}
|
|
59
50
|
catch (error) {
|
|
60
|
-
throw new McpError(ErrorCode.
|
|
51
|
+
throw new McpError(ErrorCode.INVALID_PATTERN, `Invalid regex pattern: ${formatUnknownErrorMessage(error)} (RE2: no lookahead/lookbehind/backrefs)`);
|
|
61
52
|
}
|
|
62
53
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
54
|
+
function createRegexReplacementMatcher(regex) {
|
|
55
|
+
return {
|
|
56
|
+
testBuffer(buffer) {
|
|
57
|
+
return regex.test(buffer);
|
|
58
|
+
},
|
|
59
|
+
count(content) {
|
|
60
|
+
regex.lastIndex = 0;
|
|
61
|
+
let matchCount = 0;
|
|
62
|
+
while (regex.exec(content) !== null) {
|
|
63
|
+
matchCount++;
|
|
64
|
+
if (regex.lastIndex === 0) {
|
|
65
|
+
regex.lastIndex++;
|
|
66
|
+
}
|
|
75
67
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
class LiteralReplacementMatcher {
|
|
85
|
-
searchPattern;
|
|
86
|
-
caseSensitive;
|
|
87
|
-
patternLength;
|
|
88
|
-
searchBuffer;
|
|
89
|
-
constructor(searchPattern, caseSensitive) {
|
|
90
|
-
this.searchPattern = searchPattern;
|
|
91
|
-
this.caseSensitive = caseSensitive;
|
|
92
|
-
this.patternLength = searchPattern.length;
|
|
93
|
-
this.searchBuffer = caseSensitive
|
|
94
|
-
? Buffer.from(searchPattern, 'utf8')
|
|
95
|
-
: null;
|
|
96
|
-
}
|
|
97
|
-
testBuffer(buffer) {
|
|
98
|
-
if (!this.searchBuffer)
|
|
99
|
-
return true;
|
|
100
|
-
return buffer.indexOf(this.searchBuffer) !== -1;
|
|
101
|
-
}
|
|
102
|
-
count(content) {
|
|
103
|
-
let matchCount = 0;
|
|
104
|
-
let pos = content.indexOf(this.searchPattern);
|
|
105
|
-
while (pos !== -1) {
|
|
106
|
-
matchCount++;
|
|
107
|
-
pos = content.indexOf(this.searchPattern, pos + this.patternLength);
|
|
108
|
-
}
|
|
109
|
-
return matchCount;
|
|
110
|
-
}
|
|
111
|
-
replace(content, replacement) {
|
|
112
|
-
return content.replaceAll(this.searchPattern, () => replacement);
|
|
113
|
-
}
|
|
68
|
+
return matchCount;
|
|
69
|
+
},
|
|
70
|
+
replace(content, replacement) {
|
|
71
|
+
regex.lastIndex = 0;
|
|
72
|
+
return content.replace(regex, replacement);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
114
75
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return
|
|
76
|
+
function createLiteralReplacementMatcher(searchPattern, caseSensitive) {
|
|
77
|
+
const patternLength = searchPattern.length;
|
|
78
|
+
const searchBuffer = caseSensitive
|
|
79
|
+
? Buffer.from(searchPattern, 'utf8')
|
|
80
|
+
: null;
|
|
81
|
+
return {
|
|
82
|
+
testBuffer(buffer) {
|
|
83
|
+
if (!searchBuffer)
|
|
84
|
+
return true;
|
|
85
|
+
return buffer.indexOf(searchBuffer) !== -1;
|
|
86
|
+
},
|
|
87
|
+
count(content) {
|
|
88
|
+
let matchCount = 0;
|
|
89
|
+
let pos = content.indexOf(searchPattern);
|
|
90
|
+
while (pos !== -1) {
|
|
91
|
+
matchCount++;
|
|
92
|
+
pos = content.indexOf(searchPattern, pos + patternLength);
|
|
93
|
+
}
|
|
94
|
+
return matchCount;
|
|
95
|
+
},
|
|
96
|
+
replace(content, replacement) {
|
|
97
|
+
return content.replaceAll(searchPattern, () => replacement);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
121
100
|
}
|
|
122
101
|
function buildReplacementPlan(content, replacement, matcher) {
|
|
123
102
|
const matchCount = matcher.count(content);
|
|
@@ -133,8 +112,8 @@ function buildReplacementPlan(content, replacement, matcher) {
|
|
|
133
112
|
function formatFileTooLargeError(filePath, size, maxFileSize) {
|
|
134
113
|
return `File too large: ${filePath} (${size} bytes > ${maxFileSize} bytes)`;
|
|
135
114
|
}
|
|
136
|
-
async function processEntry(entryPath) {
|
|
137
|
-
const { options, signal, summary } =
|
|
115
|
+
async function processEntry(entryPath, ctx) {
|
|
116
|
+
const { options, signal, summary } = ctx;
|
|
138
117
|
let validPath;
|
|
139
118
|
try {
|
|
140
119
|
validPath = await validatePathForWrite(entryPath, signal);
|
|
@@ -143,12 +122,12 @@ async function processEntry(entryPath) {
|
|
|
143
122
|
summary.failedFiles++;
|
|
144
123
|
recordFailure(summary.failures, {
|
|
145
124
|
path: entryPath,
|
|
146
|
-
error:
|
|
125
|
+
error: buildStructuredError(error, ErrorCode.UNKNOWN, entryPath),
|
|
147
126
|
});
|
|
148
127
|
return;
|
|
149
128
|
}
|
|
150
129
|
try {
|
|
151
|
-
const plan = await readReplacementPlan(validPath);
|
|
130
|
+
const plan = await readReplacementPlan(validPath, ctx);
|
|
152
131
|
if (!plan) {
|
|
153
132
|
return;
|
|
154
133
|
}
|
|
@@ -172,15 +151,15 @@ async function processEntry(entryPath) {
|
|
|
172
151
|
summary.failedFiles++;
|
|
173
152
|
recordFailure(summary.failures, {
|
|
174
153
|
path: validPath,
|
|
175
|
-
error:
|
|
154
|
+
error: buildStructuredError(error, ErrorCode.UNKNOWN, validPath),
|
|
176
155
|
});
|
|
177
156
|
}
|
|
178
157
|
}
|
|
179
|
-
async function readReplacementPlan(validPath) {
|
|
180
|
-
const { matcher, replacement, maxFileSize, signal } =
|
|
158
|
+
async function readReplacementPlan(validPath, ctx) {
|
|
159
|
+
const { matcher, replacement, maxFileSize, signal } = ctx;
|
|
181
160
|
let fileHandle;
|
|
182
161
|
try {
|
|
183
|
-
const fd = await
|
|
162
|
+
const fd = await open(validPath, 'r');
|
|
184
163
|
fileHandle = fd;
|
|
185
164
|
const stats = await fileHandle.stat();
|
|
186
165
|
if (stats.size > maxFileSize) {
|
|
@@ -218,7 +197,7 @@ async function maybeAppendPatchDiff(summary, params) {
|
|
|
218
197
|
const patch = await new Promise((resolve) => {
|
|
219
198
|
// Defer to event loop to avoid blocking on large diffs
|
|
220
199
|
setImmediate(() => {
|
|
221
|
-
createTwoFilesPatch(
|
|
200
|
+
createTwoFilesPatch(basename(params.filePath), basename(params.filePath), params.originalContent, params.updatedContent, 'Original', 'Modified', {
|
|
222
201
|
callback: (res) => {
|
|
223
202
|
resolve(res ?? '');
|
|
224
203
|
},
|
|
@@ -234,7 +213,6 @@ async function maybeAppendPatchDiff(summary, params) {
|
|
|
234
213
|
}
|
|
235
214
|
async function processEntriesConcurrently(entries, options) {
|
|
236
215
|
const pending = new Set();
|
|
237
|
-
const seen = new Set();
|
|
238
216
|
const { signal, concurrency, maxEntries, onEntry, runEntry } = options;
|
|
239
217
|
let dispatched = 0;
|
|
240
218
|
let stoppedByLimit = false;
|
|
@@ -250,9 +228,6 @@ async function processEntriesConcurrently(entries, options) {
|
|
|
250
228
|
stoppedByLimit = true;
|
|
251
229
|
break;
|
|
252
230
|
}
|
|
253
|
-
if (seen.has(entry.path))
|
|
254
|
-
continue;
|
|
255
|
-
seen.add(entry.path);
|
|
256
231
|
await waitForSlot();
|
|
257
232
|
onEntry();
|
|
258
233
|
dispatched++;
|
|
@@ -289,22 +264,19 @@ async function resolveSearchRoot(pathValue, signal) {
|
|
|
289
264
|
function createReplacementRegex(args) {
|
|
290
265
|
if (!args.isRegex)
|
|
291
266
|
return undefined;
|
|
292
|
-
if (!safeRegex(args.searchPattern)) {
|
|
293
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT, `Unsafe regex pattern: ${args.searchPattern}`);
|
|
294
|
-
}
|
|
295
267
|
return createRegexMatcher(args.searchPattern, args.caseSensitive);
|
|
296
268
|
}
|
|
297
269
|
function createReplacementMatcher(args) {
|
|
298
270
|
const regex = createReplacementRegex(args);
|
|
299
271
|
if (regex) {
|
|
300
|
-
return
|
|
272
|
+
return createRegexReplacementMatcher(regex);
|
|
301
273
|
}
|
|
302
274
|
if (!args.caseSensitive) {
|
|
303
275
|
const escaped = args.searchPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
304
276
|
const caseInsensitiveRegex = new RE2(escaped, 'gi');
|
|
305
|
-
return
|
|
277
|
+
return createRegexReplacementMatcher(caseInsensitiveRegex);
|
|
306
278
|
}
|
|
307
|
-
return
|
|
279
|
+
return createLiteralReplacementMatcher(args.searchPattern, args.caseSensitive);
|
|
308
280
|
}
|
|
309
281
|
async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
310
282
|
const maxFileSize = MAX_TEXT_FILE_SIZE;
|
|
@@ -314,7 +286,7 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
|
314
286
|
cwd: root,
|
|
315
287
|
pattern: args.filePattern,
|
|
316
288
|
excludePatterns: args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS,
|
|
317
|
-
includeHidden: args.includeHidden
|
|
289
|
+
includeHidden: args.includeHidden,
|
|
318
290
|
baseNameMatch: false,
|
|
319
291
|
caseSensitiveMatch: true, // Default to sensitive for file paths
|
|
320
292
|
followSymbolicLinks: false,
|
|
@@ -323,14 +295,11 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
|
323
295
|
suppressErrors: true,
|
|
324
296
|
});
|
|
325
297
|
const summary = createReplaceSummary(root);
|
|
326
|
-
const
|
|
327
|
-
const timerEndName = `searchAndReplaceEnd_${Date.now()}`;
|
|
328
|
-
const metricName = `searchAndReplace_${Date.now()}`;
|
|
329
|
-
performance.mark(timerStartName);
|
|
298
|
+
const t0 = performance.now();
|
|
330
299
|
const context = {
|
|
331
300
|
options: {
|
|
332
301
|
dryRun: args.dryRun,
|
|
333
|
-
returnDiff: args.returnDiff
|
|
302
|
+
returnDiff: args.returnDiff,
|
|
334
303
|
},
|
|
335
304
|
replacement: args.replacement,
|
|
336
305
|
matcher,
|
|
@@ -338,38 +307,31 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
|
338
307
|
signal,
|
|
339
308
|
summary,
|
|
340
309
|
};
|
|
341
|
-
const { stoppedByLimit } = await
|
|
310
|
+
const { stoppedByLimit } = await processEntriesConcurrently(entries, {
|
|
342
311
|
signal,
|
|
343
312
|
concurrency: REPLACE_CONCURRENCY,
|
|
344
313
|
...(args.maxFiles !== undefined ? { maxEntries: args.maxFiles } : {}),
|
|
345
314
|
onEntry: () => {
|
|
346
315
|
summary.processedFiles++;
|
|
347
|
-
|
|
348
|
-
throttleModulo: 25,
|
|
349
|
-
});
|
|
316
|
+
onProgress({ current: summary.processedFiles });
|
|
350
317
|
},
|
|
351
|
-
runEntry: processEntry,
|
|
352
|
-
})
|
|
353
|
-
performance.
|
|
354
|
-
performance.measure(metricName, timerStartName, timerEndName);
|
|
355
|
-
const durationMs = performance.getEntriesByName(metricName)[0]?.duration ?? 0;
|
|
356
|
-
performance.clearMarks(timerStartName);
|
|
357
|
-
performance.clearMarks(timerEndName);
|
|
358
|
-
performance.clearMeasures(metricName);
|
|
359
|
-
summary.perfTimeMs = durationMs;
|
|
318
|
+
runEntry: (entryPath) => processEntry(entryPath, context),
|
|
319
|
+
});
|
|
320
|
+
summary.perfTimeMs = performance.now() - t0;
|
|
360
321
|
if (stoppedByLimit) {
|
|
361
322
|
summary.stoppedReason = 'maxFiles';
|
|
362
323
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
324
|
+
onProgress({ current: summary.processedFiles });
|
|
325
|
+
if (!args.dryRun && summary.totalMatches > 0) {
|
|
326
|
+
Logger.info(`search_and_replace: ${summary.filesChanged} file(s), ${summary.totalMatches} match(es)`);
|
|
327
|
+
}
|
|
367
328
|
return buildToolResponse(buildSearchAndReplaceText(summary, args.dryRun), buildSearchAndReplaceStructuredResult(summary, args));
|
|
368
329
|
}
|
|
369
330
|
export function registerSearchAndReplaceTool(server, options = {}) {
|
|
370
331
|
const handler = (args, extra) => executeToolWithDiagnostics({
|
|
371
332
|
toolName: 'search_and_replace',
|
|
372
333
|
extra,
|
|
334
|
+
outputSchema: SearchAndReplaceOutputSchema,
|
|
373
335
|
timedSignal: {},
|
|
374
336
|
...(args.path ? { context: { path: args.path } } : {}),
|
|
375
337
|
run: async (signal) => {
|
|
@@ -401,7 +363,7 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
401
363
|
throw error;
|
|
402
364
|
}
|
|
403
365
|
},
|
|
404
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.
|
|
366
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN, args.path),
|
|
405
367
|
});
|
|
406
368
|
const { isInitialized } = options;
|
|
407
369
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
@@ -437,6 +399,5 @@ function buildSearchAndReplaceStructuredResult(summary, args) {
|
|
|
437
399
|
: {}),
|
|
438
400
|
...(summary.diffTruncated ? { diffTruncated: true } : {}),
|
|
439
401
|
...(summary.stoppedReason ? { stoppedReason: summary.stoppedReason } : {}),
|
|
440
|
-
dryRun: args.dryRun,
|
|
441
402
|
};
|
|
442
403
|
}
|
package/dist/tools/roots.js
CHANGED
|
@@ -11,7 +11,6 @@ export const LIST_ALLOWED_DIRECTORIES_TOOL = {
|
|
|
11
11
|
inputSchema: ListAllowedDirectoriesInputSchema,
|
|
12
12
|
outputSchema: ListAllowedDirectoriesOutputSchema,
|
|
13
13
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
14
|
-
nuances: ['Returns absolute paths of all allowed directories.'],
|
|
15
14
|
taskSupport: 'forbidden',
|
|
16
15
|
};
|
|
17
16
|
function buildTextRoots(dirs) {
|
|
@@ -28,8 +27,6 @@ function handleListAllowedDirectories() {
|
|
|
28
27
|
const structured = {
|
|
29
28
|
ok: true,
|
|
30
29
|
directories: dirs,
|
|
31
|
-
rootsCount: dirs.length,
|
|
32
|
-
hasMultipleRoots: dirs.length > 1,
|
|
33
30
|
};
|
|
34
31
|
return buildToolResponse(buildTextRoots(dirs), structured);
|
|
35
32
|
}
|
|
@@ -37,8 +34,9 @@ export function registerListAllowedDirectoriesTool(server, options = {}) {
|
|
|
37
34
|
const handler = (_args, extra) => executeToolWithDiagnostics({
|
|
38
35
|
toolName: 'roots',
|
|
39
36
|
extra,
|
|
37
|
+
outputSchema: ListAllowedDirectoriesOutputSchema,
|
|
40
38
|
run: () => handleListAllowedDirectories(),
|
|
41
|
-
onError: (error) => buildToolErrorResponse(error, ErrorCode.
|
|
39
|
+
onError: (error) => buildToolErrorResponse(error, ErrorCode.UNKNOWN),
|
|
42
40
|
});
|
|
43
41
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
44
42
|
guard: options.isInitialized,
|
|
@@ -47,9 +45,7 @@ export function registerListAllowedDirectoriesTool(server, options = {}) {
|
|
|
47
45
|
if (result.isError)
|
|
48
46
|
return `≣ roots • failed`;
|
|
49
47
|
const sc = result.structuredContent;
|
|
50
|
-
|
|
51
|
-
return `≣ roots • failed`;
|
|
52
|
-
const count = sc.rootsCount ?? 0;
|
|
48
|
+
const count = sc.directories?.length ?? 0;
|
|
53
49
|
return `≣ roots • ${count} ${count === 1 ? 'root' : 'roots'}`;
|
|
54
50
|
},
|
|
55
51
|
});
|