@j0hanz/filesystem-mcp 1.2.2 → 1.2.3

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 (46) hide show
  1. package/README.md +11 -0
  2. package/dist/cli.js +2 -4
  3. package/dist/completions.js +15 -6
  4. package/dist/lib/file-operations/glob-engine.js +3 -9
  5. package/dist/lib/file-operations/read-multiple-files.js +4 -23
  6. package/dist/lib/file-operations/search-content.js +11 -18
  7. package/dist/lib/observability.js +4 -4
  8. package/dist/lib/path-validation.js +22 -9
  9. package/dist/pkg-info.d.ts +6 -0
  10. package/dist/pkg-info.js +9 -0
  11. package/dist/schemas.d.ts +28 -28
  12. package/dist/schemas.js +26 -26
  13. package/dist/server/bootstrap.d.ts +4 -0
  14. package/dist/server/bootstrap.js +117 -0
  15. package/dist/server/capabilities.d.ts +10 -0
  16. package/dist/server/capabilities.js +40 -0
  17. package/dist/server/logging.d.ts +7 -0
  18. package/dist/server/logging.js +41 -0
  19. package/dist/server/roots-manager.d.ts +19 -0
  20. package/dist/server/roots-manager.js +173 -0
  21. package/dist/server/types.d.ts +4 -0
  22. package/dist/server/types.js +1 -0
  23. package/dist/server.d.ts +2 -8
  24. package/dist/server.js +1 -317
  25. package/dist/tools/apply-patch.js +3 -2
  26. package/dist/tools/calculate-hash.js +3 -2
  27. package/dist/tools/create-directory.js +3 -2
  28. package/dist/tools/delete-file.js +9 -2
  29. package/dist/tools/diff-files.js +3 -2
  30. package/dist/tools/edit-file.js +5 -4
  31. package/dist/tools/list-directory.js +13 -2
  32. package/dist/tools/move-file.js +11 -3
  33. package/dist/tools/read-multiple.js +6 -5
  34. package/dist/tools/read.js +3 -2
  35. package/dist/tools/replace-in-files.js +3 -2
  36. package/dist/tools/roots.js +12 -2
  37. package/dist/tools/search-content.js +10 -15
  38. package/dist/tools/search-files.js +13 -9
  39. package/dist/tools/shared.d.ts +3 -1
  40. package/dist/tools/shared.js +19 -1
  41. package/dist/tools/stat-many.js +6 -5
  42. package/dist/tools/stat.js +4 -3
  43. package/dist/tools/task-support.js +57 -10
  44. package/dist/tools/tree.js +15 -2
  45. package/dist/tools/write-file.js +3 -2
  46. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -1,317 +1 @@
1
- import * as fs from 'node:fs/promises';
2
- import * as path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { InMemoryTaskMessageQueue, InMemoryTaskStore, } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
5
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
- import { InitializedNotificationSchema, RootsListChangedNotificationSchema, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
- import { z } from 'zod';
9
- import packageJsonRaw from '../package.json' with { type: 'json' };
10
- import { registerCompletions } from './completions.js';
11
- import { formatUnknownErrorMessage } from './lib/errors.js';
12
- import { assertNotAborted, createTimedAbortSignal, withAbort, } from './lib/fs-helpers.js';
13
- import { getAllowedDirectories, getValidRootDirectories, isPathWithinDirectories, normalizePath, setAllowedDirectoriesResolved, } from './lib/path-validation.js';
14
- import { createInMemoryResourceStore } from './lib/resource-store.js';
15
- import { isRecord } from './lib/type-guards.js';
16
- import { registerGetHelpPrompt } from './prompts.js';
17
- import { registerInstructionResource, registerResultResources, } from './resources.js';
18
- import { registerAllTools } from './tools.js';
19
- import { withDefaultIcons } from './tools/shared.js';
20
- const PackageJsonSchema = z.object({
21
- version: z.string(),
22
- description: z.string().optional(),
23
- homepage: z.string().optional(),
24
- });
25
- const { version: SERVER_VERSION, description: SERVER_DESCRIPTION, homepage: SERVER_HOMEPAGE, } = PackageJsonSchema.parse(packageJsonRaw);
26
- function normalizeCLIDirectories(dirs) {
27
- const normalized = [];
28
- for (const dir of dirs) {
29
- const trimmed = dir.trim();
30
- if (trimmed.length === 0)
31
- continue;
32
- normalized.push(normalizePath(trimmed));
33
- }
34
- return normalized;
35
- }
36
- const ROOTS_TIMEOUT_MS = 5000;
37
- const ROOTS_DEBOUNCE_MS = 100;
38
- const MCP_LOGGER_NAME = 'filesystem-mcp';
39
- const LOG_LEVEL_ORDER = {
40
- debug: 0,
41
- info: 1,
42
- notice: 2,
43
- warning: 3,
44
- error: 4,
45
- critical: 5,
46
- alert: 6,
47
- emergency: 7,
48
- };
49
- function canSendMcpLogs(server) {
50
- const capabilities = server.server.getClientCapabilities();
51
- if (!isRecord(capabilities))
52
- return false;
53
- if (!('logging' in capabilities))
54
- return false;
55
- return Boolean(capabilities.logging);
56
- }
57
- function logToMcp(server, level, data, minLevel = 'debug') {
58
- if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[minLevel]) {
59
- return;
60
- }
61
- if (!server || !canSendMcpLogs(server)) {
62
- console.error(data);
63
- return;
64
- }
65
- const params = {
66
- level,
67
- logger: MCP_LOGGER_NAME,
68
- data,
69
- };
70
- void server.sendLoggingMessage(params).catch((error) => {
71
- console.error(`Failed to send MCP log: ${level} │ ${data}`, formatUnknownErrorMessage(error));
72
- });
73
- }
74
- class RootsManager {
75
- rootsUpdateTimeout;
76
- rootDirectories = [];
77
- clientInitialized = false;
78
- options;
79
- loggingState;
80
- constructor(options, loggingState) {
81
- this.options = options;
82
- this.loggingState = loggingState ?? { minimumLevel: 'debug' };
83
- }
84
- isInitialized() {
85
- return this.clientInitialized;
86
- }
87
- logMissingDirectoriesIfNeeded(server) {
88
- if (getAllowedDirectories().length === 0) {
89
- this.logMissingDirectories(server);
90
- }
91
- }
92
- registerHandlers(server) {
93
- server.server.setNotificationHandler(InitializedNotificationSchema, async () => {
94
- this.clientInitialized = true;
95
- await this.updateRootsFromClient(server);
96
- });
97
- server.server.setNotificationHandler(RootsListChangedNotificationSchema, () => {
98
- if (!this.clientInitialized)
99
- return;
100
- this.scheduleRootsUpdate(server);
101
- });
102
- }
103
- scheduleRootsUpdate(server) {
104
- if (this.rootsUpdateTimeout) {
105
- this.rootsUpdateTimeout.refresh();
106
- return;
107
- }
108
- this.rootsUpdateTimeout = setTimeout(() => {
109
- this.rootsUpdateTimeout = undefined;
110
- void this.updateRootsFromClient(server);
111
- }, ROOTS_DEBOUNCE_MS);
112
- this.rootsUpdateTimeout.unref();
113
- }
114
- async recomputeAllowedDirectories() {
115
- const cliAllowedDirs = normalizeCLIDirectories(this.options.cliAllowedDirs ?? []);
116
- const allowCwd = this.options.allowCwd === true;
117
- const allowCwdDirs = allowCwd ? [normalizePath(process.cwd())] : [];
118
- const baseline = [...cliAllowedDirs, ...allowCwdDirs];
119
- const { signal, cleanup } = createTimedAbortSignal(undefined, ROOTS_TIMEOUT_MS);
120
- try {
121
- const rootsToInclude = baseline.length > 0
122
- ? await filterRootsWithinBaseline(this.rootDirectories, baseline, signal)
123
- : this.rootDirectories;
124
- const combined = [...baseline, ...rootsToInclude];
125
- await setAllowedDirectoriesResolved(combined, signal);
126
- }
127
- finally {
128
- cleanup();
129
- }
130
- }
131
- logMissingDirectories(server) {
132
- if (this.options.allowCwd) {
133
- logToMcp(server, 'notice', 'No allowed directories specified. Using the current working directory as an allowed directory.', this.loggingState.minimumLevel);
134
- return;
135
- }
136
- logToMcp(server, 'warning', 'No allowed directories specified. Please provide directories as command-line arguments or enable --allow-cwd to use the current working directory.', this.loggingState.minimumLevel);
137
- }
138
- async updateRootsFromClient(server) {
139
- try {
140
- const clientCapabilities = server.server.getClientCapabilities();
141
- if (!clientCapabilities?.roots) {
142
- this.rootDirectories = [];
143
- return;
144
- }
145
- const rootsResult = await server.server.listRoots(undefined, {
146
- timeout: ROOTS_TIMEOUT_MS,
147
- });
148
- const roots = extractRoots(rootsResult);
149
- this.rootDirectories = await resolveRootDirectories(roots);
150
- }
151
- catch (error) {
152
- logToMcp(server, 'debug', `[DEBUG] MCP Roots protocol unavailable or failed: ${formatUnknownErrorMessage(error)}`, this.loggingState.minimumLevel);
153
- }
154
- finally {
155
- await this.recomputeAllowedDirectories();
156
- }
157
- }
158
- }
159
- const rootsManagers = new WeakMap();
160
- function getRootsManager(server) {
161
- const manager = rootsManagers.get(server);
162
- if (!manager) {
163
- throw new Error('Roots manager not initialized for server instance');
164
- }
165
- return manager;
166
- }
167
- const RootSchema = z.strictObject({
168
- uri: z.string(),
169
- name: z.string().optional(),
170
- });
171
- const RootsResponseSchema = z.object({
172
- roots: z.array(RootSchema).optional(),
173
- });
174
- function extractRoots(value) {
175
- const parsed = RootsResponseSchema.safeParse(value);
176
- if (!parsed.success || !parsed.data.roots) {
177
- return [];
178
- }
179
- const roots = [];
180
- for (const root of parsed.data.roots) {
181
- if (isRoot(root)) {
182
- roots.push(normalizeRoot(root));
183
- }
184
- }
185
- return roots;
186
- }
187
- async function resolveRootDirectories(roots) {
188
- if (roots.length === 0)
189
- return [];
190
- const { signal, cleanup } = createTimedAbortSignal(undefined, ROOTS_TIMEOUT_MS);
191
- try {
192
- return await getValidRootDirectories(roots, signal);
193
- }
194
- finally {
195
- cleanup();
196
- }
197
- }
198
- function isRoot(value) {
199
- return isRecord(value) && typeof value['uri'] === 'string';
200
- }
201
- function normalizeRoot(root) {
202
- return root.name ? { uri: root.uri, name: root.name } : { uri: root.uri };
203
- }
204
- async function filterRootsWithinBaseline(roots, baseline, signal) {
205
- const normalizedBaseline = normalizeCLIDirectories(baseline);
206
- const filtered = [];
207
- for (const root of roots) {
208
- const normalizedRoot = normalizePath(root);
209
- const isValid = await isRootWithinBaseline(normalizedRoot, normalizedBaseline, signal);
210
- if (isValid)
211
- filtered.push(normalizedRoot);
212
- }
213
- return filtered;
214
- }
215
- async function isRootWithinBaseline(normalizedRoot, baseline, signal) {
216
- if (!isPathWithinDirectories(normalizedRoot, baseline)) {
217
- return false;
218
- }
219
- try {
220
- assertNotAborted(signal);
221
- const realPath = await withAbort(fs.realpath(normalizedRoot), signal);
222
- const normalizedReal = normalizePath(realPath);
223
- return isPathWithinDirectories(normalizedReal, baseline);
224
- }
225
- catch {
226
- return false;
227
- }
228
- }
229
- async function loadServerInstructions() {
230
- const defaultInstructions = `
231
- Filesystem MCP Instructions
232
- (Detailed instructions failed to load - check logs)
233
- `;
234
- try {
235
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
236
- return await fs.readFile(path.join(currentDir, 'instructions.md'), 'utf-8');
237
- }
238
- catch (error) {
239
- console.error('[WARNING] Failed to load instructions.md:', formatUnknownErrorMessage(error));
240
- return defaultInstructions;
241
- }
242
- }
243
- async function getLocalIconInfo() {
244
- const name = 'logo.svg';
245
- const mime = 'image/svg+xml';
246
- try {
247
- const iconPath = new URL(`../assets/${name}`, import.meta.url);
248
- const buffer = await fs.readFile(iconPath);
249
- return {
250
- src: `data:${mime};base64,${buffer.toString('base64')}`,
251
- mimeType: mime,
252
- };
253
- }
254
- catch {
255
- return undefined;
256
- }
257
- }
258
- export async function createServer(options = {}) {
259
- const resourceStore = createInMemoryResourceStore();
260
- const serverInstructions = await loadServerInstructions();
261
- const localIcon = await getLocalIconInfo();
262
- const taskStore = new InMemoryTaskStore();
263
- const taskMessageQueue = new InMemoryTaskMessageQueue();
264
- const serverConfig = {
265
- capabilities: {
266
- logging: {},
267
- resources: {},
268
- tools: {},
269
- prompts: { listChanged: true },
270
- completions: {},
271
- tasks: {
272
- list: {},
273
- cancel: {},
274
- requests: { tools: { call: {} } },
275
- },
276
- },
277
- taskStore,
278
- taskMessageQueue,
279
- };
280
- if (serverInstructions) {
281
- serverConfig.instructions = serverInstructions;
282
- }
283
- const server = new McpServer(withDefaultIcons({
284
- name: 'filesystem-mcp',
285
- title: 'Filesystem MCP',
286
- version: SERVER_VERSION,
287
- ...(SERVER_DESCRIPTION ? { description: SERVER_DESCRIPTION } : {}),
288
- ...(SERVER_HOMEPAGE ? { websiteUrl: SERVER_HOMEPAGE } : {}),
289
- }, localIcon), serverConfig);
290
- const loggingState = {
291
- minimumLevel: 'debug',
292
- };
293
- const rootsManager = new RootsManager(options, loggingState);
294
- rootsManagers.set(server, rootsManager);
295
- server.server.setRequestHandler(SetLevelRequestSchema, (req) => {
296
- loggingState.minimumLevel = req.params.level;
297
- return {};
298
- });
299
- registerInstructionResource(server, serverInstructions, localIcon);
300
- registerGetHelpPrompt(server, serverInstructions, localIcon);
301
- registerResultResources(server, resourceStore, localIcon);
302
- registerCompletions(server);
303
- registerAllTools(server, {
304
- resourceStore,
305
- isInitialized: () => rootsManager.isInitialized(),
306
- ...(localIcon ? { iconInfo: localIcon } : {}),
307
- });
308
- return server;
309
- }
310
- export async function startServer(server) {
311
- const transport = new StdioServerTransport();
312
- const rootsManager = getRootsManager(server);
313
- rootsManager.registerHandlers(server);
314
- await rootsManager.recomputeAllowedDirectories();
315
- await server.connect(transport);
316
- rootsManager.logMissingDirectoriesIfNeeded(server);
317
- }
1
+ export { createServer, startServer } from './server/bootstrap.js';
@@ -6,7 +6,7 @@ import { ErrorCode, McpError } from '../lib/errors.js';
6
6
  import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
7
7
  import { validateExistingPath } from '../lib/path-validation.js';
8
8
  import { ApplyPatchInputSchema, ApplyPatchOutputSchema } from '../schemas.js';
9
- import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
9
+ import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
10
10
  import { registerToolTaskIfAvailable } from './task-support.js';
11
11
  const APPLY_PATCH_TOOL = {
12
12
  title: 'Apply Patch',
@@ -69,7 +69,8 @@ export function registerApplyPatchTool(server, options = {}) {
69
69
  run: (signal) => handleApplyPatch(args, signal),
70
70
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
71
71
  });
72
- const wrappedHandler = wrapToolHandler(handler, {
72
+ const validatedHandler = withValidatedArgs(ApplyPatchInputSchema, handler);
73
+ const wrappedHandler = wrapToolHandler(validatedHandler, {
73
74
  guard: options.isInitialized,
74
75
  progressMessage: (args) => {
75
76
  const name = path.basename(args.path);
@@ -9,7 +9,7 @@ import { globEntries } from '../lib/file-operations/glob-engine.js';
9
9
  import { assertNotAborted, withAbort } from '../lib/fs-helpers.js';
10
10
  import { validateExistingPath } from '../lib/path-validation.js';
11
11
  import { CalculateHashInputSchema, CalculateHashOutputSchema, } from '../schemas.js';
12
- import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
12
+ import { buildToolErrorResponse, buildToolResponse, createProgressReporter, executeToolWithDiagnostics, notifyProgress, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
13
13
  import { registerToolTaskIfAvailable } from './task-support.js';
14
14
  const WINDOWS_PATH_SEPARATOR = /\\/gu;
15
15
  const CALCULATE_HASH_TOOL = {
@@ -204,7 +204,8 @@ export function registerCalculateHashTool(server, options = {}) {
204
204
  },
205
205
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
206
206
  });
207
- const wrappedHandler = wrapToolHandler(handler, {
207
+ const validatedHandler = withValidatedArgs(CalculateHashInputSchema, handler);
208
+ const wrappedHandler = wrapToolHandler(validatedHandler, {
208
209
  guard: options.isInitialized,
209
210
  });
210
211
  if (registerToolTaskIfAvailable(server, 'calculate_hash', CALCULATE_HASH_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
@@ -4,7 +4,7 @@ import { ErrorCode } from '../lib/errors.js';
4
4
  import { withAbort } from '../lib/fs-helpers.js';
5
5
  import { validatePathForWrite } from '../lib/path-validation.js';
6
6
  import { CreateDirectoryInputSchema, CreateDirectoryOutputSchema, } from '../schemas.js';
7
- import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, IDEMPOTENT_WRITE_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
7
+ import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, IDEMPOTENT_WRITE_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  import { registerToolTaskIfAvailable } from './task-support.js';
9
9
  const CREATE_DIRECTORY_TOOL = {
10
10
  title: 'Create Directory',
@@ -30,7 +30,8 @@ export function registerCreateDirectoryTool(server, options = {}) {
30
30
  run: (signal) => handleCreateDirectory(args, signal),
31
31
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
32
32
  });
33
- const wrappedHandler = wrapToolHandler(handler, {
33
+ const validatedHandler = withValidatedArgs(CreateDirectoryInputSchema, handler);
34
+ const wrappedHandler = wrapToolHandler(validatedHandler, {
34
35
  guard: options.isInitialized,
35
36
  progressMessage: (args) => {
36
37
  const name = path.basename(args.path) || args.path;
@@ -4,7 +4,7 @@ import { ErrorCode, isNodeError } from '../lib/errors.js';
4
4
  import { withAbort } from '../lib/fs-helpers.js';
5
5
  import { validatePathForWrite } from '../lib/path-validation.js';
6
6
  import { DeleteFileInputSchema, DeleteFileOutputSchema } from '../schemas.js';
7
- import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
7
+ import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  import { registerToolTaskIfAvailable } from './task-support.js';
9
9
  const DELETE_FILE_TOOL = {
10
10
  title: 'Delete File',
@@ -74,9 +74,16 @@ export function registerDeleteFileTool(server, options = {}) {
74
74
  return buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path);
75
75
  },
76
76
  });
77
- const wrappedHandler = wrapToolHandler(handler, {
77
+ const validatedHandler = withValidatedArgs(DeleteFileInputSchema, handler);
78
+ const wrappedHandler = wrapToolHandler(validatedHandler, {
78
79
  guard: options.isInitialized,
79
80
  progressMessage: (args) => `🛠 rm: ${path.basename(args.path)}`,
81
+ completionMessage: (args, result) => {
82
+ const name = path.basename(args.path);
83
+ if (result.isError)
84
+ return `🛠 rm: ${name} • failed`;
85
+ return `🛠 rm: ${name} • deleted`;
86
+ },
80
87
  });
81
88
  if (registerToolTaskIfAvailable(server, 'rm', DELETE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
82
89
  return;
@@ -6,7 +6,7 @@ import { ErrorCode, McpError } from '../lib/errors.js';
6
6
  import { withAbort } from '../lib/fs-helpers.js';
7
7
  import { validateExistingPath } from '../lib/path-validation.js';
8
8
  import { DiffFilesInputSchema, DiffFilesOutputSchema } from '../schemas.js';
9
- import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
9
+ import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
10
10
  const DIFF_FILES_TOOL = {
11
11
  title: 'Diff Files',
12
12
  description: 'Generate a unified diff between two files. ' +
@@ -80,7 +80,8 @@ export function registerDiffFilesTool(server, options = {}) {
80
80
  run: (signal) => handleDiffFiles(args, signal, options.resourceStore),
81
81
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.original),
82
82
  });
83
- server.registerTool('diff_files', withDefaultIcons({ ...DIFF_FILES_TOOL }, options.iconInfo), wrapToolHandler(handler, {
83
+ const validatedHandler = withValidatedArgs(DiffFilesInputSchema, handler);
84
+ server.registerTool('diff_files', withDefaultIcons({ ...DIFF_FILES_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
84
85
  guard: options.isInitialized,
85
86
  progressMessage: (args) => {
86
87
  const name1 = path.basename(args.original);
@@ -4,7 +4,7 @@ import { ErrorCode } from '../lib/errors.js';
4
4
  import { atomicWriteFile } from '../lib/fs-helpers.js';
5
5
  import { validateExistingPath } from '../lib/path-validation.js';
6
6
  import { EditFileInputSchema, EditFileOutputSchema } from '../schemas.js';
7
- import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
7
+ import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  const EDIT_FILE_TOOL = {
9
9
  title: 'Edit File',
10
10
  description: 'Edit a file by replacing text. Sequentially applies a list of string replacements. ' +
@@ -79,7 +79,8 @@ export function registerEditFileTool(server, options = {}) {
79
79
  run: (signal) => handleEditFile(args, signal),
80
80
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
81
81
  });
82
- server.registerTool('edit', withDefaultIcons({ ...EDIT_FILE_TOOL }, options.iconInfo), wrapToolHandler(handler, {
82
+ const validatedHandler = withValidatedArgs(EditFileInputSchema, handler);
83
+ server.registerTool('edit', withDefaultIcons({ ...EDIT_FILE_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
83
84
  guard: options.isInitialized,
84
85
  progressMessage: (args) => {
85
86
  const name = path.basename(args.path);
@@ -88,10 +89,10 @@ export function registerEditFileTool(server, options = {}) {
88
89
  completionMessage: (args, result) => {
89
90
  const name = path.basename(args.path);
90
91
  if (result.isError)
91
- return `🛠 edit: ${name} • Failed`;
92
+ return `🛠 edit: ${name} • failed`;
92
93
  const sc = result.structuredContent;
93
94
  if (!sc.ok)
94
- return `🛠 edit: ${name} • Failed`;
95
+ return `🛠 edit: ${name} • failed`;
95
96
  if (sc.lineRange) {
96
97
  return `🛠 edit: ${name} • [${sc.lineRange[0]}-${sc.lineRange[1]}]`;
97
98
  }
@@ -4,7 +4,7 @@ import { DEFAULT_EXCLUDE_PATTERNS } from '../lib/constants.js';
4
4
  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
- import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
7
+ import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, resolvePathOrRoot, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  const LIST_DIRECTORY_TOOL = {
9
9
  title: 'List Directory',
10
10
  description: 'List the immediate contents of a directory (non-recursive). ' +
@@ -96,7 +96,8 @@ export function registerListDirectoryTool(server, options = {}) {
96
96
  run: (signal) => handleListDirectory(args, signal),
97
97
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, args.path ?? '.'),
98
98
  });
99
- server.registerTool('ls', withDefaultIcons({ ...LIST_DIRECTORY_TOOL }, options.iconInfo), wrapToolHandler(handler, {
99
+ const validatedHandler = withValidatedArgs(ListDirectoryInputSchema, handler);
100
+ server.registerTool('ls', withDefaultIcons({ ...LIST_DIRECTORY_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
100
101
  guard: options.isInitialized,
101
102
  progressMessage: (args) => {
102
103
  if (args.path) {
@@ -104,5 +105,15 @@ export function registerListDirectoryTool(server, options = {}) {
104
105
  }
105
106
  return '≣ ls';
106
107
  },
108
+ completionMessage: (args, result) => {
109
+ const base = args.path ? path.basename(args.path) : '.';
110
+ if (result.isError)
111
+ return `≣ ls: ${base} • failed`;
112
+ const sc = result.structuredContent;
113
+ if (!sc.ok)
114
+ return `≣ ls: ${base} • failed`;
115
+ const count = sc.totalEntries ?? 0;
116
+ return `≣ ls: ${base} • ${count} ${count === 1 ? 'entry' : 'entries'}`;
117
+ },
107
118
  }));
108
119
  }
@@ -4,7 +4,7 @@ import { ErrorCode, isNodeError } from '../lib/errors.js';
4
4
  import { withAbort } from '../lib/fs-helpers.js';
5
5
  import { validateExistingPath, validatePathForWrite, } from '../lib/path-validation.js';
6
6
  import { MoveFileInputSchema, MoveFileOutputSchema } from '../schemas.js';
7
- import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, wrapToolHandler, } from './shared.js';
7
+ import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  import { registerToolTaskIfAvailable } from './task-support.js';
9
9
  const MOVE_FILE_TOOL = {
10
10
  title: 'Move File',
@@ -46,9 +46,17 @@ export function registerMoveFileTool(server, options = {}) {
46
46
  run: (signal) => handleMoveFile(args, signal),
47
47
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.source),
48
48
  });
49
- const wrappedHandler = wrapToolHandler(handler, {
49
+ const validatedHandler = withValidatedArgs(MoveFileInputSchema, handler);
50
+ const wrappedHandler = wrapToolHandler(validatedHandler, {
50
51
  guard: options.isInitialized,
51
- progressMessage: (args) => `🛠 mv: ${path.basename(args.source)} ${path.basename(args.destination)}`,
52
+ progressMessage: (args) => `🛠 mv: ${path.basename(args.source)} ${path.basename(args.destination)}`,
53
+ completionMessage: (args, result) => {
54
+ const src = path.basename(args.source);
55
+ const dst = path.basename(args.destination);
56
+ if (result.isError)
57
+ return `🛠 mv: ${src} → ${dst} • failed`;
58
+ return `🛠 mv: ${src} → ${dst} • moved`;
59
+ },
52
60
  });
53
61
  if (registerToolTaskIfAvailable(server, 'mv', MOVE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
54
62
  return;
@@ -3,7 +3,7 @@ import { DEFAULT_READ_MANY_MAX_TOTAL_SIZE, DEFAULT_SEARCH_TIMEOUT_MS, } from '..
3
3
  import { ErrorCode } from '../lib/errors.js';
4
4
  import { readMultipleFiles } from '../lib/file-operations/read-multiple-files.js';
5
5
  import { ReadMultipleFilesInputSchema, ReadMultipleFilesOutputSchema, } from '../schemas.js';
6
- import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
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
8
  const READ_MULTIPLE_FILES_TOOL = {
9
9
  title: 'Read Multiple Files',
@@ -121,19 +121,20 @@ export function registerReadMultipleFilesTool(server, options = {}) {
121
121
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, primaryPath),
122
122
  });
123
123
  };
124
- const wrappedHandler = wrapToolHandler(handler, {
124
+ const validatedHandler = withValidatedArgs(ReadMultipleFilesInputSchema, handler);
125
+ const wrappedHandler = wrapToolHandler(validatedHandler, {
125
126
  guard: options.isInitialized,
126
127
  progressMessage: (args) => {
127
128
  const first = path.basename(args.paths[0] ?? '');
128
129
  const extra = args.paths.length > 1 ? `, ${path.basename(args.paths[1] ?? '')}…` : '';
129
130
  return `🕮 read_many: ${args.paths.length} files [${first}${extra}]`;
130
131
  },
131
- completionMessage: (_args, result) => {
132
+ completionMessage: (args, result) => {
132
133
  if (result.isError)
133
- return `🕮 read_many • failed`;
134
+ return `🕮 read_many: ${args.paths.length} files • failed`;
134
135
  const sc = result.structuredContent;
135
136
  if (!sc.ok)
136
- return `🕮 read_many • failed`;
137
+ return `🕮 read_many: ${args.paths.length} files • failed`;
137
138
  const total = sc.summary?.total ?? 0;
138
139
  const succeeded = sc.summary?.succeeded ?? 0;
139
140
  const failed = sc.summary?.failed ?? 0;
@@ -3,7 +3,7 @@ import { DEFAULT_SEARCH_TIMEOUT_MS, MAX_TEXT_FILE_SIZE, } from '../lib/constants
3
3
  import { ErrorCode } from '../lib/errors.js';
4
4
  import { readFile } from '../lib/fs-helpers.js';
5
5
  import { ReadFileInputSchema, ReadFileOutputSchema } from '../schemas.js';
6
- import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
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
8
  const READ_FILE_TOOL = {
9
9
  title: 'Read File',
@@ -80,7 +80,8 @@ export function registerReadFileTool(server, options = {}) {
80
80
  run: (signal) => handleReadFile(args, signal, options.resourceStore),
81
81
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, args.path),
82
82
  });
83
- const wrappedHandler = wrapToolHandler(handler, {
83
+ const validatedHandler = withValidatedArgs(ReadFileInputSchema, handler);
84
+ const wrappedHandler = wrapToolHandler(validatedHandler, {
84
85
  guard: options.isInitialized,
85
86
  progressMessage: (args) => {
86
87
  const name = path.basename(args.path);
@@ -8,7 +8,7 @@ import { globEntries } from '../lib/file-operations/glob-engine.js';
8
8
  import { atomicWriteFile, withAbort } from '../lib/fs-helpers.js';
9
9
  import { validateExistingPath, validatePathForWrite, } from '../lib/path-validation.js';
10
10
  import { SearchAndReplaceInputSchema, SearchAndReplaceOutputSchema, } from '../schemas.js';
11
- import { buildToolErrorResponse, buildToolResponse, createProgressReporter, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, notifyProgress, resolvePathOrRoot, withDefaultIcons, wrapToolHandler, } from './shared.js';
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
13
  const SEARCH_AND_REPLACE_TOOL = {
14
14
  title: 'Search and Replace',
@@ -279,7 +279,8 @@ export function registerSearchAndReplaceTool(server, options = {}) {
279
279
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
280
280
  });
281
281
  const { isInitialized } = options;
282
- const wrappedHandler = wrapToolHandler(handler, {
282
+ const validatedHandler = withValidatedArgs(SearchAndReplaceInputSchema, handler);
283
+ const wrappedHandler = wrapToolHandler(validatedHandler, {
283
284
  guard: isInitialized,
284
285
  });
285
286
  if (registerToolTaskIfAvailable(server, 'search_and_replace', SEARCH_AND_REPLACE_TOOL, wrappedHandler, options.iconInfo, isInitialized))
@@ -2,7 +2,7 @@ import { joinLines } from '../config.js';
2
2
  import { ErrorCode } from '../lib/errors.js';
3
3
  import { getAllowedDirectories } from '../lib/path-validation.js';
4
4
  import { ListAllowedDirectoriesInputSchema, ListAllowedDirectoriesOutputSchema, } from '../schemas.js';
5
- import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, wrapToolHandler, } from './shared.js';
5
+ import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
6
6
  const LIST_ALLOWED_DIRECTORIES_TOOL = {
7
7
  title: 'Workspace Roots',
8
8
  description: 'List the workspace roots this server can access. ' +
@@ -38,8 +38,18 @@ export function registerListAllowedDirectoriesTool(server, options = {}) {
38
38
  run: () => handleListAllowedDirectories(),
39
39
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN),
40
40
  });
41
- server.registerTool('roots', withDefaultIcons({ ...LIST_ALLOWED_DIRECTORIES_TOOL }, options.iconInfo), wrapToolHandler(handler, {
41
+ const validatedHandler = withValidatedArgs(ListAllowedDirectoriesInputSchema, handler);
42
+ server.registerTool('roots', withDefaultIcons({ ...LIST_ALLOWED_DIRECTORIES_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
42
43
  guard: options.isInitialized,
43
44
  progressMessage: () => '≣ roots',
45
+ completionMessage: (_args, result) => {
46
+ if (result.isError)
47
+ return `≣ roots • failed`;
48
+ const sc = result.structuredContent;
49
+ if (!sc.ok)
50
+ return `≣ roots • failed`;
51
+ const count = sc.rootsCount ?? 0;
52
+ return `≣ roots • ${count} ${count === 1 ? 'root' : 'roots'}`;
53
+ },
44
54
  }));
45
55
  }