@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.
@@ -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,
@@ -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 {};
@@ -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
- index,
94
- value: await processFileInfo(filePath, options),
95
- }), PARALLEL_CONCURRENCY, options.signal);
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) => readSingleFile(task, readOptions), PARALLEL_CONCURRENCY, signal);
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);
@@ -12,6 +12,10 @@ interface TreeOptions {
12
12
  includeIgnored?: boolean;
13
13
  timeoutMs?: number;
14
14
  signal?: AbortSignal;
15
+ onProgress?: (progress: {
16
+ total?: number;
17
+ current: number;
18
+ }) => void;
15
19
  }
16
20
  interface TreeResult {
17
21
  root: string;
@@ -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 {
@@ -15,6 +15,7 @@ export interface ResourceStore {
15
15
  }): TextResourceEntry;
16
16
  getText(uri: string): TextResourceEntry;
17
17
  clear(): void;
18
+ keys(): string[];
18
19
  }
19
20
  interface ResourceStoreOptions {
20
21
  maxEntries: number;
@@ -147,5 +147,8 @@ export function createInMemoryResourceStore(options = {}) {
147
147
  bytes: bytesBeforeClear,
148
148
  });
149
149
  }
150
- return { putText, getText, clear };
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
  });
@@ -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('debug');
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.writeHead(400, { 'Content-Type': 'application/json' });
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.writeHead(400, { 'Content-Type': 'application/json' });
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.writeHead(400, { 'Content-Type': 'application/json' });
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.writeHead(500, { 'Content-Type': 'application/json' });
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
  }
@@ -38,4 +38,8 @@ export interface ToolContract {
38
38
  * Common pitfalls or warnings for documentation.
39
39
  */
40
40
  gotchas?: string[];
41
+ /**
42
+ * Task support level for the tool. Defaults to 'optional'.
43
+ */
44
+ taskSupport?: 'optional' | 'required' | 'forbidden';
41
45
  }
@@ -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
  }
@@ -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) => handleReadMultipleFiles(args, signal, options.resourceStore),
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
- if (!args.dryRun) {
114
- let newContent;
115
- if (args.isRegex && regex) {
116
- regex.lastIndex = 0;
117
- newContent = content.replace(regex, args.replacement);
118
- }
119
- else {
120
- newContent = content.replaceAll(args.searchPattern, () => args.replacement);
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
  }
@@ -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
  }
@@ -26,6 +26,7 @@ export const SEARCH_CONTENT_TOOL = {
26
26
  gotchas: [
27
27
  'Inline match rows are capped (first 50); full structured results are externalized via `resourceUri`.',
28
28
  ],
29
+ taskSupport: 'required',
29
30
  };
30
31
  function assertValidRegexPattern(pattern) {
31
32
  try {
@@ -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);
@@ -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 sendProgressNotification(extra, params) {
220
- if (!canSendProgress(extra))
221
- return;
222
- try {
223
- await extra.sendNotification({
224
- method: 'notifications/progress',
225
- params,
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
- catch (error) {
229
- // Ignore progress notification failures to avoid breaking tool execution.
230
- console.error('Failed to send progress notification:', error);
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 (!canSendProgress(extra)) {
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 sendProgressNotification(extra, {
255
- progressToken: token,
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 (!canSendProgress(extra))
295
+ if (!canReportProgress(extra))
264
296
  return;
265
- const token = extra._meta.progressToken;
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 (!canSendProgress(extra)) {
300
+ if (!canReportProgress(extra)) {
275
301
  return run();
276
302
  }
277
- const token = extra._meta.progressToken;
278
303
  const total = 1;
279
- await sendProgressNotification(extra, {
280
- progressToken: token,
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 sendProgressNotification(extra, {
289
- progressToken: token,
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 sendProgressNotification(extra, {
298
- progressToken: token,
299
- progress: total,
320
+ void reportProgress(extra, {
321
+ current: total,
300
322
  total,
301
323
  message: `${message} • failed`,
302
324
  });
@@ -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) => handleGetMultipleFileInfo(args, 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
- tasks.registerToolTask(toolName, withDefaultIcons({ ...toolDef, execution: { taskSupport: 'optional' } }, iconInfo), taskHandler);
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) {
@@ -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) => handleTree(args, 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))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "mcpName": "io.github.j0hanz/filesystem-mcp",
5
5
  "description": "MCP Server that enables LLMs to interact with the local filesystem.",
6
6
  "type": "module",