@j0hanz/filesystem-mcp 1.2.3 → 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 (66) hide show
  1. package/README.md +8 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +13 -1
  4. package/dist/completions.d.ts +1 -1
  5. package/dist/completions.js +36 -1
  6. package/dist/index.js +26 -8
  7. package/dist/lib/observability.d.ts +6 -0
  8. package/dist/lib/observability.js +1 -1
  9. package/dist/lib/resource-store.js +53 -0
  10. package/dist/prompts.js +34 -14
  11. package/dist/resources/generated-instructions.d.ts +1 -0
  12. package/dist/resources/generated-instructions.js +100 -0
  13. package/dist/resources.d.ts +1 -0
  14. package/dist/resources.js +36 -1
  15. package/dist/schemas.d.ts +6 -0
  16. package/dist/schemas.js +24 -0
  17. package/dist/server/bootstrap.d.ts +2 -0
  18. package/dist/server/bootstrap.js +226 -20
  19. package/dist/server.d.ts +1 -1
  20. package/dist/server.js +1 -1
  21. package/dist/tools/apply-patch.d.ts +2 -1
  22. package/dist/tools/apply-patch.js +7 -5
  23. package/dist/tools/calculate-hash.d.ts +2 -1
  24. package/dist/tools/calculate-hash.js +9 -5
  25. package/dist/tools/contract.d.ts +41 -0
  26. package/dist/tools/contract.js +1 -0
  27. package/dist/tools/create-directory.d.ts +2 -1
  28. package/dist/tools/create-directory.js +6 -5
  29. package/dist/tools/delete-file.d.ts +2 -1
  30. package/dist/tools/delete-file.js +9 -5
  31. package/dist/tools/diff-files.d.ts +2 -1
  32. package/dist/tools/diff-files.js +7 -4
  33. package/dist/tools/edit-file.d.ts +2 -1
  34. package/dist/tools/edit-file.js +19 -6
  35. package/dist/tools/list-directory.d.ts +2 -1
  36. package/dist/tools/list-directory.js +36 -7
  37. package/dist/tools/move-file.d.ts +2 -1
  38. package/dist/tools/move-file.js +7 -5
  39. package/dist/tools/read-multiple.d.ts +2 -1
  40. package/dist/tools/read-multiple.js +10 -5
  41. package/dist/tools/read.d.ts +2 -1
  42. package/dist/tools/read.js +9 -5
  43. package/dist/tools/replace-in-files.d.ts +2 -1
  44. package/dist/tools/replace-in-files.js +14 -7
  45. package/dist/tools/roots.d.ts +2 -1
  46. package/dist/tools/roots.js +7 -4
  47. package/dist/tools/search-content.d.ts +2 -1
  48. package/dist/tools/search-content.js +14 -6
  49. package/dist/tools/search-files.d.ts +2 -1
  50. package/dist/tools/search-files.js +39 -7
  51. package/dist/tools/shared.d.ts +2 -2
  52. package/dist/tools/shared.js +16 -1
  53. package/dist/tools/stat-many.d.ts +2 -1
  54. package/dist/tools/stat-many.js +7 -5
  55. package/dist/tools/stat.d.ts +2 -1
  56. package/dist/tools/stat.js +7 -4
  57. package/dist/tools/task-support.d.ts +2 -0
  58. package/dist/tools/task-support.js +48 -7
  59. package/dist/tools/tree.d.ts +2 -1
  60. package/dist/tools/tree.js +7 -5
  61. package/dist/tools/write-file.d.ts +2 -1
  62. package/dist/tools/write-file.js +12 -5
  63. package/dist/tools.d.ts +2 -0
  64. package/dist/tools.js +39 -18
  65. package/package.json +1 -2
  66. package/dist/instructions.md +0 -200
@@ -5,7 +5,8 @@ import { ErrorCode } from '../lib/errors.js';
5
5
  import { listDirectory } from '../lib/file-operations/list-directory.js';
6
6
  import { ListDirectoryInputSchema, ListDirectoryOutputSchema, } from '../schemas.js';
7
7
  import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
- const LIST_DIRECTORY_TOOL = {
8
+ export const LIST_DIRECTORY_TOOL = {
9
+ name: 'ls',
9
10
  title: 'List Directory',
10
11
  description: 'List the immediate contents of a directory (non-recursive). ' +
11
12
  'Returns name, relative path, type (file/directory/symlink), size, and modified date. ' +
@@ -15,6 +16,7 @@ const LIST_DIRECTORY_TOOL = {
15
16
  inputSchema: ListDirectoryInputSchema,
16
17
  outputSchema: ListDirectoryOutputSchema,
17
18
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
19
+ nuances: ['`pattern` enables filtered recursive traversal up to `maxDepth`.'],
18
20
  };
19
21
  function buildListTextResult(result) {
20
22
  const { entries, summary, path } = result;
@@ -53,7 +55,7 @@ function buildStructuredListEntry(entry) {
53
55
  modified: entry.modified?.toISOString(),
54
56
  };
55
57
  }
56
- function buildStructuredListResult(result) {
58
+ function buildStructuredListResult(result, nextCursor) {
57
59
  const { entries, summary, path: resultPath } = result;
58
60
  const structuredEntries = [];
59
61
  for (const entry of entries) {
@@ -71,22 +73,48 @@ function buildStructuredListResult(result) {
71
73
  ...(summary.skippedInaccessible
72
74
  ? { skippedInaccessible: summary.skippedInaccessible }
73
75
  : {}),
76
+ ...(nextCursor !== undefined ? { nextCursor } : {}),
74
77
  };
75
78
  }
79
+ function encodeCursor(offset) {
80
+ return Buffer.from(JSON.stringify({ offset })).toString('base64url');
81
+ }
82
+ function decodeCursor(cursor) {
83
+ try {
84
+ const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
85
+ if (typeof parsed === 'object' &&
86
+ parsed !== null &&
87
+ typeof parsed.offset === 'number') {
88
+ const { offset } = parsed;
89
+ return Number.isInteger(offset) && offset >= 0 ? offset : 0;
90
+ }
91
+ }
92
+ catch {
93
+ // ignore malformed cursor
94
+ }
95
+ return 0;
96
+ }
76
97
  async function handleListDirectory(args, signal) {
77
98
  const dirPath = resolvePathOrRoot(args.path);
99
+ const cursorOffset = args.cursor !== undefined ? decodeCursor(args.cursor) : 0;
100
+ const pageSize = args.maxEntries ?? 20_000;
78
101
  const options = {
79
102
  includeHidden: args.includeHidden,
80
103
  excludePatterns: args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS,
81
104
  sortBy: args.sortBy,
82
105
  includeSymlinkTargets: args.includeSymlinkTargets,
83
106
  ...(args.maxDepth !== undefined ? { maxDepth: args.maxDepth } : {}),
84
- ...(args.maxEntries !== undefined ? { maxEntries: args.maxEntries } : {}),
107
+ maxEntries: cursorOffset + pageSize,
85
108
  ...(args.pattern !== undefined ? { pattern: args.pattern } : {}),
86
109
  ...(signal ? { signal } : {}),
87
110
  };
88
111
  const result = await listDirectory(dirPath, options);
89
- return buildToolResponse(buildListTextResult(result), buildStructuredListResult(result));
112
+ const displayEntries = cursorOffset > 0 ? result.entries.slice(cursorOffset) : result.entries;
113
+ const nextCursor = result.summary.truncated && displayEntries.length > 0
114
+ ? encodeCursor(cursorOffset + displayEntries.length)
115
+ : undefined;
116
+ const displayResult = { ...result, entries: displayEntries };
117
+ return buildToolResponse(buildListTextResult(displayResult), buildStructuredListResult(displayResult, nextCursor));
90
118
  }
91
119
  export function registerListDirectoryTool(server, options = {}) {
92
120
  const handler = (args, extra) => executeToolWithDiagnostics({
@@ -96,8 +124,7 @@ export function registerListDirectoryTool(server, options = {}) {
96
124
  run: (signal) => handleListDirectory(args, signal),
97
125
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, args.path ?? '.'),
98
126
  });
99
- const validatedHandler = withValidatedArgs(ListDirectoryInputSchema, handler);
100
- server.registerTool('ls', withDefaultIcons({ ...LIST_DIRECTORY_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
127
+ const wrappedHandler = wrapToolHandler(handler, {
101
128
  guard: options.isInitialized,
102
129
  progressMessage: (args) => {
103
130
  if (args.path) {
@@ -115,5 +142,7 @@ export function registerListDirectoryTool(server, options = {}) {
115
142
  const count = sc.totalEntries ?? 0;
116
143
  return `≣ ls: ${base} • ${count} ${count === 1 ? 'entry' : 'entries'}`;
117
144
  },
118
- }));
145
+ });
146
+ const validatedHandler = withValidatedArgs(ListDirectoryInputSchema, wrappedHandler);
147
+ server.registerTool('ls', withDefaultIcons({ ...LIST_DIRECTORY_TOOL }, options.iconInfo), validatedHandler);
119
148
  }
@@ -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 MOVE_FILE_TOOL: ToolContract;
3
4
  export declare function registerMoveFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -6,12 +6,14 @@ import { validateExistingPath, validatePathForWrite, } from '../lib/path-validat
6
6
  import { MoveFileInputSchema, MoveFileOutputSchema } 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 MOVE_FILE_TOOL = {
9
+ export const MOVE_FILE_TOOL = {
10
+ name: 'mv',
10
11
  title: 'Move File',
11
12
  description: 'Move or rename a file or directory.',
12
13
  inputSchema: MoveFileInputSchema,
13
14
  outputSchema: MoveFileOutputSchema,
14
15
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
16
+ nuances: ['Cross-device moves fall back to copy+delete.'],
15
17
  };
16
18
  async function handleMoveFile(args, signal) {
17
19
  const validSource = await validateExistingPath(args.source, signal);
@@ -46,8 +48,7 @@ export function registerMoveFileTool(server, options = {}) {
46
48
  run: (signal) => handleMoveFile(args, signal),
47
49
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.source),
48
50
  });
49
- const validatedHandler = withValidatedArgs(MoveFileInputSchema, handler);
50
- const wrappedHandler = wrapToolHandler(validatedHandler, {
51
+ const wrappedHandler = wrapToolHandler(handler, {
51
52
  guard: options.isInitialized,
52
53
  progressMessage: (args) => `🛠 mv: ${path.basename(args.source)} → ${path.basename(args.destination)}`,
53
54
  completionMessage: (args, result) => {
@@ -58,7 +59,8 @@ export function registerMoveFileTool(server, options = {}) {
58
59
  return `🛠 mv: ${src} → ${dst} • moved`;
59
60
  },
60
61
  });
61
- if (registerToolTaskIfAvailable(server, 'mv', MOVE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
62
+ const validatedHandler = withValidatedArgs(MoveFileInputSchema, wrappedHandler);
63
+ if (registerToolTaskIfAvailable(server, 'mv', MOVE_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
62
64
  return;
63
- server.registerTool('mv', withDefaultIcons({ ...MOVE_FILE_TOOL }, options.iconInfo), wrappedHandler);
65
+ server.registerTool('mv', withDefaultIcons({ ...MOVE_FILE_TOOL }, options.iconInfo), validatedHandler);
64
66
  }
@@ -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 READ_MULTIPLE_FILES_TOOL: ToolContract;
3
4
  export declare function registerReadMultipleFilesTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -5,7 +5,8 @@ import { readMultipleFiles } from '../lib/file-operations/read-multiple-files.js
5
5
  import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
6
6
  import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
7
7
  import { registerToolTaskIfAvailable } from './task-support.js';
8
- const READ_MULTIPLE_FILES_TOOL = {
8
+ export const READ_MULTIPLE_FILES_TOOL = {
9
+ name: 'read_many',
9
10
  title: 'Read Multiple Files',
10
11
  description: 'Read multiple text files in a single request. ' +
11
12
  'Returns contents and metadata for each file. ' +
@@ -13,6 +14,10 @@ const READ_MULTIPLE_FILES_TOOL = {
13
14
  inputSchema: ReadMultipleFilesInputSchema,
14
15
  outputSchema: ReadMultipleFilesOutputSchema,
15
16
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
17
+ nuances: ['Total read budget is capped by `MAX_READ_MANY_TOTAL_SIZE`.'],
18
+ gotchas: [
19
+ 'Per-file `truncationReason` can be `head`, `range`, or `externalized`.',
20
+ ],
16
21
  };
17
22
  async function handleReadMultipleFiles(args, signal, resourceStore) {
18
23
  const options = {
@@ -121,8 +126,7 @@ export function registerReadMultipleFilesTool(server, options = {}) {
121
126
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, primaryPath),
122
127
  });
123
128
  };
124
- const validatedHandler = withValidatedArgs(ReadMultipleFilesInputSchema, handler);
125
- const wrappedHandler = wrapToolHandler(validatedHandler, {
129
+ const wrappedHandler = wrapToolHandler(handler, {
126
130
  guard: options.isInitialized,
127
131
  progressMessage: (args) => {
128
132
  const first = path.basename(args.paths[0] ?? '');
@@ -143,7 +147,8 @@ export function registerReadMultipleFilesTool(server, options = {}) {
143
147
  return `🕮 read_many: ${total} files read`;
144
148
  },
145
149
  });
146
- if (registerToolTaskIfAvailable(server, 'read_many', READ_MULTIPLE_FILES_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
150
+ const validatedHandler = withValidatedArgs(ReadMultipleFilesInputSchema, wrappedHandler);
151
+ if (registerToolTaskIfAvailable(server, 'read_many', READ_MULTIPLE_FILES_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
147
152
  return;
148
- server.registerTool('read_many', withDefaultIcons({ ...READ_MULTIPLE_FILES_TOOL }, options.iconInfo), wrappedHandler);
153
+ server.registerTool('read_many', withDefaultIcons({ ...READ_MULTIPLE_FILES_TOOL }, options.iconInfo), validatedHandler);
149
154
  }
@@ -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 READ_FILE_TOOL: ToolContract;
3
4
  export declare function registerReadFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -5,7 +5,8 @@ import { readFile } from '../lib/fs-helpers.js';
5
5
  import { ReadFileInputSchema, ReadFileOutputSchema } from '../schemas.js';
6
6
  import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
7
7
  import { registerToolTaskIfAvailable } from './task-support.js';
8
- const READ_FILE_TOOL = {
8
+ export const READ_FILE_TOOL = {
9
+ name: 'read',
9
10
  title: 'Read File',
10
11
  description: 'Read the text contents of a file. ' +
11
12
  'Use head parameter to preview the first N lines of large files. ' +
@@ -13,6 +14,9 @@ const READ_FILE_TOOL = {
13
14
  inputSchema: ReadFileInputSchema,
14
15
  outputSchema: ReadFileOutputSchema,
15
16
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
17
+ nuances: [
18
+ 'Large content is externalized to `filesystem-mcp://result/{id}` and preview is returned inline.',
19
+ ],
16
20
  };
17
21
  async function handleReadFile(args, signal, resourceStore) {
18
22
  const options = {
@@ -80,8 +84,7 @@ export function registerReadFileTool(server, options = {}) {
80
84
  run: (signal) => handleReadFile(args, signal, options.resourceStore),
81
85
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, args.path),
82
86
  });
83
- const validatedHandler = withValidatedArgs(ReadFileInputSchema, handler);
84
- const wrappedHandler = wrapToolHandler(validatedHandler, {
87
+ const wrappedHandler = wrapToolHandler(handler, {
85
88
  guard: options.isInitialized,
86
89
  progressMessage: (args) => {
87
90
  const name = path.basename(args.path);
@@ -105,7 +108,8 @@ export function registerReadFileTool(server, options = {}) {
105
108
  return `🕮 read: ${name} • ${sc.totalLines ?? '?'} lines`;
106
109
  },
107
110
  });
108
- if (registerToolTaskIfAvailable(server, 'read', READ_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
111
+ const validatedHandler = withValidatedArgs(ReadFileInputSchema, wrappedHandler);
112
+ if (registerToolTaskIfAvailable(server, 'read', READ_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
109
113
  return;
110
- server.registerTool('read', withDefaultIcons({ ...READ_FILE_TOOL }, options.iconInfo), wrappedHandler);
114
+ server.registerTool('read', withDefaultIcons({ ...READ_FILE_TOOL }, options.iconInfo), validatedHandler);
111
115
  }
@@ -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_AND_REPLACE_TOOL: ToolContract;
3
4
  export declare function registerSearchAndReplaceTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -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
  }