@j0hanz/filesystem-mcp 1.2.4 → 1.3.1

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.
Files changed (62) hide show
  1. package/README.md +8 -0
  2. package/dist/completions.d.ts +1 -1
  3. package/dist/completions.js +36 -1
  4. package/dist/lib/observability.d.ts +6 -0
  5. package/dist/lib/observability.js +1 -1
  6. package/dist/lib/resource-store.js +53 -0
  7. package/dist/prompts.js +34 -14
  8. package/dist/resources/generated-instructions.d.ts +1 -0
  9. package/dist/resources/generated-instructions.js +100 -0
  10. package/dist/resources.d.ts +1 -0
  11. package/dist/resources.js +36 -1
  12. package/dist/schemas.d.ts +6 -0
  13. package/dist/schemas.js +24 -0
  14. package/dist/server/bootstrap.js +45 -22
  15. package/dist/server.d.ts +1 -1
  16. package/dist/server.js +1 -1
  17. package/dist/tools/apply-patch.d.ts +2 -1
  18. package/dist/tools/apply-patch.js +7 -5
  19. package/dist/tools/calculate-hash.d.ts +2 -1
  20. package/dist/tools/calculate-hash.js +9 -5
  21. package/dist/tools/contract.d.ts +41 -0
  22. package/dist/tools/contract.js +1 -0
  23. package/dist/tools/create-directory.d.ts +2 -1
  24. package/dist/tools/create-directory.js +6 -5
  25. package/dist/tools/delete-file.d.ts +2 -1
  26. package/dist/tools/delete-file.js +9 -5
  27. package/dist/tools/diff-files.d.ts +2 -1
  28. package/dist/tools/diff-files.js +7 -4
  29. package/dist/tools/edit-file.d.ts +2 -1
  30. package/dist/tools/edit-file.js +19 -6
  31. package/dist/tools/list-directory.d.ts +2 -1
  32. package/dist/tools/list-directory.js +36 -7
  33. package/dist/tools/move-file.d.ts +2 -1
  34. package/dist/tools/move-file.js +7 -5
  35. package/dist/tools/read-multiple.d.ts +2 -1
  36. package/dist/tools/read-multiple.js +10 -5
  37. package/dist/tools/read.d.ts +2 -1
  38. package/dist/tools/read.js +9 -5
  39. package/dist/tools/replace-in-files.d.ts +2 -1
  40. package/dist/tools/replace-in-files.js +14 -7
  41. package/dist/tools/roots.d.ts +2 -1
  42. package/dist/tools/roots.js +7 -4
  43. package/dist/tools/search-content.d.ts +2 -1
  44. package/dist/tools/search-content.js +14 -6
  45. package/dist/tools/search-files.d.ts +2 -1
  46. package/dist/tools/search-files.js +39 -7
  47. package/dist/tools/shared.d.ts +2 -2
  48. package/dist/tools/shared.js +16 -1
  49. package/dist/tools/stat-many.d.ts +2 -1
  50. package/dist/tools/stat-many.js +7 -5
  51. package/dist/tools/stat.d.ts +2 -1
  52. package/dist/tools/stat.js +7 -4
  53. package/dist/tools/task-support.d.ts +2 -0
  54. package/dist/tools/task-support.js +61 -9
  55. package/dist/tools/tree.d.ts +2 -1
  56. package/dist/tools/tree.js +7 -5
  57. package/dist/tools/write-file.d.ts +2 -1
  58. package/dist/tools/write-file.js +12 -5
  59. package/dist/tools.d.ts +2 -0
  60. package/dist/tools.js +39 -18
  61. package/package.json +1 -2
  62. package/dist/instructions.md +0 -200
@@ -10,7 +10,8 @@ import { validateExistingPath, validatePathForWrite, } from '../lib/path-validat
10
10
  import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
11
11
  import { buildToolErrorResponse, buildToolResponse, createProgressReporter, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, notifyProgress, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
12
12
  import { registerToolTaskIfAvailable } from './task-support.js';
13
- const SEARCH_AND_REPLACE_TOOL = {
13
+ export const SEARCH_AND_REPLACE_TOOL = {
14
+ name: 'search_and_replace',
14
15
  title: 'Search and Replace',
15
16
  description: 'Search and replace text across multiple files matching a glob pattern. ' +
16
17
  'Replaces ALL occurrences in each file (unlike `edit` which replaces only the first). ' +
@@ -20,6 +21,12 @@ const SEARCH_AND_REPLACE_TOOL = {
20
21
  inputSchema: SearchAndReplaceInputSchema,
21
22
  outputSchema: SearchAndReplaceOutputSchema,
22
23
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
24
+ gotchas: [
25
+ 'Literal mode is default; `isRegex=true` enables RE2 + capture replacements (`$1`, `$2`).',
26
+ ],
27
+ nuances: [
28
+ 'Changed-file sample and failure sample are capped/truncated in output.',
29
+ ],
23
30
  };
24
31
  const MAX_FAILURES = 20;
25
32
  const REPLACE_CONCURRENCY = Math.min(PARALLEL_CONCURRENCY, 8);
@@ -189,8 +196,8 @@ async function handleSearchAndReplace(args, signal, onProgress = () => { }) {
189
196
  const entries = globEntries({
190
197
  cwd: root,
191
198
  pattern: args.filePattern,
192
- excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
193
- includeHidden: false,
199
+ excludePatterns: args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS,
200
+ includeHidden: args.includeHidden ?? false,
194
201
  baseNameMatch: false,
195
202
  caseSensitiveMatch: true, // Default to sensitive for file paths
196
203
  followSymbolicLinks: false,
@@ -279,11 +286,11 @@ export function registerSearchAndReplaceTool(server, options = {}) {
279
286
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
280
287
  });
281
288
  const { isInitialized } = options;
282
- const validatedHandler = withValidatedArgs(SearchAndReplaceInputSchema, handler);
283
- const wrappedHandler = wrapToolHandler(validatedHandler, {
289
+ const wrappedHandler = wrapToolHandler(handler, {
284
290
  guard: isInitialized,
285
291
  });
286
- if (registerToolTaskIfAvailable(server, 'search_and_replace', SEARCH_AND_REPLACE_TOOL, wrappedHandler, options.iconInfo, isInitialized))
292
+ const validatedHandler = withValidatedArgs(SearchAndReplaceInputSchema, wrappedHandler);
293
+ if (registerToolTaskIfAvailable(server, 'search_and_replace', SEARCH_AND_REPLACE_TOOL, validatedHandler, options.iconInfo, isInitialized))
287
294
  return;
288
- server.registerTool('search_and_replace', withDefaultIcons({ ...SEARCH_AND_REPLACE_TOOL }, options.iconInfo), wrappedHandler);
295
+ server.registerTool('search_and_replace', withDefaultIcons({ ...SEARCH_AND_REPLACE_TOOL }, options.iconInfo), validatedHandler);
289
296
  }
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const LIST_ALLOWED_DIRECTORIES_TOOL: ToolContract;
3
4
  export declare function registerListAllowedDirectoriesTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -3,7 +3,8 @@ 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
- const LIST_ALLOWED_DIRECTORIES_TOOL = {
6
+ export const LIST_ALLOWED_DIRECTORIES_TOOL = {
7
+ name: 'roots',
7
8
  title: 'Workspace Roots',
8
9
  description: 'List the workspace roots this server can access. ' +
9
10
  'Call this first to see available directories. ' +
@@ -11,6 +12,7 @@ const LIST_ALLOWED_DIRECTORIES_TOOL = {
11
12
  inputSchema: ListAllowedDirectoriesInputSchema,
12
13
  outputSchema: ListAllowedDirectoriesOutputSchema,
13
14
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
15
+ nuances: ['Other tools are constrained to these roots.'],
14
16
  };
15
17
  function buildTextRoots(dirs) {
16
18
  if (dirs.length === 0) {
@@ -38,8 +40,7 @@ export function registerListAllowedDirectoriesTool(server, options = {}) {
38
40
  run: () => handleListAllowedDirectories(),
39
41
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN),
40
42
  });
41
- const validatedHandler = withValidatedArgs(ListAllowedDirectoriesInputSchema, handler);
42
- server.registerTool('roots', withDefaultIcons({ ...LIST_ALLOWED_DIRECTORIES_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
43
+ const wrappedHandler = wrapToolHandler(handler, {
43
44
  guard: options.isInitialized,
44
45
  progressMessage: () => '≣ roots',
45
46
  completionMessage: (_args, result) => {
@@ -51,5 +52,7 @@ export function registerListAllowedDirectoriesTool(server, options = {}) {
51
52
  const count = sc.rootsCount ?? 0;
52
53
  return `≣ roots • ${count} ${count === 1 ? 'root' : 'roots'}`;
53
54
  },
54
- }));
55
+ });
56
+ const validatedHandler = withValidatedArgs(ListAllowedDirectoriesInputSchema, wrappedHandler);
57
+ server.registerTool('roots', withDefaultIcons({ ...LIST_ALLOWED_DIRECTORIES_TOOL }, options.iconInfo), validatedHandler);
55
58
  }
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const SEARCH_CONTENT_TOOL: ToolContract;
3
4
  export declare function registerSearchContentTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -7,8 +7,9 @@ import { searchContent } from '../lib/file-operations/search-content.js';
7
7
  import { SearchContentInputSchema, SearchContentOutputSchema, } from '../schemas.js';
8
8
  import { buildResourceLink, buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
9
9
  import { registerToolTaskIfAvailable } from './task-support.js';
10
- const MAX_INLINE_MATCHES = 50;
11
- const SEARCH_CONTENT_TOOL = {
10
+ const MAX_INLINE_MATCHES = parseInt(process.env['FS_CONTEXT_MAX_INLINE_MATCHES'] ?? '', 10) || 50;
11
+ export const SEARCH_CONTENT_TOOL = {
12
+ name: 'grep',
12
13
  title: 'Search Content',
13
14
  description: 'Search for text within file contents (grep-like). ' +
14
15
  'Returns matching lines. ' +
@@ -18,6 +19,13 @@ const SEARCH_CONTENT_TOOL = {
18
19
  inputSchema: SearchContentInputSchema,
19
20
  outputSchema: SearchContentOutputSchema,
20
21
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
22
+ nuances: [
23
+ 'Inline match rows are capped (first 50); full structured results are externalized via `resourceUri`.',
24
+ 'Skips binary and oversized files.',
25
+ ],
26
+ gotchas: [
27
+ 'Inline match rows are capped (first 50); full structured results are externalized via `resourceUri`.',
28
+ ],
21
29
  };
22
30
  function assertValidRegexPattern(pattern) {
23
31
  try {
@@ -261,11 +269,11 @@ export function registerSearchContentTool(server, options = {}) {
261
269
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path ?? '.'),
262
270
  });
263
271
  const { isInitialized } = options;
264
- const validatedHandler = withValidatedArgs(SearchContentInputSchema, handler);
265
- const wrappedHandler = wrapToolHandler(validatedHandler, {
272
+ const wrappedHandler = wrapToolHandler(handler, {
266
273
  guard: isInitialized,
267
274
  });
268
- if (registerToolTaskIfAvailable(server, 'grep', SEARCH_CONTENT_TOOL, wrappedHandler, options.iconInfo, isInitialized))
275
+ const validatedHandler = withValidatedArgs(SearchContentInputSchema, wrappedHandler);
276
+ if (registerToolTaskIfAvailable(server, 'grep', SEARCH_CONTENT_TOOL, validatedHandler, options.iconInfo, isInitialized))
269
277
  return;
270
- server.registerTool('grep', withDefaultIcons({ ...SEARCH_CONTENT_TOOL }, options.iconInfo), wrappedHandler);
278
+ server.registerTool('grep', withDefaultIcons({ ...SEARCH_CONTENT_TOOL }, options.iconInfo), validatedHandler);
271
279
  }
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const SEARCH_FILES_TOOL: ToolContract;
3
4
  export declare function registerSearchFilesTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -6,7 +6,26 @@ import { searchFiles } from '../lib/file-operations/search-files.js';
6
6
  import { SearchFilesInputSchema, SearchFilesOutputSchema } from '../schemas.js';
7
7
  import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  import { registerToolTaskIfAvailable } from './task-support.js';
9
- const SEARCH_FILES_TOOL = {
9
+ function encodeCursor(offset) {
10
+ return Buffer.from(JSON.stringify({ offset })).toString('base64url');
11
+ }
12
+ function decodeCursor(cursor) {
13
+ try {
14
+ const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
15
+ if (typeof parsed === 'object' &&
16
+ parsed !== null &&
17
+ typeof parsed.offset === 'number') {
18
+ const { offset } = parsed;
19
+ return Number.isInteger(offset) && offset >= 0 ? offset : 0;
20
+ }
21
+ }
22
+ catch {
23
+ // ignore malformed cursor
24
+ }
25
+ return 0;
26
+ }
27
+ export const SEARCH_FILES_TOOL = {
28
+ name: 'find',
10
29
  title: 'Find Files',
11
30
  description: 'Find files by glob pattern (e.g., **/*.ts). ' +
12
31
  'Returns a list of matching files with metadata. ' +
@@ -15,12 +34,19 @@ const SEARCH_FILES_TOOL = {
15
34
  inputSchema: SearchFilesInputSchema,
16
35
  outputSchema: SearchFilesOutputSchema,
17
36
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
37
+ nuances: [
38
+ 'Respects `.gitignore` unless `includeIgnored=true`.',
39
+ 'Returns relative paths plus metadata; may truncate.',
40
+ ],
18
41
  };
19
42
  async function handleSearchFiles(args, signal, onProgress) {
20
43
  const basePath = resolvePathOrRoot(args.path);
21
44
  const excludePatterns = args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS;
45
+ const cursorOffset = args.cursor !== undefined ? decodeCursor(args.cursor) : 0;
46
+ const pageSize = args.maxResults;
47
+ const fetchMax = cursorOffset + pageSize;
22
48
  const searchOptions = {
23
- maxResults: args.maxResults,
49
+ maxResults: fetchMax,
24
50
  includeHidden: args.includeHidden,
25
51
  sortBy: args.sortBy,
26
52
  respectGitignore: !args.includeIgnored,
@@ -29,8 +55,13 @@ async function handleSearchFiles(args, signal, onProgress) {
29
55
  ...(signal ? { signal } : {}),
30
56
  };
31
57
  const result = await searchFiles(basePath, args.pattern, excludePatterns, searchOptions);
58
+ const allResults = result.results;
59
+ const displayResults = cursorOffset > 0 ? allResults.slice(cursorOffset) : allResults;
60
+ const nextCursor = result.summary.truncated && displayResults.length > 0
61
+ ? encodeCursor(cursorOffset + displayResults.length)
62
+ : undefined;
32
63
  const relativeResults = [];
33
- for (const entry of result.results) {
64
+ for (const entry of displayResults) {
34
65
  relativeResults.push({
35
66
  path: path.relative(result.basePath, entry.path),
36
67
  size: entry.size,
@@ -53,6 +84,7 @@ async function handleSearchFiles(args, signal, onProgress) {
53
84
  ...(result.summary.stoppedReason
54
85
  ? { stoppedReason: result.summary.stoppedReason }
55
86
  : {}),
87
+ ...(nextCursor !== undefined ? { nextCursor } : {}),
56
88
  };
57
89
  let truncatedReason;
58
90
  if (result.summary.truncated) {
@@ -152,11 +184,11 @@ export function registerSearchFilesTool(server, options = {}) {
152
184
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
153
185
  });
154
186
  const { isInitialized } = options;
155
- const validatedHandler = withValidatedArgs(SearchFilesInputSchema, handler);
156
- const wrappedHandler = wrapToolHandler(validatedHandler, {
187
+ const wrappedHandler = wrapToolHandler(handler, {
157
188
  guard: isInitialized,
158
189
  });
159
- if (registerToolTaskIfAvailable(server, 'find', SEARCH_FILES_TOOL, wrappedHandler, options.iconInfo, isInitialized))
190
+ const validatedHandler = withValidatedArgs(SearchFilesInputSchema, wrappedHandler);
191
+ if (registerToolTaskIfAvailable(server, 'find', SEARCH_FILES_TOOL, validatedHandler, options.iconInfo, isInitialized))
160
192
  return;
161
- server.registerTool('find', withDefaultIcons({ ...SEARCH_FILES_TOOL }, options.iconInfo), wrappedHandler);
193
+ server.registerTool('find', withDefaultIcons({ ...SEARCH_FILES_TOOL }, options.iconInfo), validatedHandler);
162
194
  }
@@ -4,6 +4,7 @@ import type { FileInfo } from '../config.js';
4
4
  import { ErrorCode } from '../lib/errors.js';
5
5
  import type { ResourceStore } from '../lib/resource-store.js';
6
6
  import type { ToolErrorResponseSchema } from '../schemas.js';
7
+ export { type ToolContract } from './contract.js';
7
8
  export declare const READ_ONLY_TOOL_ANNOTATIONS: {
8
9
  readonly readOnlyHint: true;
9
10
  readonly idempotentHint: true;
@@ -48,7 +49,7 @@ interface ToolErrorResponse extends Record<string, unknown> {
48
49
  }
49
50
  export type ToolResult<T> = ToolResponse<T> | ToolErrorResponse;
50
51
  export declare function parseToolArgs<Schema extends z.ZodType>(schema: Schema, args: unknown): z.infer<Schema>;
51
- export declare function withValidatedArgs<Args, Result>(schema: z.ZodType<Args>, handler: (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>): (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>;
52
+ 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>>;
52
53
  type ProgressToken = string | number;
53
54
  export interface ToolExtra {
54
55
  signal?: AbortSignal;
@@ -116,4 +117,3 @@ export declare function wrapToolHandler<Args, Result>(handler: (args: Args, extr
116
117
  completionMessage?: (args: Args, result: ToolResult<Result>) => string | undefined;
117
118
  }): (args: Args, extra?: ToolExtra) => Promise<ToolResult<Result>>;
118
119
  export declare function resolvePathOrRoot(pathValue: string | undefined): string;
119
- export {};
@@ -1,12 +1,20 @@
1
+ import { channel } from 'node:diagnostics_channel';
1
2
  import { z } from 'zod';
2
3
  import { createDetailedError, ErrorCode, formatDetailedError, getSuggestion, McpError, } from '../lib/errors.js';
3
4
  import { createTimedAbortSignal } from '../lib/fs-helpers.js';
4
5
  import { withToolDiagnostics } from '../lib/observability.js';
5
6
  import { getAllowedDirectories } from '../lib/path-validation.js';
6
- const MAX_INLINE_CONTENT_CHARS = 20_000;
7
+ export {} from './contract.js';
8
+ const MAX_INLINE_CONTENT_CHARS = parseInt(process.env['FS_CONTEXT_MAX_INLINE_CHARS'] ?? '', 10) || 20_000;
7
9
  const MAX_INLINE_PREVIEW_CHARS = 4_000;
8
10
  const PROGRESS_RATE_LIMIT_MS = 50;
9
11
  const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes']);
12
+ const CONTEXT_DIAGNOSTICS_CHANNEL = channel('filesystem-mcp:context');
13
+ function publishContextDiagnostics(event) {
14
+ if (!CONTEXT_DIAGNOSTICS_CHANNEL.hasSubscribers)
15
+ return;
16
+ CONTEXT_DIAGNOSTICS_CHANNEL.publish(event);
17
+ }
10
18
  export const READ_ONLY_TOOL_ANNOTATIONS = {
11
19
  readOnlyHint: true,
12
20
  idempotentHint: true,
@@ -61,6 +69,13 @@ export function maybeExternalizeTextContent(resourceStore, content, params) {
61
69
  ...(params.mimeType !== undefined ? { mimeType: params.mimeType } : {}),
62
70
  text: content,
63
71
  });
72
+ publishContextDiagnostics({
73
+ phase: 'externalize_text',
74
+ name: params.name,
75
+ ...(params.mimeType !== undefined ? { mimeType: params.mimeType } : {}),
76
+ chars: content.length,
77
+ uri: entry.uri,
78
+ });
64
79
  return {
65
80
  entry,
66
81
  preview: buildTextPreview(content),
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const GET_MULTIPLE_FILE_INFO_TOOL: ToolContract;
3
4
  export declare function registerGetMultipleFileInfoTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -6,12 +6,14 @@ import { getMultipleFileInfo } from '../lib/file-operations/file-info.js';
6
6
  import { GetMultipleFileInfoInputSchema, GetMultipleFileInfoOutputSchema, } from '../schemas.js';
7
7
  import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  import { registerToolTaskIfAvailable } from './task-support.js';
9
- const GET_MULTIPLE_FILE_INFO_TOOL = {
9
+ export const GET_MULTIPLE_FILE_INFO_TOOL = {
10
+ name: 'stat_many',
10
11
  title: 'Get Multiple File Info',
11
12
  description: 'Get metadata for multiple files or directories in one request.',
12
13
  inputSchema: GetMultipleFileInfoInputSchema,
13
14
  outputSchema: GetMultipleFileInfoOutputSchema,
14
15
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
16
+ nuances: ['Use before read/search when file size/type uncertainty exists.'],
15
17
  };
16
18
  function formatFileInfoDetail(info) {
17
19
  const lines = [
@@ -73,8 +75,7 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
73
75
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, primaryPath),
74
76
  });
75
77
  };
76
- const validatedHandler = withValidatedArgs(GetMultipleFileInfoInputSchema, handler);
77
- const wrappedHandler = wrapToolHandler(validatedHandler, {
78
+ const wrappedHandler = wrapToolHandler(handler, {
78
79
  guard: options.isInitialized,
79
80
  progressMessage: (args) => {
80
81
  const first = path.basename(args.paths[0] ?? '');
@@ -95,7 +96,8 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
95
96
  return `🕮 stat_many: ${total} OK`;
96
97
  },
97
98
  });
98
- if (registerToolTaskIfAvailable(server, 'stat_many', GET_MULTIPLE_FILE_INFO_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
99
+ const validatedHandler = withValidatedArgs(GetMultipleFileInfoInputSchema, wrappedHandler);
100
+ if (registerToolTaskIfAvailable(server, 'stat_many', GET_MULTIPLE_FILE_INFO_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
99
101
  return;
100
- server.registerTool('stat_many', withDefaultIcons({ ...GET_MULTIPLE_FILE_INFO_TOOL }, options.iconInfo), wrappedHandler);
102
+ server.registerTool('stat_many', withDefaultIcons({ ...GET_MULTIPLE_FILE_INFO_TOOL }, options.iconInfo), validatedHandler);
101
103
  }
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const GET_FILE_INFO_TOOL: ToolContract;
3
4
  export declare function registerGetFileInfoTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -5,12 +5,14 @@ import { ErrorCode } from '../lib/errors.js';
5
5
  import { getFileInfo } from '../lib/file-operations/file-info.js';
6
6
  import { GetFileInfoInputSchema, GetFileInfoOutputSchema } from '../schemas.js';
7
7
  import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
- const GET_FILE_INFO_TOOL = {
8
+ export const GET_FILE_INFO_TOOL = {
9
+ name: 'stat',
9
10
  title: 'Get File Info',
10
11
  description: 'Get metadata (size, modified time, permissions, mime type) for a file or directory.',
11
12
  inputSchema: GetFileInfoInputSchema,
12
13
  outputSchema: GetFileInfoOutputSchema,
13
14
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
15
+ nuances: ['Use before read/search when file size/type uncertainty exists.'],
14
16
  };
15
17
  function formatFileInfoDetails(info) {
16
18
  const lines = [
@@ -45,8 +47,7 @@ export function registerGetFileInfoTool(server, options = {}) {
45
47
  run: (signal) => handleGetFileInfo(args, signal),
46
48
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, args.path),
47
49
  });
48
- const validatedHandler = withValidatedArgs(GetFileInfoInputSchema, handler);
49
- server.registerTool('stat', withDefaultIcons({ ...GET_FILE_INFO_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
50
+ const wrappedHandler = wrapToolHandler(handler, {
50
51
  guard: options.isInitialized,
51
52
  progressMessage: (args) => `🕮 stat: ${path.basename(args.path)}`,
52
53
  completionMessage: (args, result) => {
@@ -58,5 +59,7 @@ export function registerGetFileInfoTool(server, options = {}) {
58
59
  return `🕮 stat: ${name} • failed`;
59
60
  return `🕮 stat: ${sc.info.name} • ${sc.info.type}, ${formatBytes(sc.info.size)}`;
60
61
  },
61
- }));
62
+ });
63
+ const validatedHandler = withValidatedArgs(GetFileInfoInputSchema, wrappedHandler);
64
+ server.registerTool('stat', withDefaultIcons({ ...GET_FILE_INFO_TOOL }, options.iconInfo), validatedHandler);
62
65
  }
@@ -18,8 +18,10 @@ export declare function tryRegisterToolTask<Args extends ZodRawShapeCompat | Any
18
18
  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
19
  export declare function createToolTaskHandler<Result>(run: (args: undefined, extra: TaskToolExtra) => Promise<ToolResult<Result>>, options?: {
20
20
  guard?: () => boolean;
21
+ toolName?: string;
21
22
  }): ToolTaskHandler;
22
23
  export declare function createToolTaskHandler<Args extends ZodRawShapeCompat | AnySchema, Result>(run: (args: ToolArgs<Args>, extra: TaskToolExtra) => Promise<ToolResult<Result>>, options?: {
23
24
  guard?: () => boolean;
25
+ toolName?: string;
24
26
  }): ToolTaskHandler<Args>;
25
27
  export {};
@@ -1,3 +1,4 @@
1
+ import { channel } from 'node:diagnostics_channel';
1
2
  import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
2
3
  import { ErrorCode, McpError } from '../lib/errors.js';
3
4
  import { isRecord } from '../lib/type-guards.js';
@@ -43,6 +44,12 @@ function hasTaskToolCapability(server) {
43
44
  }
44
45
  const RELATED_TASK_META_KEY = 'io.modelcontextprotocol/related-task';
45
46
  const TASK_STATUS_NOTIFICATION_METHOD = 'notifications/tasks/status';
47
+ const TASK_DIAGNOSTICS_CHANNEL = channel('filesystem-mcp:tasks');
48
+ function publishTaskDiagnostics(event) {
49
+ if (!TASK_DIAGNOSTICS_CHANNEL.hasSubscribers)
50
+ return;
51
+ TASK_DIAGNOSTICS_CHANNEL.publish(event);
52
+ }
46
53
  function isRequestTaskStore(value) {
47
54
  if (!isRecord(value))
48
55
  return false;
@@ -174,7 +181,7 @@ function buildTaskStatusNotificationParams(task) {
174
181
  params.statusMessage = task.statusMessage;
175
182
  return params;
176
183
  }
177
- async function notifyTaskStatusIfPossible(extra, taskStore, taskId) {
184
+ async function notifyTaskStatusIfPossible(extra, taskStore, taskId, toolName) {
178
185
  const { sendNotification } = extra;
179
186
  if (typeof sendNotification !== 'function')
180
187
  return;
@@ -186,8 +193,19 @@ async function notifyTaskStatusIfPossible(extra, taskStore, taskId) {
186
193
  method: TASK_STATUS_NOTIFICATION_METHOD,
187
194
  params: buildTaskStatusNotificationParams(normalized),
188
195
  });
196
+ publishTaskDiagnostics({
197
+ phase: 'task_status_notified',
198
+ taskId,
199
+ status: normalized.status,
200
+ ...(toolName !== undefined ? { toolName } : {}),
201
+ });
189
202
  }
190
203
  catch {
204
+ publishTaskDiagnostics({
205
+ phase: 'task_status_notify_failed',
206
+ taskId,
207
+ ...(toolName !== undefined ? { toolName } : {}),
208
+ });
191
209
  // Never fail task execution because status notifications are optional.
192
210
  }
193
211
  }
@@ -206,6 +224,14 @@ function getTaskId(extra) {
206
224
  function isErrorResult(result) {
207
225
  return 'isError' in result && result.isError === true;
208
226
  }
227
+ // Strips structuredContent from a tool result if present, without modifying the original object. This is used when storing error results as 'completed' to prevent client-side output schema validation errors, while still allowing the human-readable error message in content[0].text to be returned to clients.
228
+ function withoutStructuredContent(result) {
229
+ if (!Object.hasOwn(result, 'structuredContent'))
230
+ return result;
231
+ const stripped = { ...result };
232
+ delete stripped['structuredContent'];
233
+ return stripped;
234
+ }
209
235
  const TERMINAL_TASK_STATUSES = new Set([
210
236
  'completed',
211
237
  'failed',
@@ -234,18 +260,33 @@ async function tryStoreTaskResult(taskStore, taskId, status, result) {
234
260
  throw error;
235
261
  }
236
262
  }
237
- async function runTaskInBackground(run, args, extra, taskStore, taskId) {
263
+ async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName) {
238
264
  try {
239
- const result = maybeStripStructuredContentFromResult(await run(args, extra));
240
- const status = isErrorResult(result) ? 'failed' : 'completed';
265
+ const rawResult = await run(args, extra);
266
+ const status = isCancelledToolResult(rawResult) ? 'failed' : 'completed';
267
+ const result = isErrorResult(rawResult) && status === 'completed'
268
+ ? withoutStructuredContent(rawResult)
269
+ : maybeStripStructuredContentFromResult(rawResult);
241
270
  await tryStoreTaskResult(taskStore, taskId, status, result);
242
- await notifyTaskStatusIfPossible(extra, taskStore, taskId);
271
+ publishTaskDiagnostics({
272
+ phase: 'task_result_stored',
273
+ taskId,
274
+ status,
275
+ ...(toolName !== undefined ? { toolName } : {}),
276
+ });
277
+ await notifyTaskStatusIfPossible(extra, taskStore, taskId, toolName);
243
278
  }
244
279
  catch (error) {
245
280
  const fallback = maybeStripStructuredContentFromResult(buildToolErrorResponse(error, ErrorCode.E_UNKNOWN));
246
281
  try {
247
282
  await tryStoreTaskResult(taskStore, taskId, 'failed', fallback);
248
- await notifyTaskStatusIfPossible(extra, taskStore, taskId);
283
+ publishTaskDiagnostics({
284
+ phase: 'task_result_stored',
285
+ taskId,
286
+ status: 'failed',
287
+ ...(toolName !== undefined ? { toolName } : {}),
288
+ });
289
+ await notifyTaskStatusIfPossible(extra, taskStore, taskId, toolName);
249
290
  }
250
291
  catch (innerError) {
251
292
  console.error(`Failed to store task failure result for task ${taskId}:`, innerError);
@@ -267,7 +308,10 @@ export function tryRegisterToolTask(server, toolName, toolDef, taskHandler, icon
267
308
  return true;
268
309
  }
269
310
  export function registerToolTaskIfAvailable(server, toolName, toolDef, run, iconInfo, guard) {
270
- const taskOptions = guard ? { guard } : undefined;
311
+ const taskOptions = {
312
+ ...(guard ? { guard } : {}),
313
+ toolName,
314
+ };
271
315
  return tryRegisterToolTask(server, toolName, toolDef, createToolTaskHandler(run, taskOptions), iconInfo);
272
316
  }
273
317
  export function createToolTaskHandler(run, options) {
@@ -281,13 +325,21 @@ export function createToolTaskHandler(run, options) {
281
325
  const task = await taskStore.createTask({
282
326
  ttl: extra.taskRequestedTtl ?? null,
283
327
  });
328
+ publishTaskDiagnostics({
329
+ phase: 'task_created',
330
+ taskId: task.taskId,
331
+ status: task.status,
332
+ ...(options?.toolName !== undefined
333
+ ? { toolName: options.toolName }
334
+ : {}),
335
+ });
284
336
  const taskExtra = {
285
337
  ...extra,
286
338
  taskStore,
287
339
  taskId: task.taskId,
288
340
  };
289
- void notifyTaskStatusIfPossible(taskExtra, taskStore, task.taskId);
290
- void runTaskInBackground(run, args, taskExtra, taskStore, task.taskId);
341
+ void notifyTaskStatusIfPossible(taskExtra, taskStore, task.taskId, options?.toolName);
342
+ void runTaskInBackground(run, args, taskExtra, taskStore, task.taskId, options?.toolName);
291
343
  return { task };
292
344
  });
293
345
  const getTask = (async (argsOrExtra, maybeExtra) => {
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const TREE_TOOL: ToolContract;
3
4
  export declare function registerTreeTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -5,7 +5,8 @@ import { formatTreeAscii, treeDirectory } from '../lib/file-operations/tree.js';
5
5
  import { TreeInputSchema, TreeOutputSchema } from '../schemas.js';
6
6
  import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
7
7
  import { registerToolTaskIfAvailable } from './task-support.js';
8
- const TREE_TOOL = {
8
+ export const TREE_TOOL = {
9
+ name: 'tree',
9
10
  title: 'Tree',
10
11
  description: 'Render a directory tree (bounded recursion). ' +
11
12
  'Returns an ASCII tree for quick scanning and a structured JSON tree for programmatic use. ' +
@@ -13,6 +14,7 @@ const TREE_TOOL = {
13
14
  inputSchema: TreeInputSchema,
14
15
  outputSchema: TreeOutputSchema,
15
16
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
17
+ gotchas: ['`maxDepth=0` returns only the root node.'],
16
18
  };
17
19
  async function handleTree(args, signal) {
18
20
  const basePath = resolvePathOrRoot(args.path);
@@ -47,8 +49,7 @@ export function registerTreeTool(server, options = {}) {
47
49
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, targetPath),
48
50
  });
49
51
  };
50
- const validatedHandler = withValidatedArgs(TreeInputSchema, handler);
51
- const wrappedHandler = wrapToolHandler(validatedHandler, {
52
+ const wrappedHandler = wrapToolHandler(handler, {
52
53
  guard: options.isInitialized,
53
54
  progressMessage: (args) => {
54
55
  if (args.path) {
@@ -69,7 +70,8 @@ export function registerTreeTool(server, options = {}) {
69
70
  return `≣ tree: ${base} • ${count} ${count === 1 ? 'entry' : 'entries'}`;
70
71
  },
71
72
  });
72
- if (registerToolTaskIfAvailable(server, 'tree', TREE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
73
+ const validatedHandler = withValidatedArgs(TreeInputSchema, wrappedHandler);
74
+ if (registerToolTaskIfAvailable(server, 'tree', TREE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
73
75
  return;
74
- server.registerTool('tree', withDefaultIcons({ ...TREE_TOOL }, options.iconInfo), wrappedHandler);
76
+ server.registerTool('tree', withDefaultIcons({ ...TREE_TOOL }, options.iconInfo), validatedHandler);
75
77
  }
@@ -1,3 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { type ToolRegistrationOptions } from './shared.js';
2
+ import { type ToolContract, type ToolRegistrationOptions } from './shared.js';
3
+ export declare const WRITE_FILE_TOOL: ToolContract;
3
4
  export declare function registerWriteFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -6,12 +6,19 @@ import { validatePathForWrite } from '../lib/path-validation.js';
6
6
  import { WriteFileInputSchema, WriteFileOutputSchema } from '../schemas.js';
7
7
  import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  import { registerToolTaskIfAvailable } from './task-support.js';
9
- const WRITE_FILE_TOOL = {
9
+ export const WRITE_FILE_TOOL = {
10
+ name: 'write',
10
11
  title: 'Write File',
11
12
  description: 'Write content to a file. Creates the file if it does not exist.',
12
13
  inputSchema: WriteFileInputSchema,
13
14
  outputSchema: WriteFileOutputSchema,
14
15
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
16
+ nuances: [
17
+ 'Creates parent directories automatically; overwrites existing content.',
18
+ ],
19
+ gotchas: [
20
+ 'Creates parent directories automatically; overwrites existing content.',
21
+ ],
15
22
  };
16
23
  async function handleWriteFile(args, signal) {
17
24
  const validPath = await validatePathForWrite(args.path, signal);
@@ -34,8 +41,7 @@ export function registerWriteFileTool(server, options = {}) {
34
41
  run: (signal) => handleWriteFile(args, signal),
35
42
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
36
43
  });
37
- const validatedHandler = withValidatedArgs(WriteFileInputSchema, handler);
38
- const wrappedHandler = wrapToolHandler(validatedHandler, {
44
+ const wrappedHandler = wrapToolHandler(handler, {
39
45
  guard: options.isInitialized,
40
46
  progressMessage: (args) => `🛠 write: ${path.basename(args.path)} [${args.content.length} chars]`,
41
47
  completionMessage: (args, result) => {
@@ -48,7 +54,8 @@ export function registerWriteFileTool(server, options = {}) {
48
54
  return `🛠 write: ${name} • ${sc.bytesWritten ?? 0} bytes`;
49
55
  },
50
56
  });
51
- if (registerToolTaskIfAvailable(server, 'write', WRITE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
57
+ const validatedHandler = withValidatedArgs(WriteFileInputSchema, wrappedHandler);
58
+ if (registerToolTaskIfAvailable(server, 'write', WRITE_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
52
59
  return;
53
- server.registerTool('write', withDefaultIcons({ ...WRITE_FILE_TOOL }, options.iconInfo), wrappedHandler);
60
+ server.registerTool('write', withDefaultIcons({ ...WRITE_FILE_TOOL }, options.iconInfo), validatedHandler);
54
61
  }
package/dist/tools.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type ToolContract } from './tools/contract.js';
2
3
  import type { ToolRegistrationOptions } from './tools/shared.js';
3
4
  export { buildToolErrorResponse, buildToolResponse } from './tools/shared.js';
5
+ export declare const ALL_TOOLS: ToolContract[];
4
6
  export declare function registerAllTools(server: McpServer, options?: ToolRegistrationOptions): void;