@j0hanz/filesystem-mcp 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/constants.d.ts +4 -0
- package/dist/lib/constants.js +22 -0
- package/dist/lib/file-operations/file-info.d.ts +1 -0
- package/dist/lib/file-operations/file-info.js +5 -4
- package/dist/lib/file-operations/read-multiple-files.d.ts +1 -0
- package/dist/lib/file-operations/read-multiple-files.js +7 -3
- package/dist/lib/file-operations/tree.d.ts +4 -0
- package/dist/lib/file-operations/tree.js +1 -0
- package/dist/schemas.d.ts +1 -0
- package/dist/schemas.js +1 -0
- package/dist/server/bootstrap.js +16 -76
- package/dist/tools/diff-files.js +3 -0
- package/dist/tools/edit-file.js +3 -0
- package/dist/tools/list-directory.js +3 -0
- package/dist/tools/read-multiple.js +53 -21
- package/dist/tools/replace-in-files.js +20 -8
- package/dist/tools/roots.js +3 -0
- package/dist/tools/stat-many.js +53 -21
- package/dist/tools/task-support.js +13 -2
- package/dist/tools/tree.js +46 -21
- package/package.json +1 -1
package/dist/lib/constants.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
declare const VALID_LOG_LEVELS: readonly ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"];
|
|
2
|
+
export type ValidLogLevel = (typeof VALID_LOG_LEVELS)[number];
|
|
3
|
+
export declare const DEFAULT_LOG_LEVEL: "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency";
|
|
1
4
|
export declare const PARALLEL_CONCURRENCY: number;
|
|
2
5
|
export declare const MAX_SEARCHABLE_FILE_SIZE: number;
|
|
3
6
|
export declare const MAX_TEXT_FILE_SIZE: number;
|
|
@@ -19,3 +22,4 @@ export declare const SENSITIVE_FILE_ALLOWLIST: string[];
|
|
|
19
22
|
export declare const KNOWN_BINARY_EXTENSIONS: Set<string>;
|
|
20
23
|
export declare const DEFAULT_EXCLUDE_PATTERNS: string[];
|
|
21
24
|
export declare function getMimeType(ext: string): string;
|
|
25
|
+
export {};
|
package/dist/lib/constants.js
CHANGED
|
@@ -43,6 +43,28 @@ function parseEnvList(envVar) {
|
|
|
43
43
|
}
|
|
44
44
|
return entries;
|
|
45
45
|
}
|
|
46
|
+
const VALID_LOG_LEVELS = [
|
|
47
|
+
'debug',
|
|
48
|
+
'info',
|
|
49
|
+
'notice',
|
|
50
|
+
'warning',
|
|
51
|
+
'error',
|
|
52
|
+
'critical',
|
|
53
|
+
'alert',
|
|
54
|
+
'emergency',
|
|
55
|
+
];
|
|
56
|
+
function parseEnvLogLevel(envVar, defaultValue) {
|
|
57
|
+
const value = process.env[envVar];
|
|
58
|
+
if (!value)
|
|
59
|
+
return defaultValue;
|
|
60
|
+
const normalized = value.trim().toLowerCase();
|
|
61
|
+
if (VALID_LOG_LEVELS.includes(normalized)) {
|
|
62
|
+
return normalized;
|
|
63
|
+
}
|
|
64
|
+
console.error(`[WARNING] Invalid ${envVar} value: ${value} (must be ${VALID_LOG_LEVELS.join('|')}). Using default: ${defaultValue}`);
|
|
65
|
+
return defaultValue;
|
|
66
|
+
}
|
|
67
|
+
export const DEFAULT_LOG_LEVEL = parseEnvLogLevel('FILESYSTEM_MCP_LOG_LEVEL', 'debug');
|
|
46
68
|
// Auto-tuned parallelism based on CPU cores (no env override)
|
|
47
69
|
const BYTES_PER_PARALLEL_TASK = 64 * MIB;
|
|
48
70
|
const BYTES_PER_SEARCH_WORKER = 128 * MIB;
|
|
@@ -2,6 +2,7 @@ import type { FileInfo, GetMultipleFileInfoResult } from '../../config.js';
|
|
|
2
2
|
interface FileInfoOptions {
|
|
3
3
|
includeMimeType?: boolean | undefined;
|
|
4
4
|
signal?: AbortSignal | undefined;
|
|
5
|
+
onProgress?: () => void;
|
|
5
6
|
}
|
|
6
7
|
export declare function getFileInfo(filePath: string, options?: FileInfoOptions): Promise<FileInfo>;
|
|
7
8
|
type GetMultipleFileInfoOptions = FileInfoOptions;
|
|
@@ -89,10 +89,11 @@ function buildIndexedPathTasks(paths) {
|
|
|
89
89
|
return tasks;
|
|
90
90
|
}
|
|
91
91
|
async function readFileInfoInParallel(paths, options) {
|
|
92
|
-
return processInParallel(buildIndexedPathTasks(paths), async ({ filePath, index }) =>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
return processInParallel(buildIndexedPathTasks(paths), async ({ filePath, index }) => {
|
|
93
|
+
const value = await processFileInfo(filePath, options);
|
|
94
|
+
options.onProgress?.();
|
|
95
|
+
return { index, value };
|
|
96
|
+
}, PARALLEL_CONCURRENCY, options.signal);
|
|
96
97
|
}
|
|
97
98
|
function applyResults(output, results) {
|
|
98
99
|
for (const result of results) {
|
|
@@ -19,6 +19,7 @@ interface ReadMultipleOptions {
|
|
|
19
19
|
startLine?: number;
|
|
20
20
|
endLine?: number;
|
|
21
21
|
signal?: AbortSignal;
|
|
22
|
+
onReadComplete?: () => void;
|
|
22
23
|
}
|
|
23
24
|
export declare function readMultipleFiles(filePaths: readonly string[], options?: ReadMultipleOptions): Promise<ReadMultipleResult[]>;
|
|
24
25
|
export {};
|
|
@@ -48,12 +48,16 @@ async function readSingleFile(task, readOptions) {
|
|
|
48
48
|
value: buildReadMultipleResult(filePath, result),
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
-
async function readFilesInParallel(filesToProcess, options, signal) {
|
|
51
|
+
async function readFilesInParallel(filesToProcess, options, signal, onReadComplete) {
|
|
52
52
|
const readOptions = buildReadOptions(options);
|
|
53
53
|
if (signal) {
|
|
54
54
|
readOptions.signal = signal;
|
|
55
55
|
}
|
|
56
|
-
return processInParallel(filesToProcess, async (task) =>
|
|
56
|
+
return processInParallel(filesToProcess, async (task) => {
|
|
57
|
+
const result = await readSingleFile(task, readOptions);
|
|
58
|
+
onReadComplete?.();
|
|
59
|
+
return result;
|
|
60
|
+
}, PARALLEL_CONCURRENCY, signal);
|
|
57
61
|
}
|
|
58
62
|
function normalizeReadMultipleOptions(options) {
|
|
59
63
|
const normalized = {
|
|
@@ -248,7 +252,7 @@ export async function readMultipleFiles(filePaths, options = {}) {
|
|
|
248
252
|
const output = buildOutput(filePaths);
|
|
249
253
|
const { skippedBudget, validated } = await collectFileBudget(filePaths, normalized.maxTotalSize, normalized.maxSize, signal);
|
|
250
254
|
const filesToProcess = buildFilesToProcess(filePaths, validated, skippedBudget);
|
|
251
|
-
const { results, errors } = await readFilesInParallel(filesToProcess, normalized, signal);
|
|
255
|
+
const { results, errors } = await readFilesInParallel(filesToProcess, normalized, signal, options.onReadComplete);
|
|
252
256
|
applyResults(output, results);
|
|
253
257
|
applyErrors(output, errors, filesToProcess, filePaths);
|
|
254
258
|
applySkippedBudget(output, skippedBudget, filePaths, normalized.maxTotalSize);
|
|
@@ -265,6 +265,7 @@ export async function treeDirectory(dirPath, options = {}) {
|
|
|
265
265
|
const parent = ensureParentNodes(rootNode, nodeByPath, resolved.relativePosix);
|
|
266
266
|
upsertChildNode(parent, nodeByPath, resolved, childPathIndexByParent);
|
|
267
267
|
totalEntries += 1;
|
|
268
|
+
options.onProgress?.({ current: totalEntries });
|
|
268
269
|
}
|
|
269
270
|
sortTree(rootNode);
|
|
270
271
|
return {
|
package/dist/schemas.d.ts
CHANGED
|
@@ -733,6 +733,7 @@ export declare const SearchAndReplaceOutputSchema: z.ZodObject<{
|
|
|
733
733
|
matches: z.ZodNumber;
|
|
734
734
|
}, z.core.$strict>>>;
|
|
735
735
|
changedFilesTruncated: z.ZodOptional<z.ZodBoolean>;
|
|
736
|
+
diff: z.ZodOptional<z.ZodString>;
|
|
736
737
|
dryRun: z.ZodOptional<z.ZodBoolean>;
|
|
737
738
|
error: z.ZodOptional<z.ZodObject<{
|
|
738
739
|
code: z.ZodEnum<{
|
package/dist/schemas.js
CHANGED
|
@@ -662,6 +662,7 @@ export const SearchAndReplaceOutputSchema = z.strictObject({
|
|
|
662
662
|
.boolean()
|
|
663
663
|
.optional()
|
|
664
664
|
.describe('Changed file list truncated'),
|
|
665
|
+
diff: z.string().optional().describe('Unified diff of changes (dryRun only)'),
|
|
665
666
|
dryRun: z.boolean().optional(),
|
|
666
667
|
error: ErrorSchema.optional(),
|
|
667
668
|
});
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -7,6 +7,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
7
7
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
8
8
|
import { isInitializeRequest, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
import { registerCompletions } from '../completions.js';
|
|
10
|
+
import { DEFAULT_LOG_LEVEL } from '../lib/constants.js';
|
|
10
11
|
import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
11
12
|
import { createInMemoryResourceStore } from '../lib/resource-store.js';
|
|
12
13
|
import { pkgInfo } from '../pkg-info.js';
|
|
@@ -77,7 +78,7 @@ export async function createServer(options = {}) {
|
|
|
77
78
|
...(SERVER_DESCRIPTION ? { description: SERVER_DESCRIPTION } : {}),
|
|
78
79
|
...(SERVER_HOMEPAGE ? { websiteUrl: SERVER_HOMEPAGE } : {}),
|
|
79
80
|
}, localIcon), serverConfig);
|
|
80
|
-
const loggingState = createLoggingState(
|
|
81
|
+
const loggingState = createLoggingState(DEFAULT_LOG_LEVEL);
|
|
81
82
|
const rootsManager = new RootsManager(options, loggingState);
|
|
82
83
|
rootsManagers.set(server, rootsManager);
|
|
83
84
|
server.server.setRequestHandler(SetLevelRequestSchema, (req) => {
|
|
@@ -160,6 +161,14 @@ async function createHttpSession(options, sessions) {
|
|
|
160
161
|
await mcpServer.connect(transport);
|
|
161
162
|
return { server: mcpServer, transport };
|
|
162
163
|
}
|
|
164
|
+
function sendJsonRpcError(res, status, code, message) {
|
|
165
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
166
|
+
res.end(JSON.stringify({
|
|
167
|
+
jsonrpc: '2.0',
|
|
168
|
+
error: { code, message },
|
|
169
|
+
id: null,
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
163
172
|
export async function startHttpServer(port, options) {
|
|
164
173
|
const sessions = new Map();
|
|
165
174
|
async function handleMcpRequest(req, res) {
|
|
@@ -198,91 +207,27 @@ export async function startHttpServer(port, options) {
|
|
|
198
207
|
if (session) {
|
|
199
208
|
await session.transport.handleRequest(req, res, body);
|
|
200
209
|
}
|
|
201
|
-
else {
|
|
202
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
203
|
-
res.end(JSON.stringify({
|
|
204
|
-
jsonrpc: '2.0',
|
|
205
|
-
error: {
|
|
206
|
-
code: -32000,
|
|
207
|
-
message: 'Bad Request: Session not found',
|
|
208
|
-
},
|
|
209
|
-
id: null,
|
|
210
|
-
}));
|
|
211
|
-
}
|
|
212
210
|
}
|
|
213
211
|
else if (!sessionId && isInitializeRequest(body)) {
|
|
214
212
|
const { transport } = await createHttpSession(options, sessions);
|
|
215
213
|
await transport.handleRequest(req, res, body);
|
|
216
214
|
}
|
|
217
|
-
else {
|
|
218
|
-
res
|
|
219
|
-
res.end(JSON.stringify({
|
|
220
|
-
jsonrpc: '2.0',
|
|
221
|
-
error: {
|
|
222
|
-
code: -32000,
|
|
223
|
-
message: 'Bad Request: No valid session ID provided',
|
|
224
|
-
},
|
|
225
|
-
id: null,
|
|
226
|
-
}));
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
else if (method === 'GET') {
|
|
230
|
-
if (!sessionId || !sessions.has(sessionId)) {
|
|
231
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
232
|
-
res.end(JSON.stringify({
|
|
233
|
-
jsonrpc: '2.0',
|
|
234
|
-
error: {
|
|
235
|
-
code: -32000,
|
|
236
|
-
message: 'Bad Request: Invalid or missing session ID',
|
|
237
|
-
},
|
|
238
|
-
id: null,
|
|
239
|
-
}));
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
const session = sessions.get(sessionId);
|
|
243
|
-
if (session) {
|
|
244
|
-
await session.transport.handleRequest(req, res);
|
|
215
|
+
else if (sessionId) {
|
|
216
|
+
sendJsonRpcError(res, 400, -32000, 'Bad Request: Session not found');
|
|
245
217
|
}
|
|
246
218
|
else {
|
|
247
|
-
res
|
|
248
|
-
res.end(JSON.stringify({
|
|
249
|
-
jsonrpc: '2.0',
|
|
250
|
-
error: {
|
|
251
|
-
code: -32000,
|
|
252
|
-
message: 'Bad Request: Session not found',
|
|
253
|
-
},
|
|
254
|
-
id: null,
|
|
255
|
-
}));
|
|
219
|
+
sendJsonRpcError(res, 400, -32000, 'Bad Request: No valid session ID provided');
|
|
256
220
|
}
|
|
257
221
|
}
|
|
258
|
-
else if (method === 'DELETE') {
|
|
222
|
+
else if (method === 'GET' || method === 'DELETE') {
|
|
259
223
|
if (!sessionId || !sessions.has(sessionId)) {
|
|
260
|
-
res
|
|
261
|
-
res.end(JSON.stringify({
|
|
262
|
-
jsonrpc: '2.0',
|
|
263
|
-
error: {
|
|
264
|
-
code: -32000,
|
|
265
|
-
message: 'Bad Request: Invalid or missing session ID',
|
|
266
|
-
},
|
|
267
|
-
id: null,
|
|
268
|
-
}));
|
|
224
|
+
sendJsonRpcError(res, 400, -32000, 'Bad Request: Invalid or missing session ID');
|
|
269
225
|
return;
|
|
270
226
|
}
|
|
271
227
|
const session = sessions.get(sessionId);
|
|
272
228
|
if (session) {
|
|
273
229
|
await session.transport.handleRequest(req, res);
|
|
274
230
|
}
|
|
275
|
-
else {
|
|
276
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
277
|
-
res.end(JSON.stringify({
|
|
278
|
-
jsonrpc: '2.0',
|
|
279
|
-
error: {
|
|
280
|
-
code: -32000,
|
|
281
|
-
message: 'Bad Request: Session not found',
|
|
282
|
-
},
|
|
283
|
-
id: null,
|
|
284
|
-
}));
|
|
285
|
-
}
|
|
286
231
|
}
|
|
287
232
|
else {
|
|
288
233
|
res.writeHead(405, { Allow: 'GET, POST, DELETE' });
|
|
@@ -292,12 +237,7 @@ export async function startHttpServer(port, options) {
|
|
|
292
237
|
catch (error) {
|
|
293
238
|
console.error('[HTTP] Error handling request:', formatUnknownErrorMessage(error));
|
|
294
239
|
if (!res.headersSent) {
|
|
295
|
-
res
|
|
296
|
-
res.end(JSON.stringify({
|
|
297
|
-
jsonrpc: '2.0',
|
|
298
|
-
error: { code: -32603, message: 'Internal Server Error' },
|
|
299
|
-
id: null,
|
|
300
|
-
}));
|
|
240
|
+
sendJsonRpcError(res, 500, -32603, 'Internal Server Error');
|
|
301
241
|
}
|
|
302
242
|
}
|
|
303
243
|
}
|
package/dist/tools/diff-files.js
CHANGED
|
@@ -7,6 +7,7 @@ import { withAbort } from '../lib/fs-helpers.js';
|
|
|
7
7
|
import { validateExistingPath } from '../lib/path-validation.js';
|
|
8
8
|
import { DiffFilesInputSchema, DiffFilesOutputSchema } from '../schemas.js';
|
|
9
9
|
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
10
|
+
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
10
11
|
export const DIFF_FILES_TOOL = {
|
|
11
12
|
name: 'diff_files',
|
|
12
13
|
title: 'Diff Files',
|
|
@@ -104,5 +105,7 @@ export function registerDiffFilesTool(server, options = {}) {
|
|
|
104
105
|
},
|
|
105
106
|
});
|
|
106
107
|
const validatedHandler = withValidatedArgs(DiffFilesInputSchema, wrappedHandler);
|
|
108
|
+
if (registerToolTaskIfAvailable(server, 'diff_files', DIFF_FILES_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
|
|
109
|
+
return;
|
|
107
110
|
server.registerTool('diff_files', withDefaultIcons({ ...DIFF_FILES_TOOL }, options.iconInfo), validatedHandler);
|
|
108
111
|
}
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -5,6 +5,7 @@ import { atomicWriteFile } from '../lib/fs-helpers.js';
|
|
|
5
5
|
import { validateExistingPath } from '../lib/path-validation.js';
|
|
6
6
|
import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
|
+
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
9
|
export const EDIT_FILE_TOOL = {
|
|
9
10
|
name: 'edit',
|
|
10
11
|
title: 'Edit File',
|
|
@@ -111,5 +112,7 @@ export function registerEditFileTool(server, options = {}) {
|
|
|
111
112
|
},
|
|
112
113
|
});
|
|
113
114
|
const validatedHandler = withValidatedArgs(EditFileInputSchema, wrappedHandler);
|
|
115
|
+
if (registerToolTaskIfAvailable(server, 'edit', EDIT_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
|
|
116
|
+
return;
|
|
114
117
|
server.registerTool('edit', withDefaultIcons({ ...EDIT_FILE_TOOL }, options.iconInfo), validatedHandler);
|
|
115
118
|
}
|
|
@@ -5,6 +5,7 @@ import { ErrorCode } from '../lib/errors.js';
|
|
|
5
5
|
import { listDirectory } from '../lib/file-operations/list-directory.js';
|
|
6
6
|
import { ListDirectoryInputSchema, ListDirectoryOutputSchema, } from '../schemas.js';
|
|
7
7
|
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
8
|
+
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
9
|
export const LIST_DIRECTORY_TOOL = {
|
|
9
10
|
name: 'ls',
|
|
10
11
|
title: 'List Directory',
|
|
@@ -144,5 +145,7 @@ export function registerListDirectoryTool(server, options = {}) {
|
|
|
144
145
|
},
|
|
145
146
|
});
|
|
146
147
|
const validatedHandler = withValidatedArgs(ListDirectoryInputSchema, wrappedHandler);
|
|
148
|
+
if (registerToolTaskIfAvailable(server, 'ls', LIST_DIRECTORY_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
|
|
149
|
+
return;
|
|
147
150
|
server.registerTool('ls', withDefaultIcons({ ...LIST_DIRECTORY_TOOL }, options.iconInfo), validatedHandler);
|
|
148
151
|
}
|
|
@@ -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, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
6
|
+
import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, maybeExternalizeTextContent, notifyProgress, 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',
|
|
@@ -19,12 +19,13 @@ export const READ_MULTIPLE_FILES_TOOL = {
|
|
|
19
19
|
'Per-file `truncationReason` can be `head`, `range`, or `externalized`.',
|
|
20
20
|
],
|
|
21
21
|
};
|
|
22
|
-
async function handleReadMultipleFiles(args, signal, resourceStore) {
|
|
22
|
+
async function handleReadMultipleFiles(args, signal, resourceStore, onReadComplete) {
|
|
23
23
|
const options = {
|
|
24
24
|
...(signal ? { signal } : {}),
|
|
25
25
|
...(args.head !== undefined ? { head: args.head } : {}),
|
|
26
26
|
...(args.startLine !== undefined ? { startLine: args.startLine } : {}),
|
|
27
27
|
...(args.endLine !== undefined ? { endLine: args.endLine } : {}),
|
|
28
|
+
...(onReadComplete ? { onReadComplete } : {}),
|
|
28
29
|
};
|
|
29
30
|
const results = await readMultipleFiles(args.paths, options);
|
|
30
31
|
const maxTotalSize = DEFAULT_READ_MANY_MAX_TOTAL_SIZE;
|
|
@@ -122,30 +123,61 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
122
123
|
extra,
|
|
123
124
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
124
125
|
context: { path: primaryPath },
|
|
125
|
-
run: (signal) =>
|
|
126
|
+
run: async (signal) => {
|
|
127
|
+
const first = path.basename(args.paths[0] ?? '');
|
|
128
|
+
const extraPaths = args.paths.length > 1
|
|
129
|
+
? `, ${path.basename(args.paths[1] ?? '')}${args.paths.length > 2 ? '…' : ''}`
|
|
130
|
+
: '';
|
|
131
|
+
const context = `${args.paths.length} files [${first}${extraPaths}]`;
|
|
132
|
+
let progressCursor = 0;
|
|
133
|
+
notifyProgress(extra, {
|
|
134
|
+
current: 0,
|
|
135
|
+
message: `🕮 read_many: ${context}`,
|
|
136
|
+
});
|
|
137
|
+
const baseReporter = createProgressReporter(extra);
|
|
138
|
+
const onReadComplete = () => {
|
|
139
|
+
progressCursor++;
|
|
140
|
+
baseReporter({
|
|
141
|
+
current: progressCursor,
|
|
142
|
+
message: `🕮 read_many: ${context} [${progressCursor}/${args.paths.length} read]`,
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
try {
|
|
146
|
+
const result = await handleReadMultipleFiles(args, signal, options.resourceStore, onReadComplete);
|
|
147
|
+
const sc = result.structuredContent;
|
|
148
|
+
const total = sc.summary?.total ?? 0;
|
|
149
|
+
const failed = sc.summary?.failed ?? 0;
|
|
150
|
+
const succeeded = sc.summary?.succeeded ?? 0;
|
|
151
|
+
let suffix;
|
|
152
|
+
if (failed) {
|
|
153
|
+
suffix = `${succeeded}/${total} read, ${failed} failed`;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
suffix = `${total} files read`;
|
|
157
|
+
}
|
|
158
|
+
const finalCurrent = Math.max(total, progressCursor + 1);
|
|
159
|
+
notifyProgress(extra, {
|
|
160
|
+
current: finalCurrent,
|
|
161
|
+
total: finalCurrent,
|
|
162
|
+
message: `🕮 read_many: ${context} • ${suffix}`,
|
|
163
|
+
});
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
168
|
+
notifyProgress(extra, {
|
|
169
|
+
current: finalCurrent,
|
|
170
|
+
total: finalCurrent,
|
|
171
|
+
message: `🕮 read_many: ${context} • failed`,
|
|
172
|
+
});
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
},
|
|
126
176
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, primaryPath),
|
|
127
177
|
});
|
|
128
178
|
};
|
|
129
179
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
130
180
|
guard: options.isInitialized,
|
|
131
|
-
progressMessage: (args) => {
|
|
132
|
-
const first = path.basename(args.paths[0] ?? '');
|
|
133
|
-
const extra = args.paths.length > 1 ? `, ${path.basename(args.paths[1] ?? '')}…` : '';
|
|
134
|
-
return `🕮 read_many: ${args.paths.length} files [${first}${extra}]`;
|
|
135
|
-
},
|
|
136
|
-
completionMessage: (args, result) => {
|
|
137
|
-
if (result.isError)
|
|
138
|
-
return `🕮 read_many: ${args.paths.length} files • failed`;
|
|
139
|
-
const sc = result.structuredContent;
|
|
140
|
-
if (!sc.ok)
|
|
141
|
-
return `🕮 read_many: ${args.paths.length} files • failed`;
|
|
142
|
-
const total = sc.summary?.total ?? 0;
|
|
143
|
-
const succeeded = sc.summary?.succeeded ?? 0;
|
|
144
|
-
const failed = sc.summary?.failed ?? 0;
|
|
145
|
-
if (failed)
|
|
146
|
-
return `🕮 read_many: ${succeeded}/${total} read, ${failed} failed`;
|
|
147
|
-
return `🕮 read_many: ${total} files read`;
|
|
148
|
-
},
|
|
149
181
|
});
|
|
150
182
|
const validatedHandler = withValidatedArgs(ReadMultipleFilesInputSchema, wrappedHandler);
|
|
151
183
|
if (registerToolTaskIfAvailable(server, 'read_many', READ_MULTIPLE_FILES_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { createTwoFilesPatch } from 'diff';
|
|
3
4
|
import RE2 from 're2';
|
|
4
5
|
import safeRegex from 'safe-regex2';
|
|
5
6
|
import { DEFAULT_EXCLUDE_PATTERNS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../lib/constants.js';
|
|
@@ -17,6 +18,7 @@ export const SEARCH_AND_REPLACE_TOOL = {
|
|
|
17
18
|
'Replaces ALL occurrences in each file (unlike `edit` which replaces only the first). ' +
|
|
18
19
|
'Use `filePattern` to scope which files are touched. ' +
|
|
19
20
|
'Always run with `dryRun: true` first to verify matches before writing. ' +
|
|
21
|
+
'Returns a unified diff of changes in `dryRun` mode. ' +
|
|
20
22
|
'Literal mode (default) matches exact text; `isRegex: true` enables RE2 regex with capture groups ($1, $2).',
|
|
21
23
|
inputSchema: SearchAndReplaceInputSchema,
|
|
22
24
|
outputSchema: SearchAndReplaceOutputSchema,
|
|
@@ -31,6 +33,7 @@ export const SEARCH_AND_REPLACE_TOOL = {
|
|
|
31
33
|
const MAX_FAILURES = 20;
|
|
32
34
|
const REPLACE_CONCURRENCY = Math.min(PARALLEL_CONCURRENCY, 8);
|
|
33
35
|
const MAX_CHANGED_FILES = 100;
|
|
36
|
+
const MAX_DIFF_SIZE = 20 * 1024; // 20KB limit for diff output
|
|
34
37
|
function recordFailure(failures, failure) {
|
|
35
38
|
if (failures.length >= MAX_FAILURES)
|
|
36
39
|
return;
|
|
@@ -110,15 +113,22 @@ async function processEntry(entryPath, args, regex, maxFileSize, signal, summary
|
|
|
110
113
|
summary.totalMatches += matchCount;
|
|
111
114
|
summary.filesChanged++;
|
|
112
115
|
recordChangedFile(summary, validPath, matchCount);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
let newContent;
|
|
117
|
+
if (args.isRegex && regex) {
|
|
118
|
+
regex.lastIndex = 0;
|
|
119
|
+
newContent = content.replace(regex, args.replacement);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
newContent = content.replaceAll(args.searchPattern, () => args.replacement);
|
|
123
|
+
}
|
|
124
|
+
if (args.dryRun && summary.diff.length < MAX_DIFF_SIZE) {
|
|
125
|
+
const patch = createTwoFilesPatch(path.basename(validPath), path.basename(validPath), content, newContent, 'Original', 'Modified');
|
|
126
|
+
// Only append if it won't exceed the limit too much
|
|
127
|
+
if (summary.diff.length + patch.length <= MAX_DIFF_SIZE + 1024) {
|
|
128
|
+
summary.diff += patch;
|
|
121
129
|
}
|
|
130
|
+
}
|
|
131
|
+
if (!args.dryRun) {
|
|
122
132
|
await atomicWriteFile(validPath, newContent, {
|
|
123
133
|
encoding: 'utf-8',
|
|
124
134
|
signal,
|
|
@@ -167,6 +177,7 @@ function createReplaceSummary(root) {
|
|
|
167
177
|
failures: [],
|
|
168
178
|
changedFiles: [],
|
|
169
179
|
changedFilesTruncated: false,
|
|
180
|
+
diff: '',
|
|
170
181
|
};
|
|
171
182
|
}
|
|
172
183
|
async function resolveSearchRoot(pathValue, signal) {
|
|
@@ -228,6 +239,7 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
|
228
239
|
? { changedFiles: summary.changedFiles }
|
|
229
240
|
: {}),
|
|
230
241
|
...(summary.changedFilesTruncated ? { changedFilesTruncated: true } : {}),
|
|
242
|
+
...(args.dryRun && summary.diff ? { diff: summary.diff } : {}),
|
|
231
243
|
dryRun: args.dryRun,
|
|
232
244
|
});
|
|
233
245
|
}
|
package/dist/tools/roots.js
CHANGED
|
@@ -3,6 +3,7 @@ import { ErrorCode } from '../lib/errors.js';
|
|
|
3
3
|
import { getAllowedDirectories } from '../lib/path-validation.js';
|
|
4
4
|
import { ListAllowedDirectoriesInputSchema, ListAllowedDirectoriesOutputSchema, } from '../schemas.js';
|
|
5
5
|
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
6
|
+
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
6
7
|
export const LIST_ALLOWED_DIRECTORIES_TOOL = {
|
|
7
8
|
name: 'roots',
|
|
8
9
|
title: 'Workspace Roots',
|
|
@@ -54,5 +55,7 @@ export function registerListAllowedDirectoriesTool(server, options = {}) {
|
|
|
54
55
|
},
|
|
55
56
|
});
|
|
56
57
|
const validatedHandler = withValidatedArgs(ListAllowedDirectoriesInputSchema, wrappedHandler);
|
|
58
|
+
if (registerToolTaskIfAvailable(server, 'roots', LIST_ALLOWED_DIRECTORIES_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
|
|
59
|
+
return;
|
|
57
60
|
server.registerTool('roots', withDefaultIcons({ ...LIST_ALLOWED_DIRECTORIES_TOOL }, options.iconInfo), validatedHandler);
|
|
58
61
|
}
|
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, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
|
+
import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, 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',
|
|
@@ -28,10 +28,11 @@ function formatFileInfoDetail(info) {
|
|
|
28
28
|
lines.push(` Target: ${info.symlinkTarget}`);
|
|
29
29
|
return joinLines(lines);
|
|
30
30
|
}
|
|
31
|
-
async function handleGetMultipleFileInfo(args, signal) {
|
|
31
|
+
async function handleGetMultipleFileInfo(args, signal, onProgress) {
|
|
32
32
|
const result = await getMultipleFileInfo(args.paths, {
|
|
33
33
|
includeMimeType: true,
|
|
34
34
|
...(signal ? { signal } : {}),
|
|
35
|
+
...(onProgress ? { onProgress } : {}),
|
|
35
36
|
});
|
|
36
37
|
const structuredResults = [];
|
|
37
38
|
const textBlocks = [];
|
|
@@ -71,30 +72,61 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
71
72
|
extra,
|
|
72
73
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
73
74
|
context: { path: primaryPath },
|
|
74
|
-
run: (signal) =>
|
|
75
|
+
run: async (signal) => {
|
|
76
|
+
const first = path.basename(args.paths[0] ?? '');
|
|
77
|
+
const extraPaths = args.paths.length > 1
|
|
78
|
+
? `, ${path.basename(args.paths[1] ?? '')}${args.paths.length > 2 ? '…' : ''}`
|
|
79
|
+
: '';
|
|
80
|
+
const context = `${args.paths.length} paths [${first}${extraPaths}]`;
|
|
81
|
+
let progressCursor = 0;
|
|
82
|
+
notifyProgress(extra, {
|
|
83
|
+
current: 0,
|
|
84
|
+
message: `🕮 stat_many: ${context}`,
|
|
85
|
+
});
|
|
86
|
+
const baseReporter = createProgressReporter(extra);
|
|
87
|
+
const onProgress = () => {
|
|
88
|
+
progressCursor++;
|
|
89
|
+
baseReporter({
|
|
90
|
+
current: progressCursor,
|
|
91
|
+
message: `🕮 stat_many: ${context} [${progressCursor}/${args.paths.length} scanned]`,
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
try {
|
|
95
|
+
const result = await handleGetMultipleFileInfo(args, signal, onProgress);
|
|
96
|
+
const sc = result.structuredContent;
|
|
97
|
+
const total = sc.summary?.total ?? 0;
|
|
98
|
+
const failed = sc.summary?.failed ?? 0;
|
|
99
|
+
const succeeded = sc.summary?.succeeded ?? 0;
|
|
100
|
+
let suffix;
|
|
101
|
+
if (failed) {
|
|
102
|
+
suffix = `${succeeded}/${total} OK, ${failed} failed`;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
suffix = `${total} OK`;
|
|
106
|
+
}
|
|
107
|
+
const finalCurrent = Math.max(total, progressCursor + 1);
|
|
108
|
+
notifyProgress(extra, {
|
|
109
|
+
current: finalCurrent,
|
|
110
|
+
total: finalCurrent,
|
|
111
|
+
message: `🕮 stat_many: ${context} • ${suffix}`,
|
|
112
|
+
});
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
117
|
+
notifyProgress(extra, {
|
|
118
|
+
current: finalCurrent,
|
|
119
|
+
total: finalCurrent,
|
|
120
|
+
message: `🕮 stat_many: ${context} • failed`,
|
|
121
|
+
});
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
},
|
|
75
125
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, primaryPath),
|
|
76
126
|
});
|
|
77
127
|
};
|
|
78
128
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
79
129
|
guard: options.isInitialized,
|
|
80
|
-
progressMessage: (args) => {
|
|
81
|
-
const first = path.basename(args.paths[0] ?? '');
|
|
82
|
-
const extra = args.paths.length > 1 ? `, ${path.basename(args.paths[1] ?? '')}…` : '';
|
|
83
|
-
return `🕮 stat_many: ${args.paths.length} paths [${first}${extra}]`;
|
|
84
|
-
},
|
|
85
|
-
completionMessage: (args, result) => {
|
|
86
|
-
if (result.isError)
|
|
87
|
-
return `🕮 stat_many: ${args.paths.length} paths • failed`;
|
|
88
|
-
const sc = result.structuredContent;
|
|
89
|
-
if (!sc.ok)
|
|
90
|
-
return `🕮 stat_many: ${args.paths.length} paths • failed`;
|
|
91
|
-
const total = sc.summary?.total ?? 0;
|
|
92
|
-
const succeeded = sc.summary?.succeeded ?? 0;
|
|
93
|
-
const failed = sc.summary?.failed ?? 0;
|
|
94
|
-
if (failed)
|
|
95
|
-
return `🕮 stat_many: ${succeeded}/${total} OK, ${failed} failed`;
|
|
96
|
-
return `🕮 stat_many: ${total} OK`;
|
|
97
|
-
},
|
|
98
130
|
});
|
|
99
131
|
const validatedHandler = withValidatedArgs(GetMultipleFileInfoInputSchema, wrappedHandler);
|
|
100
132
|
if (registerToolTaskIfAvailable(server, 'stat_many', GET_MULTIPLE_FILE_INFO_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
|
|
@@ -224,6 +224,14 @@ function getTaskId(extra) {
|
|
|
224
224
|
function isErrorResult(result) {
|
|
225
225
|
return 'isError' in result && result.isError === true;
|
|
226
226
|
}
|
|
227
|
+
// Strips structuredContent from a tool result if present, without modifying the original object. This is used when storing error results as 'completed' to prevent client-side output schema validation errors, while still allowing the human-readable error message in content[0].text to be returned to clients.
|
|
228
|
+
function withoutStructuredContent(result) {
|
|
229
|
+
if (!Object.hasOwn(result, 'structuredContent'))
|
|
230
|
+
return result;
|
|
231
|
+
const stripped = { ...result };
|
|
232
|
+
delete stripped['structuredContent'];
|
|
233
|
+
return stripped;
|
|
234
|
+
}
|
|
227
235
|
const TERMINAL_TASK_STATUSES = new Set([
|
|
228
236
|
'completed',
|
|
229
237
|
'failed',
|
|
@@ -254,8 +262,11 @@ async function tryStoreTaskResult(taskStore, taskId, status, result) {
|
|
|
254
262
|
}
|
|
255
263
|
async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName) {
|
|
256
264
|
try {
|
|
257
|
-
const
|
|
258
|
-
const status =
|
|
265
|
+
const rawResult = await run(args, extra);
|
|
266
|
+
const status = isCancelledToolResult(rawResult) ? 'failed' : 'completed';
|
|
267
|
+
const result = isErrorResult(rawResult) && status === 'completed'
|
|
268
|
+
? withoutStructuredContent(rawResult)
|
|
269
|
+
: maybeStripStructuredContentFromResult(rawResult);
|
|
259
270
|
await tryStoreTaskResult(taskStore, taskId, status, result);
|
|
260
271
|
publishTaskDiagnostics({
|
|
261
272
|
phase: 'task_result_stored',
|
package/dist/tools/tree.js
CHANGED
|
@@ -3,7 +3,7 @@ import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
|
3
3
|
import { ErrorCode } from '../lib/errors.js';
|
|
4
4
|
import { formatTreeAscii, treeDirectory } from '../lib/file-operations/tree.js';
|
|
5
5
|
import { TreeInputSchema, TreeOutputSchema } from '../schemas.js';
|
|
6
|
-
import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
6
|
+
import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
|
|
7
7
|
import { registerToolTaskIfAvailable } from './task-support.js';
|
|
8
8
|
export const TREE_TOOL = {
|
|
9
9
|
name: 'tree',
|
|
@@ -16,7 +16,7 @@ export const TREE_TOOL = {
|
|
|
16
16
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
17
17
|
gotchas: ['`maxDepth=0` returns only the root node.'],
|
|
18
18
|
};
|
|
19
|
-
async function handleTree(args, signal) {
|
|
19
|
+
async function handleTree(args, signal, onProgress) {
|
|
20
20
|
const basePath = resolvePathOrRoot(args.path);
|
|
21
21
|
const result = await treeDirectory(basePath, {
|
|
22
22
|
maxDepth: args.maxDepth,
|
|
@@ -24,6 +24,7 @@ async function handleTree(args, signal) {
|
|
|
24
24
|
includeHidden: args.includeHidden,
|
|
25
25
|
includeIgnored: args.includeIgnored,
|
|
26
26
|
...(signal ? { signal } : {}),
|
|
27
|
+
...(onProgress ? { onProgress } : {}),
|
|
27
28
|
});
|
|
28
29
|
const ascii = formatTreeAscii(result.tree);
|
|
29
30
|
const structured = {
|
|
@@ -45,30 +46,54 @@ export function registerTreeTool(server, options = {}) {
|
|
|
45
46
|
extra,
|
|
46
47
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
47
48
|
context: { path: targetPath },
|
|
48
|
-
run: (signal) =>
|
|
49
|
+
run: async (signal) => {
|
|
50
|
+
const context = args.path ? path.basename(args.path) : '.';
|
|
51
|
+
let progressCursor = 0;
|
|
52
|
+
notifyProgress(extra, {
|
|
53
|
+
current: 0,
|
|
54
|
+
message: `≣ tree: ${context}`,
|
|
55
|
+
});
|
|
56
|
+
const baseReporter = createProgressReporter(extra);
|
|
57
|
+
const onProgress = (progress) => {
|
|
58
|
+
const { current } = progress;
|
|
59
|
+
if (current > progressCursor)
|
|
60
|
+
progressCursor = current;
|
|
61
|
+
baseReporter({
|
|
62
|
+
current,
|
|
63
|
+
message: `≣ tree: ${context} [${current} entries]`,
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
const result = await handleTree(args, signal, onProgress);
|
|
68
|
+
const sc = result.structuredContent;
|
|
69
|
+
const count = sc.totalEntries ?? 0;
|
|
70
|
+
const { truncated } = sc;
|
|
71
|
+
let suffix = `${count} ${count === 1 ? 'entry' : 'entries'}`;
|
|
72
|
+
if (truncated)
|
|
73
|
+
suffix += ' [truncated]';
|
|
74
|
+
const finalCurrent = Math.max(count, progressCursor + 1);
|
|
75
|
+
notifyProgress(extra, {
|
|
76
|
+
current: finalCurrent,
|
|
77
|
+
total: finalCurrent,
|
|
78
|
+
message: `≣ tree: ${context} • ${suffix}`,
|
|
79
|
+
});
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
84
|
+
notifyProgress(extra, {
|
|
85
|
+
current: finalCurrent,
|
|
86
|
+
total: finalCurrent,
|
|
87
|
+
message: `≣ tree: ${context} • failed`,
|
|
88
|
+
});
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
49
92
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, targetPath),
|
|
50
93
|
});
|
|
51
94
|
};
|
|
52
95
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
53
96
|
guard: options.isInitialized,
|
|
54
|
-
progressMessage: (args) => {
|
|
55
|
-
if (args.path) {
|
|
56
|
-
return `≣ tree: ${path.basename(args.path)}`;
|
|
57
|
-
}
|
|
58
|
-
return '≣ tree';
|
|
59
|
-
},
|
|
60
|
-
completionMessage: (args, result) => {
|
|
61
|
-
const base = args.path ? path.basename(args.path) : '.';
|
|
62
|
-
if (result.isError)
|
|
63
|
-
return `≣ tree: ${base} • failed`;
|
|
64
|
-
const sc = result.structuredContent;
|
|
65
|
-
if (!sc.ok)
|
|
66
|
-
return `≣ tree: ${base} • failed`;
|
|
67
|
-
const count = sc.totalEntries ?? 0;
|
|
68
|
-
if (sc.truncated)
|
|
69
|
-
return `≣ tree: ${base} • ${count} entries [truncated]`;
|
|
70
|
-
return `≣ tree: ${base} • ${count} ${count === 1 ? 'entry' : 'entries'}`;
|
|
71
|
-
},
|
|
72
97
|
});
|
|
73
98
|
const validatedHandler = withValidatedArgs(TreeInputSchema, wrappedHandler);
|
|
74
99
|
if (registerToolTaskIfAvailable(server, 'tree', TREE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
|