@j0hanz/filesystem-mcp 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/completions.js +12 -0
- 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/lib/resource-store.d.ts +1 -0
- package/dist/lib/resource-store.js +4 -1
- package/dist/schemas.d.ts +1 -0
- package/dist/schemas.js +1 -0
- package/dist/server/bootstrap.js +16 -76
- package/dist/tools/contract.d.ts +4 -0
- 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 +54 -21
- package/dist/tools/replace-in-files.js +21 -8
- package/dist/tools/roots.js +3 -0
- package/dist/tools/search-content.js +1 -0
- package/dist/tools/search-files.js +1 -0
- package/dist/tools/shared.js +57 -35
- package/dist/tools/stat-many.js +54 -21
- package/dist/tools/task-support.js +6 -1
- package/dist/tools/tree.js +47 -21
- package/package.json +1 -1
package/dist/completions.js
CHANGED
|
@@ -7,6 +7,7 @@ import { isRecord } from './lib/type-guards.js';
|
|
|
7
7
|
const MAX_COMPLETION_ITEMS = 100;
|
|
8
8
|
const COMPLETION_RATE_LIMIT_MS = 100;
|
|
9
9
|
const completionLastCallMs = new Map();
|
|
10
|
+
const completionLastResult = new Map();
|
|
10
11
|
function extractTopicCompletions(instructions) {
|
|
11
12
|
const headers = [];
|
|
12
13
|
for (const line of instructions.split('\n')) {
|
|
@@ -394,6 +395,16 @@ export function registerCompletions(server, instructions = '') {
|
|
|
394
395
|
const now = Date.now();
|
|
395
396
|
const lastCallMs = completionLastCallMs.get(argName) ?? 0;
|
|
396
397
|
if (now - lastCallMs < COMPLETION_RATE_LIMIT_MS) {
|
|
398
|
+
const lastResult = completionLastResult.get(argName);
|
|
399
|
+
if (lastResult) {
|
|
400
|
+
return {
|
|
401
|
+
completion: {
|
|
402
|
+
values: lastResult.values,
|
|
403
|
+
total: lastResult.total,
|
|
404
|
+
hasMore: lastResult.hasMore,
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
397
408
|
return { completion: { values: [], total: 0, hasMore: false } };
|
|
398
409
|
}
|
|
399
410
|
completionLastCallMs.set(argName, now);
|
|
@@ -403,6 +414,7 @@ export function registerCompletions(server, instructions = '') {
|
|
|
403
414
|
argumentName: argName,
|
|
404
415
|
...(contextArguments ? { contextArguments } : {}),
|
|
405
416
|
});
|
|
417
|
+
completionLastResult.set(argName, completions);
|
|
406
418
|
return {
|
|
407
419
|
completion: {
|
|
408
420
|
values: completions.values,
|
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 {
|
|
@@ -147,5 +147,8 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
147
147
|
bytes: bytesBeforeClear,
|
|
148
148
|
});
|
|
149
149
|
}
|
|
150
|
-
|
|
150
|
+
function keys() {
|
|
151
|
+
return Array.from(byUri.keys());
|
|
152
|
+
}
|
|
153
|
+
return { putText, getText, clear, keys };
|
|
151
154
|
}
|
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/contract.d.ts
CHANGED
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',
|
|
@@ -14,17 +14,19 @@ export const READ_MULTIPLE_FILES_TOOL = {
|
|
|
14
14
|
inputSchema: ReadMultipleFilesInputSchema,
|
|
15
15
|
outputSchema: ReadMultipleFilesOutputSchema,
|
|
16
16
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
17
|
+
taskSupport: 'required',
|
|
17
18
|
nuances: ['Total read budget is capped by `MAX_READ_MANY_TOTAL_SIZE`.'],
|
|
18
19
|
gotchas: [
|
|
19
20
|
'Per-file `truncationReason` can be `head`, `range`, or `externalized`.',
|
|
20
21
|
],
|
|
21
22
|
};
|
|
22
|
-
async function handleReadMultipleFiles(args, signal, resourceStore) {
|
|
23
|
+
async function handleReadMultipleFiles(args, signal, resourceStore, onReadComplete) {
|
|
23
24
|
const options = {
|
|
24
25
|
...(signal ? { signal } : {}),
|
|
25
26
|
...(args.head !== undefined ? { head: args.head } : {}),
|
|
26
27
|
...(args.startLine !== undefined ? { startLine: args.startLine } : {}),
|
|
27
28
|
...(args.endLine !== undefined ? { endLine: args.endLine } : {}),
|
|
29
|
+
...(onReadComplete ? { onReadComplete } : {}),
|
|
28
30
|
};
|
|
29
31
|
const results = await readMultipleFiles(args.paths, options);
|
|
30
32
|
const maxTotalSize = DEFAULT_READ_MANY_MAX_TOTAL_SIZE;
|
|
@@ -122,30 +124,61 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
122
124
|
extra,
|
|
123
125
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
124
126
|
context: { path: primaryPath },
|
|
125
|
-
run: (signal) =>
|
|
127
|
+
run: async (signal) => {
|
|
128
|
+
const first = path.basename(args.paths[0] ?? '');
|
|
129
|
+
const extraPaths = args.paths.length > 1
|
|
130
|
+
? `, ${path.basename(args.paths[1] ?? '')}${args.paths.length > 2 ? '…' : ''}`
|
|
131
|
+
: '';
|
|
132
|
+
const context = `${args.paths.length} files [${first}${extraPaths}]`;
|
|
133
|
+
let progressCursor = 0;
|
|
134
|
+
notifyProgress(extra, {
|
|
135
|
+
current: 0,
|
|
136
|
+
message: `🕮 read_many: ${context}`,
|
|
137
|
+
});
|
|
138
|
+
const baseReporter = createProgressReporter(extra);
|
|
139
|
+
const onReadComplete = () => {
|
|
140
|
+
progressCursor++;
|
|
141
|
+
baseReporter({
|
|
142
|
+
current: progressCursor,
|
|
143
|
+
message: `🕮 read_many: ${context} [${progressCursor}/${args.paths.length} read]`,
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
try {
|
|
147
|
+
const result = await handleReadMultipleFiles(args, signal, options.resourceStore, onReadComplete);
|
|
148
|
+
const sc = result.structuredContent;
|
|
149
|
+
const total = sc.summary?.total ?? 0;
|
|
150
|
+
const failed = sc.summary?.failed ?? 0;
|
|
151
|
+
const succeeded = sc.summary?.succeeded ?? 0;
|
|
152
|
+
let suffix;
|
|
153
|
+
if (failed) {
|
|
154
|
+
suffix = `${succeeded}/${total} read, ${failed} failed`;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
suffix = `${total} files read`;
|
|
158
|
+
}
|
|
159
|
+
const finalCurrent = Math.max(total, progressCursor + 1);
|
|
160
|
+
notifyProgress(extra, {
|
|
161
|
+
current: finalCurrent,
|
|
162
|
+
total: finalCurrent,
|
|
163
|
+
message: `🕮 read_many: ${context} • ${suffix}`,
|
|
164
|
+
});
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
169
|
+
notifyProgress(extra, {
|
|
170
|
+
current: finalCurrent,
|
|
171
|
+
total: finalCurrent,
|
|
172
|
+
message: `🕮 read_many: ${context} • failed`,
|
|
173
|
+
});
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
},
|
|
126
177
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, primaryPath),
|
|
127
178
|
});
|
|
128
179
|
};
|
|
129
180
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
130
181
|
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
182
|
});
|
|
150
183
|
const validatedHandler = withValidatedArgs(ReadMultipleFilesInputSchema, wrappedHandler);
|
|
151
184
|
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,10 +18,12 @@ 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,
|
|
23
25
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
26
|
+
taskSupport: 'required',
|
|
24
27
|
gotchas: [
|
|
25
28
|
'Literal mode is default; `isRegex=true` enables RE2 + capture replacements (`$1`, `$2`).',
|
|
26
29
|
],
|
|
@@ -31,6 +34,7 @@ export const SEARCH_AND_REPLACE_TOOL = {
|
|
|
31
34
|
const MAX_FAILURES = 20;
|
|
32
35
|
const REPLACE_CONCURRENCY = Math.min(PARALLEL_CONCURRENCY, 8);
|
|
33
36
|
const MAX_CHANGED_FILES = 100;
|
|
37
|
+
const MAX_DIFF_SIZE = 20 * 1024; // 20KB limit for diff output
|
|
34
38
|
function recordFailure(failures, failure) {
|
|
35
39
|
if (failures.length >= MAX_FAILURES)
|
|
36
40
|
return;
|
|
@@ -110,15 +114,22 @@ async function processEntry(entryPath, args, regex, maxFileSize, signal, summary
|
|
|
110
114
|
summary.totalMatches += matchCount;
|
|
111
115
|
summary.filesChanged++;
|
|
112
116
|
recordChangedFile(summary, validPath, matchCount);
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
let newContent;
|
|
118
|
+
if (args.isRegex && regex) {
|
|
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 && summary.diff.length < MAX_DIFF_SIZE) {
|
|
126
|
+
const patch = createTwoFilesPatch(path.basename(validPath), path.basename(validPath), content, newContent, 'Original', 'Modified');
|
|
127
|
+
// Only append if it won't exceed the limit too much
|
|
128
|
+
if (summary.diff.length + patch.length <= MAX_DIFF_SIZE + 1024) {
|
|
129
|
+
summary.diff += patch;
|
|
121
130
|
}
|
|
131
|
+
}
|
|
132
|
+
if (!args.dryRun) {
|
|
122
133
|
await atomicWriteFile(validPath, newContent, {
|
|
123
134
|
encoding: 'utf-8',
|
|
124
135
|
signal,
|
|
@@ -167,6 +178,7 @@ function createReplaceSummary(root) {
|
|
|
167
178
|
failures: [],
|
|
168
179
|
changedFiles: [],
|
|
169
180
|
changedFilesTruncated: false,
|
|
181
|
+
diff: '',
|
|
170
182
|
};
|
|
171
183
|
}
|
|
172
184
|
async function resolveSearchRoot(pathValue, signal) {
|
|
@@ -228,6 +240,7 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
|
|
|
228
240
|
? { changedFiles: summary.changedFiles }
|
|
229
241
|
: {}),
|
|
230
242
|
...(summary.changedFilesTruncated ? { changedFilesTruncated: true } : {}),
|
|
243
|
+
...(args.dryRun && summary.diff ? { diff: summary.diff } : {}),
|
|
231
244
|
dryRun: args.dryRun,
|
|
232
245
|
});
|
|
233
246
|
}
|
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
|
}
|
|
@@ -38,6 +38,7 @@ export const SEARCH_FILES_TOOL = {
|
|
|
38
38
|
'Respects `.gitignore` unless `includeIgnored=true`.',
|
|
39
39
|
'Returns relative paths plus metadata; may truncate.',
|
|
40
40
|
],
|
|
41
|
+
taskSupport: 'required',
|
|
41
42
|
};
|
|
42
43
|
async function handleSearchFiles(args, signal, onProgress) {
|
|
43
44
|
const basePath = resolvePathOrRoot(args.path);
|
package/dist/tools/shared.js
CHANGED
|
@@ -125,6 +125,11 @@ function canSendProgress(extra) {
|
|
|
125
125
|
return (extra._meta?.progressToken !== undefined &&
|
|
126
126
|
extra.sendNotification !== undefined);
|
|
127
127
|
}
|
|
128
|
+
function canReportProgress(extra) {
|
|
129
|
+
const taskExtra = extra;
|
|
130
|
+
const hasTask = taskExtra.taskId !== undefined && taskExtra.taskStore !== undefined;
|
|
131
|
+
return canSendProgress(extra) || hasTask;
|
|
132
|
+
}
|
|
128
133
|
export function withDefaultIcons(tool, iconInfo) {
|
|
129
134
|
if (!iconInfo) {
|
|
130
135
|
return maybeStripOutputSchema(tool);
|
|
@@ -216,25 +221,53 @@ export function buildToolErrorResponse(error, defaultCode, path) {
|
|
|
216
221
|
function buildNotInitializedResult() {
|
|
217
222
|
return buildToolErrorResponse(NOT_INITIALIZED_ERROR, ErrorCode.E_INVALID_INPUT);
|
|
218
223
|
}
|
|
219
|
-
async function
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
async function reportProgress(extra, progress) {
|
|
225
|
+
const taskExtra = extra;
|
|
226
|
+
if (typeof taskExtra.taskId === 'string' &&
|
|
227
|
+
taskExtra.taskStore !== undefined &&
|
|
228
|
+
taskExtra.taskStore !== null) {
|
|
229
|
+
const store = taskExtra.taskStore;
|
|
230
|
+
if (typeof store.updateTaskStatus === 'function') {
|
|
231
|
+
try {
|
|
232
|
+
let statusMessage = progress.message;
|
|
233
|
+
if (progress.total !== undefined) {
|
|
234
|
+
statusMessage = statusMessage
|
|
235
|
+
? `${statusMessage} (${progress.current}/${progress.total})`
|
|
236
|
+
: `${progress.current}/${progress.total}`;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
statusMessage ??= `${progress.current}`;
|
|
240
|
+
}
|
|
241
|
+
await store.updateTaskStatus(taskExtra.taskId, 'working', statusMessage);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
console.error('Failed to update task status message:', error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
227
247
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
248
|
+
if (canSendProgress(extra)) {
|
|
249
|
+
try {
|
|
250
|
+
await extra.sendNotification({
|
|
251
|
+
method: 'notifications/progress',
|
|
252
|
+
params: {
|
|
253
|
+
progressToken: extra._meta.progressToken,
|
|
254
|
+
progress: progress.current,
|
|
255
|
+
...(progress.total !== undefined ? { total: progress.total } : {}),
|
|
256
|
+
...(progress.message !== undefined
|
|
257
|
+
? { message: progress.message }
|
|
258
|
+
: {}),
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
console.error('Failed to send progress notification:', error);
|
|
264
|
+
}
|
|
231
265
|
}
|
|
232
266
|
}
|
|
233
267
|
export function createProgressReporter(extra) {
|
|
234
|
-
if (!
|
|
268
|
+
if (!canReportProgress(extra)) {
|
|
235
269
|
return () => { };
|
|
236
270
|
}
|
|
237
|
-
const token = extra._meta.progressToken;
|
|
238
271
|
// State for monotonic enforcement and rate-limiting.
|
|
239
272
|
let lastProgress = -1;
|
|
240
273
|
let lastSentMs = 0;
|
|
@@ -251,52 +284,41 @@ export function createProgressReporter(extra) {
|
|
|
251
284
|
return;
|
|
252
285
|
lastProgress = current;
|
|
253
286
|
lastSentMs = now;
|
|
254
|
-
void
|
|
255
|
-
|
|
256
|
-
progress: current,
|
|
287
|
+
void reportProgress(extra, {
|
|
288
|
+
current,
|
|
257
289
|
...(total !== undefined ? { total } : {}),
|
|
258
290
|
...(message !== undefined ? { message } : {}),
|
|
259
291
|
});
|
|
260
292
|
};
|
|
261
293
|
}
|
|
262
294
|
export function notifyProgress(extra, progress) {
|
|
263
|
-
if (!
|
|
295
|
+
if (!canReportProgress(extra))
|
|
264
296
|
return;
|
|
265
|
-
|
|
266
|
-
void sendProgressNotification(extra, {
|
|
267
|
-
progressToken: token,
|
|
268
|
-
progress: progress.current,
|
|
269
|
-
...(progress.total !== undefined ? { total: progress.total } : {}),
|
|
270
|
-
...(progress.message !== undefined ? { message: progress.message } : {}),
|
|
271
|
-
});
|
|
297
|
+
void reportProgress(extra, progress);
|
|
272
298
|
}
|
|
273
299
|
async function withProgress(message, extra, run, getCompletionMessage) {
|
|
274
|
-
if (!
|
|
300
|
+
if (!canReportProgress(extra)) {
|
|
275
301
|
return run();
|
|
276
302
|
}
|
|
277
|
-
const token = extra._meta.progressToken;
|
|
278
303
|
const total = 1;
|
|
279
|
-
await
|
|
280
|
-
|
|
281
|
-
progress: 0,
|
|
304
|
+
await reportProgress(extra, {
|
|
305
|
+
current: 0,
|
|
282
306
|
total,
|
|
283
307
|
message,
|
|
284
308
|
});
|
|
285
309
|
try {
|
|
286
310
|
const result = await run();
|
|
287
311
|
const endMessage = getCompletionMessage?.(result) ?? message;
|
|
288
|
-
await
|
|
289
|
-
|
|
290
|
-
progress: total,
|
|
312
|
+
await reportProgress(extra, {
|
|
313
|
+
current: total,
|
|
291
314
|
total,
|
|
292
315
|
message: endMessage,
|
|
293
316
|
});
|
|
294
317
|
return result;
|
|
295
318
|
}
|
|
296
319
|
catch (error) {
|
|
297
|
-
void
|
|
298
|
-
|
|
299
|
-
progress: total,
|
|
320
|
+
void reportProgress(extra, {
|
|
321
|
+
current: total,
|
|
300
322
|
total,
|
|
301
323
|
message: `${message} • failed`,
|
|
302
324
|
});
|
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',
|
|
@@ -13,6 +13,7 @@ export const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
|
13
13
|
inputSchema: GetMultipleFileInfoInputSchema,
|
|
14
14
|
outputSchema: GetMultipleFileInfoOutputSchema,
|
|
15
15
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
16
|
+
taskSupport: 'required',
|
|
16
17
|
nuances: ['Use before read/search when file size/type uncertainty exists.'],
|
|
17
18
|
};
|
|
18
19
|
function formatFileInfoDetail(info) {
|
|
@@ -28,10 +29,11 @@ function formatFileInfoDetail(info) {
|
|
|
28
29
|
lines.push(` Target: ${info.symlinkTarget}`);
|
|
29
30
|
return joinLines(lines);
|
|
30
31
|
}
|
|
31
|
-
async function handleGetMultipleFileInfo(args, signal) {
|
|
32
|
+
async function handleGetMultipleFileInfo(args, signal, onProgress) {
|
|
32
33
|
const result = await getMultipleFileInfo(args.paths, {
|
|
33
34
|
includeMimeType: true,
|
|
34
35
|
...(signal ? { signal } : {}),
|
|
36
|
+
...(onProgress ? { onProgress } : {}),
|
|
35
37
|
});
|
|
36
38
|
const structuredResults = [];
|
|
37
39
|
const textBlocks = [];
|
|
@@ -71,30 +73,61 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
71
73
|
extra,
|
|
72
74
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
73
75
|
context: { path: primaryPath },
|
|
74
|
-
run: (signal) =>
|
|
76
|
+
run: async (signal) => {
|
|
77
|
+
const first = path.basename(args.paths[0] ?? '');
|
|
78
|
+
const extraPaths = args.paths.length > 1
|
|
79
|
+
? `, ${path.basename(args.paths[1] ?? '')}${args.paths.length > 2 ? '…' : ''}`
|
|
80
|
+
: '';
|
|
81
|
+
const context = `${args.paths.length} paths [${first}${extraPaths}]`;
|
|
82
|
+
let progressCursor = 0;
|
|
83
|
+
notifyProgress(extra, {
|
|
84
|
+
current: 0,
|
|
85
|
+
message: `🕮 stat_many: ${context}`,
|
|
86
|
+
});
|
|
87
|
+
const baseReporter = createProgressReporter(extra);
|
|
88
|
+
const onProgress = () => {
|
|
89
|
+
progressCursor++;
|
|
90
|
+
baseReporter({
|
|
91
|
+
current: progressCursor,
|
|
92
|
+
message: `🕮 stat_many: ${context} [${progressCursor}/${args.paths.length} scanned]`,
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
const result = await handleGetMultipleFileInfo(args, signal, onProgress);
|
|
97
|
+
const sc = result.structuredContent;
|
|
98
|
+
const total = sc.summary?.total ?? 0;
|
|
99
|
+
const failed = sc.summary?.failed ?? 0;
|
|
100
|
+
const succeeded = sc.summary?.succeeded ?? 0;
|
|
101
|
+
let suffix;
|
|
102
|
+
if (failed) {
|
|
103
|
+
suffix = `${succeeded}/${total} OK, ${failed} failed`;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
suffix = `${total} OK`;
|
|
107
|
+
}
|
|
108
|
+
const finalCurrent = Math.max(total, progressCursor + 1);
|
|
109
|
+
notifyProgress(extra, {
|
|
110
|
+
current: finalCurrent,
|
|
111
|
+
total: finalCurrent,
|
|
112
|
+
message: `🕮 stat_many: ${context} • ${suffix}`,
|
|
113
|
+
});
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
118
|
+
notifyProgress(extra, {
|
|
119
|
+
current: finalCurrent,
|
|
120
|
+
total: finalCurrent,
|
|
121
|
+
message: `🕮 stat_many: ${context} • failed`,
|
|
122
|
+
});
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
75
126
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, primaryPath),
|
|
76
127
|
});
|
|
77
128
|
};
|
|
78
129
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
79
130
|
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
131
|
});
|
|
99
132
|
const validatedHandler = withValidatedArgs(GetMultipleFileInfoInputSchema, wrappedHandler);
|
|
100
133
|
if (registerToolTaskIfAvailable(server, 'stat_many', GET_MULTIPLE_FILE_INFO_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
|
|
@@ -304,7 +304,12 @@ export function tryRegisterToolTask(server, toolName, toolDef, taskHandler, icon
|
|
|
304
304
|
const tasks = getExperimentalTaskRegistration(server);
|
|
305
305
|
if (!tasks?.registerToolTask)
|
|
306
306
|
return false;
|
|
307
|
-
|
|
307
|
+
const def = toolDef;
|
|
308
|
+
const existingExecution = def.execution ?? {};
|
|
309
|
+
const taskSupport = def.taskSupport ??
|
|
310
|
+
existingExecution.taskSupport ??
|
|
311
|
+
'optional';
|
|
312
|
+
tasks.registerToolTask(toolName, withDefaultIcons({ ...toolDef, execution: { ...existingExecution, taskSupport } }, iconInfo), taskHandler);
|
|
308
313
|
return true;
|
|
309
314
|
}
|
|
310
315
|
export function registerToolTaskIfAvailable(server, toolName, toolDef, run, iconInfo, guard) {
|
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',
|
|
@@ -14,9 +14,10 @@ export const TREE_TOOL = {
|
|
|
14
14
|
inputSchema: TreeInputSchema,
|
|
15
15
|
outputSchema: TreeOutputSchema,
|
|
16
16
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
17
|
+
taskSupport: 'required',
|
|
17
18
|
gotchas: ['`maxDepth=0` returns only the root node.'],
|
|
18
19
|
};
|
|
19
|
-
async function handleTree(args, signal) {
|
|
20
|
+
async function handleTree(args, signal, onProgress) {
|
|
20
21
|
const basePath = resolvePathOrRoot(args.path);
|
|
21
22
|
const result = await treeDirectory(basePath, {
|
|
22
23
|
maxDepth: args.maxDepth,
|
|
@@ -24,6 +25,7 @@ async function handleTree(args, signal) {
|
|
|
24
25
|
includeHidden: args.includeHidden,
|
|
25
26
|
includeIgnored: args.includeIgnored,
|
|
26
27
|
...(signal ? { signal } : {}),
|
|
28
|
+
...(onProgress ? { onProgress } : {}),
|
|
27
29
|
});
|
|
28
30
|
const ascii = formatTreeAscii(result.tree);
|
|
29
31
|
const structured = {
|
|
@@ -45,30 +47,54 @@ export function registerTreeTool(server, options = {}) {
|
|
|
45
47
|
extra,
|
|
46
48
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
47
49
|
context: { path: targetPath },
|
|
48
|
-
run: (signal) =>
|
|
50
|
+
run: async (signal) => {
|
|
51
|
+
const context = args.path ? path.basename(args.path) : '.';
|
|
52
|
+
let progressCursor = 0;
|
|
53
|
+
notifyProgress(extra, {
|
|
54
|
+
current: 0,
|
|
55
|
+
message: `≣ tree: ${context}`,
|
|
56
|
+
});
|
|
57
|
+
const baseReporter = createProgressReporter(extra);
|
|
58
|
+
const onProgress = (progress) => {
|
|
59
|
+
const { current } = progress;
|
|
60
|
+
if (current > progressCursor)
|
|
61
|
+
progressCursor = current;
|
|
62
|
+
baseReporter({
|
|
63
|
+
current,
|
|
64
|
+
message: `≣ tree: ${context} [${current} entries]`,
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
try {
|
|
68
|
+
const result = await handleTree(args, signal, onProgress);
|
|
69
|
+
const sc = result.structuredContent;
|
|
70
|
+
const count = sc.totalEntries ?? 0;
|
|
71
|
+
const { truncated } = sc;
|
|
72
|
+
let suffix = `${count} ${count === 1 ? 'entry' : 'entries'}`;
|
|
73
|
+
if (truncated)
|
|
74
|
+
suffix += ' [truncated]';
|
|
75
|
+
const finalCurrent = Math.max(count, progressCursor + 1);
|
|
76
|
+
notifyProgress(extra, {
|
|
77
|
+
current: finalCurrent,
|
|
78
|
+
total: finalCurrent,
|
|
79
|
+
message: `≣ tree: ${context} • ${suffix}`,
|
|
80
|
+
});
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
85
|
+
notifyProgress(extra, {
|
|
86
|
+
current: finalCurrent,
|
|
87
|
+
total: finalCurrent,
|
|
88
|
+
message: `≣ tree: ${context} • failed`,
|
|
89
|
+
});
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
49
93
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, targetPath),
|
|
50
94
|
});
|
|
51
95
|
};
|
|
52
96
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
53
97
|
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
98
|
});
|
|
73
99
|
const validatedHandler = withValidatedArgs(TreeInputSchema, wrappedHandler);
|
|
74
100
|
if (registerToolTaskIfAvailable(server, 'tree', TREE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
|