@j0hanz/filesystem-mcp 1.3.2 → 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,
@@ -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
  }
@@ -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
  }
@@ -14,6 +14,7 @@ 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`.',
@@ -23,6 +23,7 @@ export const SEARCH_AND_REPLACE_TOOL = {
23
23
  inputSchema: SearchAndReplaceInputSchema,
24
24
  outputSchema: SearchAndReplaceOutputSchema,
25
25
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
26
+ taskSupport: 'required',
26
27
  gotchas: [
27
28
  'Literal mode is default; `isRegex=true` enables RE2 + capture replacements (`$1`, `$2`).',
28
29
  ],
@@ -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
  });
@@ -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) {
@@ -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) {
@@ -14,6 +14,7 @@ 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
20
  async function handleTree(args, signal, onProgress) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.3.2",
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",