@j0hanz/filesystem-mcp 1.3.2 → 1.5.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,
package/dist/config.d.ts CHANGED
@@ -112,8 +112,9 @@ export declare const ErrorCode: {
112
112
  export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
113
113
  export declare function formatBytes(bytes: number): string;
114
114
  export declare function joinLines(lines: readonly string[]): string;
115
- export interface OperationSummary {
115
+ interface OperationSummary {
116
116
  truncated?: boolean;
117
117
  truncatedReason?: string;
118
118
  }
119
119
  export declare function formatOperationSummary(summary: OperationSummary): string;
120
+ export {};
@@ -1,5 +1,3 @@
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
1
  export declare const DEFAULT_LOG_LEVEL: "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency";
4
2
  export declare const PARALLEL_CONCURRENCY: number;
5
3
  export declare const MAX_SEARCHABLE_FILE_SIZE: number;
@@ -22,4 +20,3 @@ export declare const SENSITIVE_FILE_ALLOWLIST: string[];
22
20
  export declare const KNOWN_BINARY_EXTENSIONS: Set<string>;
23
21
  export declare const DEFAULT_EXCLUDE_PATTERNS: string[];
24
22
  export declare function getMimeType(ext: string): string;
25
- export {};
@@ -5,7 +5,7 @@ interface OpsTraceContext {
5
5
  path?: string | undefined;
6
6
  [key: string]: unknown;
7
7
  }
8
- export interface ToolMetrics {
8
+ interface ToolMetrics {
9
9
  calls: number;
10
10
  errors: number;
11
11
  totalDurationMs: number;
@@ -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
  }
@@ -1,4 +1,6 @@
1
- import { ALL_TOOLS } from '../tools.js';
1
+ import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
2
+ import { buildCoreContextPack, getSharedConstraints, getToolContracts, } from './tool-info.js';
3
+ import { buildWorkflowGuide } from './workflows.js';
2
4
  const INSTRUCTIONS_HEADER = `# FILESYSTEM-MCP INSTRUCTIONS
3
5
 
4
6
  > Resource: \`internal://instructions\` | Prompt: \`get-help\`
@@ -24,39 +26,15 @@ const INSTRUCTIONS_HEADER = `# FILESYSTEM-MCP INSTRUCTIONS
24
26
 
25
27
  ## GOLDEN PATH WORKFLOWS
26
28
 
27
- ### A: EXPLORE
28
- 1. \`roots\` (List allowed paths).
29
- 2. \`ls\` (files) | \`tree\` (structure).
30
- 3. \`stat\` | \`stat_many\` (size/type check).
31
- 4. \`read\` | \`read_many\` (content).
32
- > **Strict:** Never guess paths. Resolve first.
29
+ See "Workflow Reference" below for detailed execution sequences.
33
30
 
34
- ### B: SEARCH
35
- 1. \`find\` (glob candidates).
36
- 2. \`grep\` (content search).
37
- 3. \`read\` (verify context).
38
- > **Tip:** Content search requires \`grep\`, not \`find\`.
39
-
40
- ### C: EDIT
41
- 1. \`edit\` (precise string match).
42
- 2. \`search_and_replace\` (bulk regex/glob).
43
- 3. \`mv\` | \`rm\` (file layout).
44
- 4. \`mkdir\` (create dirs).
45
- > **Strict:** Confirm destructive ops (\`write\`, \`mv\`, \`rm\`, bulk replace).
46
-
47
- ### D: PATCH
48
- 1. \`diff_files\` (generate).
49
- 2. \`apply_patch\` (dryRun: true).
50
- 3. \`apply_patch\` (dryRun: false).
51
- > **Tip:** Use \`diff_files\` output directly.
52
31
  `;
53
32
  const INSTRUCTIONS_FOOTER = `
54
33
  ## CONSTRAINTS
55
34
 
56
- - **Scope:** Allowed roots only (negotiated via CLI).
57
- - **Security:** Sensitive files denylisted by default.
58
- - **Limits:** Max file size & search results enforced.
59
- - **Cache:** Externalized results are ephemeral (in-memory).
35
+ ${getSharedConstraints()
36
+ .map((c) => `- ${c}`)
37
+ .join('\n')}
60
38
 
61
39
  ## ERROR HANDLING
62
40
 
@@ -87,13 +65,19 @@ function formatToolSection(tool) {
87
65
  return parts.join('\n');
88
66
  }
89
67
  export function buildServerInstructions() {
90
- const toolSections = ALL_TOOLS.map(formatToolSection).join('\n\n');
68
+ const toolSections = getToolContracts().map(formatToolSection).join('\n\n');
91
69
  return [
92
70
  INSTRUCTIONS_HEADER,
71
+ buildCoreContextPack(),
72
+ '',
73
+ buildToolCatalogDetailsOnly(),
74
+ '',
93
75
  '## TOOL REFERENCE',
94
76
  '',
95
77
  toolSections,
96
78
  '',
79
+ buildWorkflowGuide(),
80
+ '',
97
81
  '---',
98
82
  INSTRUCTIONS_FOOTER,
99
83
  ].join('\n');
@@ -0,0 +1,2 @@
1
+ export declare function buildToolCatalog(): string;
2
+ export declare function buildToolCatalogDetailsOnly(): string;
@@ -0,0 +1,29 @@
1
+ import { buildCoreContextPack } from './tool-info.js';
2
+ const CATALOG_GUIDE = `## Tool Catalog Details
3
+
4
+ ## Cross-Tool Data Flow
5
+
6
+ \`\`\`
7
+ find -> output_paths -> grep.paths
8
+ diff_files -> output_patch -> apply_patch.patch
9
+ \`\`\`
10
+
11
+ ## Search Strategy Strategy
12
+
13
+ - Use \`find\` for glob-based file discovery.
14
+ - Use \`grep\` for content-based searches.
15
+ - Use \`search_and_replace\` ONLY for bulk replacements, not for discovery.
16
+
17
+ ## Patch Management
18
+
19
+ - Always generate a patch with \`diff_files\` first.
20
+ - Always use \`dryRun: true\` with \`apply_patch\` to verify changes.
21
+ - \`apply_patch\` works on unified diff format.
22
+ `;
23
+ export function buildToolCatalog() {
24
+ // Return combined view for standalone resource usage
25
+ return `${buildCoreContextPack()}\n\n${CATALOG_GUIDE}`;
26
+ }
27
+ export function buildToolCatalogDetailsOnly() {
28
+ return CATALOG_GUIDE;
29
+ }
@@ -0,0 +1,4 @@
1
+ import type { ToolContract } from '../tools/contract.js';
2
+ export declare function getToolContracts(): ToolContract[];
3
+ export declare function buildCoreContextPack(): string;
4
+ export declare function getSharedConstraints(): string[];
@@ -0,0 +1,44 @@
1
+ import { ALL_TOOLS } from '../tools.js';
2
+ function toEntry(contract) {
3
+ const annotations = [];
4
+ if (contract.annotations?.destructiveHint)
5
+ annotations.push('[Destructive]');
6
+ if (contract.annotations?.idempotentHint)
7
+ annotations.push('[Idempotent]');
8
+ if (contract.annotations?.readOnlyHint)
9
+ annotations.push('[Read-Only]');
10
+ return {
11
+ name: contract.name,
12
+ description: contract.description,
13
+ ...(annotations.length > 0 ? { annotations } : {}),
14
+ ...(contract.nuances && contract.nuances.length > 0
15
+ ? { nuances: contract.nuances }
16
+ : {}),
17
+ ...(contract.gotchas && contract.gotchas.length > 0
18
+ ? { gotchas: contract.gotchas }
19
+ : {}),
20
+ };
21
+ }
22
+ const ENTRIES = Object.fromEntries(ALL_TOOLS.map((contract) => [contract.name, toEntry(contract)]));
23
+ export function getToolContracts() {
24
+ return ALL_TOOLS;
25
+ }
26
+ export function buildCoreContextPack() {
27
+ const names = Object.keys(ENTRIES).sort((a, b) => a.localeCompare(b));
28
+ const rows = names.map((name) => {
29
+ const e = ENTRIES[name];
30
+ if (!e)
31
+ return '';
32
+ const annotations = e.annotations ? ` ${e.annotations.join(' ')}` : '';
33
+ return `| \`${e.name}\` | ${e.description}${annotations} |`;
34
+ });
35
+ return `## Core Context Pack\n\n| Tool | Purpose |\n|------|---------|\n${rows.join('\n')}`;
36
+ }
37
+ export function getSharedConstraints() {
38
+ return [
39
+ 'Allowed roots only (negotiated via CLI).',
40
+ 'Sensitive files denylisted by default.',
41
+ 'Max file size & search results enforced.',
42
+ 'Externalized results are ephemeral (in-memory).',
43
+ ];
44
+ }
@@ -0,0 +1 @@
1
+ export declare function buildWorkflowGuide(): string;
@@ -0,0 +1,36 @@
1
+ import { getSharedConstraints } from './tool-info.js';
2
+ export function buildWorkflowGuide() {
3
+ return `## Workflow Reference
4
+
5
+ ### A: EXPLORE
6
+ 1. \`roots\` (List allowed paths).
7
+ 2. \`ls\` (files) | \`tree\` (structure).
8
+ 3. \`stat\` | \`stat_many\` (size/type check).
9
+ 4. \`read\` | \`read_many\` (content).
10
+ > **Strict:** Never guess paths. Resolve first.
11
+
12
+ ### B: SEARCH
13
+ 1. \`find\` (glob candidates).
14
+ 2. \`grep\` (content search).
15
+ 3. \`read\` (verify context).
16
+ > **Tip:** Content search requires \`grep\`, not \`find\`.
17
+
18
+ ### C: EDIT
19
+ 1. \`edit\` (precise string match).
20
+ 2. \`search_and_replace\` (bulk regex/glob).
21
+ 3. \`mv\` | \`rm\` (file layout).
22
+ 4. \`mkdir\` (create dirs).
23
+ > **Strict:** Confirm destructive ops (\`write\`, \`mv\`, \`rm\`, bulk replace).
24
+
25
+ ### D: PATCH
26
+ 1. \`diff_files\` (generate).
27
+ 2. \`apply_patch\` (dryRun: true).
28
+ 3. \`apply_patch\` (dryRun: false).
29
+ > **Tip:** Use \`diff_files\` output directly.
30
+
31
+ ## Shared Constraints
32
+ ${getSharedConstraints()
33
+ .map((c) => `- ${c}`)
34
+ .join('\n')}
35
+ `;
36
+ }
@@ -2,5 +2,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { ResourceStore } from './lib/resource-store.js';
3
3
  import { type IconInfo } from './tools/shared.js';
4
4
  export declare function registerInstructionResource(server: McpServer, instructions: string, iconInfo?: IconInfo): void;
5
+ export declare function registerToolCatalogResource(server: McpServer, iconInfo?: IconInfo): void;
6
+ export declare function registerWorkflowGuideResource(server: McpServer, iconInfo?: IconInfo): void;
5
7
  export declare function registerResultResources(server: McpServer, store: ResourceStore, iconInfo?: IconInfo): void;
6
8
  export declare function registerMetricsResource(server: McpServer, iconInfo?: IconInfo): void;
package/dist/resources.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { ErrorCode, McpError } from './lib/errors.js';
3
3
  import { globalMetrics } from './lib/observability.js';
4
+ import { buildToolCatalog } from './resources/tool-catalog.js';
5
+ import { buildWorkflowGuide } from './resources/workflows.js';
4
6
  import { withDefaultIcons } from './tools/shared.js';
5
7
  const RESULT_TEMPLATE = new ResourceTemplate('filesystem-mcp://result/{id}', {
6
8
  list: undefined,
@@ -13,6 +15,12 @@ const RESULT_RESOURCE_DESCRIPTION = 'Ephemeral cached tool output exposed as an
13
15
  const METRICS_RESOURCE_NAME = 'filesystem-mcp-metrics';
14
16
  const METRICS_RESOURCE_URI = 'filesystem-mcp://metrics';
15
17
  const METRICS_RESOURCE_DESCRIPTION = 'Live per-tool call/error/avgDurationMs metrics snapshot.';
18
+ const CATALOG_RESOURCE_NAME = 'filesystem-mcp-catalog';
19
+ const CATALOG_RESOURCE_URI = 'internal://tool-catalog';
20
+ const CATALOG_RESOURCE_DESCRIPTION = 'Detailed catalog of tools and their inter-dependencies.';
21
+ const WORKFLOW_RESOURCE_NAME = 'filesystem-mcp-workflows';
22
+ const WORKFLOW_RESOURCE_URI = 'internal://workflows';
23
+ const WORKFLOW_RESOURCE_DESCRIPTION = 'Recommended workflows for common tasks.';
16
24
  export function registerInstructionResource(server, instructions, iconInfo) {
17
25
  server.registerResource(INSTRUCTIONS_RESOURCE_NAME, INSTRUCTIONS_RESOURCE_URI, withDefaultIcons({
18
26
  title: 'Server Instructions',
@@ -32,6 +40,44 @@ export function registerInstructionResource(server, instructions, iconInfo) {
32
40
  ],
33
41
  }));
34
42
  }
43
+ export function registerToolCatalogResource(server, iconInfo) {
44
+ server.registerResource(CATALOG_RESOURCE_NAME, CATALOG_RESOURCE_URI, withDefaultIcons({
45
+ title: 'Tool Catalog',
46
+ description: CATALOG_RESOURCE_DESCRIPTION,
47
+ mimeType: 'text/markdown',
48
+ annotations: {
49
+ audience: ['assistant'],
50
+ priority: 0.6,
51
+ },
52
+ }, iconInfo), (uri) => ({
53
+ contents: [
54
+ {
55
+ uri: uri.href,
56
+ mimeType: 'text/markdown',
57
+ text: buildToolCatalog(),
58
+ },
59
+ ],
60
+ }));
61
+ }
62
+ export function registerWorkflowGuideResource(server, iconInfo) {
63
+ server.registerResource(WORKFLOW_RESOURCE_NAME, WORKFLOW_RESOURCE_URI, withDefaultIcons({
64
+ title: 'Workflow Guide',
65
+ description: WORKFLOW_RESOURCE_DESCRIPTION,
66
+ mimeType: 'text/markdown',
67
+ annotations: {
68
+ audience: ['assistant'],
69
+ priority: 0.7,
70
+ },
71
+ }, iconInfo), (uri) => ({
72
+ contents: [
73
+ {
74
+ uri: uri.href,
75
+ mimeType: 'text/markdown',
76
+ text: buildWorkflowGuide(),
77
+ },
78
+ ],
79
+ }));
80
+ }
35
81
  export function registerResultResources(server, store, iconInfo) {
36
82
  server.registerResource(RESULT_RESOURCE_NAME, RESULT_TEMPLATE, withDefaultIcons({
37
83
  title: 'Cached Tool Result',
@@ -12,7 +12,7 @@ import { formatUnknownErrorMessage } from '../lib/errors.js';
12
12
  import { createInMemoryResourceStore } from '../lib/resource-store.js';
13
13
  import { pkgInfo } from '../pkg-info.js';
14
14
  import { registerGetHelpPrompt } from '../prompts.js';
15
- import { registerInstructionResource, registerMetricsResource, registerResultResources, } from '../resources.js';
15
+ import { registerInstructionResource, registerMetricsResource, registerResultResources, registerToolCatalogResource, registerWorkflowGuideResource, } from '../resources.js';
16
16
  import { buildServerInstructions } from '../resources/generated-instructions.js';
17
17
  import { registerAllTools } from '../tools.js';
18
18
  import { withDefaultIcons } from '../tools/shared.js';
@@ -86,6 +86,8 @@ export async function createServer(options = {}) {
86
86
  return {};
87
87
  });
88
88
  registerInstructionResource(server, serverInstructions, localIcon);
89
+ registerToolCatalogResource(server, localIcon);
90
+ registerWorkflowGuideResource(server, localIcon);
89
91
  registerGetHelpPrompt(server, serverInstructions, localIcon);
90
92
  registerResultResources(server, resourceStore, localIcon);
91
93
  registerMetricsResource(server, localIcon);
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export interface CapabilityOptions {
2
+ interface CapabilityOptions {
3
3
  enablePromptListChanged?: boolean;
4
4
  enableTaskToolRequests?: boolean;
5
5
  }
package/dist/server.d.ts CHANGED
@@ -1,2 +1 @@
1
1
  export { createServer, startHttpServer, startServer, } from './server/bootstrap.js';
2
- export type { ServerOptions } from './server/types.js';
@@ -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);
@@ -48,7 +48,6 @@ interface ToolErrorResponse extends Record<string, unknown> {
48
48
  isError: true;
49
49
  }
50
50
  export type ToolResult<T> = ToolResponse<T> | ToolErrorResponse;
51
- export declare function parseToolArgs<Schema extends z.ZodType>(schema: Schema, args: unknown): z.infer<Schema>;
52
51
  export declare function withValidatedArgs<Args, Result>(schema: z.ZodType<Args>, handler: (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>): (args: unknown, extra: ToolExtra) => Promise<ToolResult<Result>>;
53
52
  type ProgressToken = string | number;
54
53
  export interface ToolExtra {
@@ -107,7 +107,7 @@ function resolveDetailedError(error, defaultCode, path) {
107
107
  export function buildToolResponse(text, structuredContent, extraContent = []) {
108
108
  return buildContentBlock(text, structuredContent, extraContent);
109
109
  }
110
- export function parseToolArgs(schema, args) {
110
+ function parseToolArgs(schema, args) {
111
111
  const candidate = args === undefined ? {} : args;
112
112
  const parsed = schema.safeParse(candidate);
113
113
  if (parsed.success) {
@@ -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) {
@@ -9,12 +9,6 @@ type TaskToolExtra = ToolExtra & {
9
9
  taskRequestedTtl?: number | null;
10
10
  };
11
11
  type ToolArgs<Args extends ZodRawShapeCompat | AnySchema | undefined> = Args extends ZodRawShapeCompat ? ShapeOutput<Args> : Args extends AnySchema ? SchemaOutput<Args> : undefined;
12
- /**
13
- * Registers a tool preferring task-capable registration when available, and
14
- * returns `true`. Returns `false` so the caller can fall through to standard
15
- * `server.registerTool`.
16
- */
17
- export declare function tryRegisterToolTask<Args extends ZodRawShapeCompat | AnySchema | undefined>(server: McpServer, toolName: string, toolDef: object, taskHandler: ToolTaskHandler<Args>, iconInfo: IconInfo | undefined): boolean;
18
12
  export declare function registerToolTaskIfAvailable<Args extends ZodRawShapeCompat | AnySchema | undefined, Result>(server: McpServer, toolName: string, toolDef: object, run: (args: ToolArgs<Args>, extra: TaskToolExtra) => Promise<ToolResult<Result>>, iconInfo: IconInfo | undefined, guard?: () => boolean): boolean;
19
13
  export declare function createToolTaskHandler<Result>(run: (args: undefined, extra: TaskToolExtra) => Promise<ToolResult<Result>>, options?: {
20
14
  guard?: () => boolean;
@@ -298,13 +298,18 @@ async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName
298
298
  * returns `true`. Returns `false` so the caller can fall through to standard
299
299
  * `server.registerTool`.
300
300
  */
301
- export function tryRegisterToolTask(server, toolName, toolDef, taskHandler, iconInfo) {
301
+ function tryRegisterToolTask(server, toolName, toolDef, taskHandler, iconInfo) {
302
302
  if (!hasTaskToolCapability(server))
303
303
  return false;
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.5.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",