@j0hanz/filesystem-mcp 1.2.4 → 1.3.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.
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 +48 -7
  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
  }
@@ -234,18 +252,30 @@ async function tryStoreTaskResult(taskStore, taskId, status, result) {
234
252
  throw error;
235
253
  }
236
254
  }
237
- async function runTaskInBackground(run, args, extra, taskStore, taskId) {
255
+ async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName) {
238
256
  try {
239
257
  const result = maybeStripStructuredContentFromResult(await run(args, extra));
240
258
  const status = isErrorResult(result) ? 'failed' : 'completed';
241
259
  await tryStoreTaskResult(taskStore, taskId, status, result);
242
- await notifyTaskStatusIfPossible(extra, taskStore, taskId);
260
+ publishTaskDiagnostics({
261
+ phase: 'task_result_stored',
262
+ taskId,
263
+ status,
264
+ ...(toolName !== undefined ? { toolName } : {}),
265
+ });
266
+ await notifyTaskStatusIfPossible(extra, taskStore, taskId, toolName);
243
267
  }
244
268
  catch (error) {
245
269
  const fallback = maybeStripStructuredContentFromResult(buildToolErrorResponse(error, ErrorCode.E_UNKNOWN));
246
270
  try {
247
271
  await tryStoreTaskResult(taskStore, taskId, 'failed', fallback);
248
- await notifyTaskStatusIfPossible(extra, taskStore, taskId);
272
+ publishTaskDiagnostics({
273
+ phase: 'task_result_stored',
274
+ taskId,
275
+ status: 'failed',
276
+ ...(toolName !== undefined ? { toolName } : {}),
277
+ });
278
+ await notifyTaskStatusIfPossible(extra, taskStore, taskId, toolName);
249
279
  }
250
280
  catch (innerError) {
251
281
  console.error(`Failed to store task failure result for task ${taskId}:`, innerError);
@@ -267,7 +297,10 @@ export function tryRegisterToolTask(server, toolName, toolDef, taskHandler, icon
267
297
  return true;
268
298
  }
269
299
  export function registerToolTaskIfAvailable(server, toolName, toolDef, run, iconInfo, guard) {
270
- const taskOptions = guard ? { guard } : undefined;
300
+ const taskOptions = {
301
+ ...(guard ? { guard } : {}),
302
+ toolName,
303
+ };
271
304
  return tryRegisterToolTask(server, toolName, toolDef, createToolTaskHandler(run, taskOptions), iconInfo);
272
305
  }
273
306
  export function createToolTaskHandler(run, options) {
@@ -281,13 +314,21 @@ export function createToolTaskHandler(run, options) {
281
314
  const task = await taskStore.createTask({
282
315
  ttl: extra.taskRequestedTtl ?? null,
283
316
  });
317
+ publishTaskDiagnostics({
318
+ phase: 'task_created',
319
+ taskId: task.taskId,
320
+ status: task.status,
321
+ ...(options?.toolName !== undefined
322
+ ? { toolName: options.toolName }
323
+ : {}),
324
+ });
284
325
  const taskExtra = {
285
326
  ...extra,
286
327
  taskStore,
287
328
  taskId: task.taskId,
288
329
  };
289
- void notifyTaskStatusIfPossible(taskExtra, taskStore, task.taskId);
290
- void runTaskInBackground(run, args, taskExtra, taskStore, task.taskId);
330
+ void notifyTaskStatusIfPossible(taskExtra, taskStore, task.taskId, options?.toolName);
331
+ void runTaskInBackground(run, args, taskExtra, taskStore, task.taskId, options?.toolName);
291
332
  return { task };
292
333
  });
293
334
  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;