@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
@@ -1,16 +1,18 @@
1
1
  import * as fs from 'node:fs/promises';
2
- import * as path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
2
+ import * as http from 'node:http';
3
+ import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
4
4
  import { InMemoryTaskMessageQueue, InMemoryTaskStore, } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
5
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
- import { SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js';
7
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
8
+ import { isInitializeRequest, SetLevelRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
9
  import { registerCompletions } from '../completions.js';
9
10
  import { formatUnknownErrorMessage } from '../lib/errors.js';
10
11
  import { createInMemoryResourceStore } from '../lib/resource-store.js';
11
12
  import { pkgInfo } from '../pkg-info.js';
12
13
  import { registerGetHelpPrompt } from '../prompts.js';
13
- import { registerInstructionResource, registerResultResources, } from '../resources.js';
14
+ import { registerInstructionResource, registerMetricsResource, registerResultResources, } from '../resources.js';
15
+ import { buildServerInstructions } from '../resources/generated-instructions.js';
14
16
  import { registerAllTools } from '../tools.js';
15
17
  import { withDefaultIcons } from '../tools/shared.js';
16
18
  import { buildServerCapabilities, supportsTaskToolRequests, } from './capabilities.js';
@@ -25,19 +27,8 @@ function getRootsManager(server) {
25
27
  }
26
28
  return manager;
27
29
  }
28
- async function loadServerInstructions() {
29
- const defaultInstructions = `
30
- Filesystem MCP Instructions
31
- (Detailed instructions failed to load - check logs)
32
- `;
33
- try {
34
- const currentDir = path.dirname(fileURLToPath(import.meta.url));
35
- return await fs.readFile(path.join(currentDir, '../instructions.md'), 'utf-8');
36
- }
37
- catch (error) {
38
- console.error('[WARNING] Failed to load instructions.md:', formatUnknownErrorMessage(error));
39
- return defaultInstructions;
40
- }
30
+ function loadServerInstructions() {
31
+ return buildServerInstructions();
41
32
  }
42
33
  async function getLocalIconInfo() {
43
34
  const name = 'logo.svg';
@@ -60,7 +51,7 @@ async function getLocalIconInfo() {
60
51
  }
61
52
  export async function createServer(options = {}) {
62
53
  const resourceStore = createInMemoryResourceStore();
63
- const serverInstructions = await loadServerInstructions();
54
+ const serverInstructions = loadServerInstructions();
64
55
  const localIcon = await getLocalIconInfo();
65
56
  const taskToolSupport = supportsTaskToolRequests();
66
57
  const serverConfig = {
@@ -74,7 +65,10 @@ export async function createServer(options = {}) {
74
65
  serverConfig.taskMessageQueue = new InMemoryTaskMessageQueue();
75
66
  }
76
67
  if (serverInstructions) {
77
- serverConfig.instructions = serverInstructions;
68
+ serverConfig.instructions =
69
+ 'filesystem-mcp: Secure local filesystem MCP server. ' +
70
+ 'Essential sequence: roots → ls/tree/find → read/grep. ' +
71
+ 'Full reference: read the internal://instructions resource or invoke the get-help prompt.';
78
72
  }
79
73
  const server = new McpServer(withDefaultIcons({
80
74
  name: 'filesystem-mcp',
@@ -93,7 +87,8 @@ export async function createServer(options = {}) {
93
87
  registerInstructionResource(server, serverInstructions, localIcon);
94
88
  registerGetHelpPrompt(server, serverInstructions, localIcon);
95
89
  registerResultResources(server, resourceStore, localIcon);
96
- registerCompletions(server);
90
+ registerMetricsResource(server, localIcon);
91
+ registerCompletions(server, serverInstructions);
97
92
  registerAllTools(server, {
98
93
  resourceStore,
99
94
  isInitialized: () => rootsManager.isInitialized(),
@@ -115,3 +110,214 @@ export async function startServer(server) {
115
110
  };
116
111
  rootsManager.logMissingDirectoriesIfNeeded(server);
117
112
  }
113
+ async function readRequestBody(req) {
114
+ return new Promise((resolve, reject) => {
115
+ const chunks = [];
116
+ req.on('data', (chunk) => {
117
+ chunks.push(chunk);
118
+ });
119
+ req.on('end', () => {
120
+ const raw = Buffer.concat(chunks).toString('utf-8');
121
+ if (!raw) {
122
+ resolve(undefined);
123
+ return;
124
+ }
125
+ try {
126
+ resolve(JSON.parse(raw));
127
+ }
128
+ catch {
129
+ resolve(undefined);
130
+ }
131
+ });
132
+ req.on('error', reject);
133
+ });
134
+ }
135
+ async function createHttpSession(options, sessions) {
136
+ const mcpServer = await createServer(options);
137
+ const rootsManager = getRootsManager(mcpServer);
138
+ rootsManager.registerHandlers(mcpServer);
139
+ await rootsManager.recomputeAllowedDirectories();
140
+ const transport = new StreamableHTTPServerTransport({
141
+ sessionIdGenerator: () => randomUUID(),
142
+ onsessioninitialized: (sessionId) => {
143
+ sessions.set(sessionId, { server: mcpServer, transport });
144
+ rootsManager.logMissingDirectoriesIfNeeded(mcpServer);
145
+ },
146
+ onsessionclosed: (sessionId) => {
147
+ sessions.delete(sessionId);
148
+ },
149
+ });
150
+ transport.onclose = () => {
151
+ const { sessionId } = transport;
152
+ if (sessionId) {
153
+ sessions.delete(sessionId);
154
+ }
155
+ rootsManager.destroy();
156
+ mcpServer.close().catch((err) => {
157
+ console.error('[HTTP] Error closing MCP server:', formatUnknownErrorMessage(err));
158
+ });
159
+ };
160
+ await mcpServer.connect(transport);
161
+ return { server: mcpServer, transport };
162
+ }
163
+ export async function startHttpServer(port, options) {
164
+ const sessions = new Map();
165
+ async function handleMcpRequest(req, res) {
166
+ const { method } = req;
167
+ const sessionId = req.headers['mcp-session-id'];
168
+ const apiKey = process.env['FILESYSTEM_MCP_API_KEY'];
169
+ if (apiKey) {
170
+ const authHeader = req.headers['authorization'];
171
+ const bearerPrefix = 'Bearer ';
172
+ let authorized = false;
173
+ if (typeof authHeader === 'string' &&
174
+ authHeader.startsWith(bearerPrefix)) {
175
+ const userKey = authHeader.slice(bearerPrefix.length);
176
+ const expectedHash = createHash('sha256').update(apiKey).digest();
177
+ const actualHash = createHash('sha256').update(userKey).digest();
178
+ authorized = timingSafeEqual(expectedHash, actualHash);
179
+ }
180
+ if (!authorized) {
181
+ res.writeHead(401, {
182
+ 'Content-Type': 'application/json',
183
+ 'WWW-Authenticate': 'Bearer',
184
+ });
185
+ res.end(JSON.stringify({
186
+ jsonrpc: '2.0',
187
+ error: { code: -32000, message: 'Unauthorized' },
188
+ id: null,
189
+ }));
190
+ return;
191
+ }
192
+ }
193
+ try {
194
+ if (method === 'POST') {
195
+ const body = await readRequestBody(req);
196
+ if (sessionId && sessions.has(sessionId)) {
197
+ const session = sessions.get(sessionId);
198
+ if (session) {
199
+ await session.transport.handleRequest(req, res, body);
200
+ }
201
+ else {
202
+ res.writeHead(400, { 'Content-Type': 'application/json' });
203
+ res.end(JSON.stringify({
204
+ jsonrpc: '2.0',
205
+ error: {
206
+ code: -32000,
207
+ message: 'Bad Request: Session not found',
208
+ },
209
+ id: null,
210
+ }));
211
+ }
212
+ }
213
+ else if (!sessionId && isInitializeRequest(body)) {
214
+ const { transport } = await createHttpSession(options, sessions);
215
+ await transport.handleRequest(req, res, body);
216
+ }
217
+ else {
218
+ res.writeHead(400, { 'Content-Type': 'application/json' });
219
+ res.end(JSON.stringify({
220
+ jsonrpc: '2.0',
221
+ error: {
222
+ code: -32000,
223
+ message: 'Bad Request: No valid session ID provided',
224
+ },
225
+ id: null,
226
+ }));
227
+ }
228
+ }
229
+ else if (method === 'GET') {
230
+ if (!sessionId || !sessions.has(sessionId)) {
231
+ res.writeHead(400, { 'Content-Type': 'application/json' });
232
+ res.end(JSON.stringify({
233
+ jsonrpc: '2.0',
234
+ error: {
235
+ code: -32000,
236
+ message: 'Bad Request: Invalid or missing session ID',
237
+ },
238
+ id: null,
239
+ }));
240
+ return;
241
+ }
242
+ const session = sessions.get(sessionId);
243
+ if (session) {
244
+ await session.transport.handleRequest(req, res);
245
+ }
246
+ else {
247
+ res.writeHead(400, { 'Content-Type': 'application/json' });
248
+ res.end(JSON.stringify({
249
+ jsonrpc: '2.0',
250
+ error: {
251
+ code: -32000,
252
+ message: 'Bad Request: Session not found',
253
+ },
254
+ id: null,
255
+ }));
256
+ }
257
+ }
258
+ else if (method === 'DELETE') {
259
+ if (!sessionId || !sessions.has(sessionId)) {
260
+ res.writeHead(400, { 'Content-Type': 'application/json' });
261
+ res.end(JSON.stringify({
262
+ jsonrpc: '2.0',
263
+ error: {
264
+ code: -32000,
265
+ message: 'Bad Request: Invalid or missing session ID',
266
+ },
267
+ id: null,
268
+ }));
269
+ return;
270
+ }
271
+ const session = sessions.get(sessionId);
272
+ if (session) {
273
+ await session.transport.handleRequest(req, res);
274
+ }
275
+ else {
276
+ res.writeHead(400, { 'Content-Type': 'application/json' });
277
+ res.end(JSON.stringify({
278
+ jsonrpc: '2.0',
279
+ error: {
280
+ code: -32000,
281
+ message: 'Bad Request: Session not found',
282
+ },
283
+ id: null,
284
+ }));
285
+ }
286
+ }
287
+ else {
288
+ res.writeHead(405, { Allow: 'GET, POST, DELETE' });
289
+ res.end('Method Not Allowed');
290
+ }
291
+ }
292
+ catch (error) {
293
+ console.error('[HTTP] Error handling request:', formatUnknownErrorMessage(error));
294
+ if (!res.headersSent) {
295
+ res.writeHead(500, { 'Content-Type': 'application/json' });
296
+ res.end(JSON.stringify({
297
+ jsonrpc: '2.0',
298
+ error: { code: -32603, message: 'Internal Server Error' },
299
+ id: null,
300
+ }));
301
+ }
302
+ }
303
+ }
304
+ const httpServer = http.createServer((req, res) => {
305
+ const urlPath = (req.url ?? '/').split('?')[0];
306
+ if (urlPath === '/mcp') {
307
+ handleMcpRequest(req, res).catch((err) => {
308
+ console.error('[HTTP] Unhandled error in request handler:', formatUnknownErrorMessage(err));
309
+ });
310
+ }
311
+ else {
312
+ res.writeHead(404);
313
+ res.end('Not Found');
314
+ }
315
+ });
316
+ return new Promise((resolve, reject) => {
317
+ httpServer.once('error', reject);
318
+ httpServer.listen(port, () => {
319
+ console.error(`MCP HTTP server listening on port ${port}`);
320
+ resolve(httpServer);
321
+ });
322
+ });
323
+ }
package/dist/server.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { createServer, startServer } from './server/bootstrap.js';
1
+ export { createServer, startHttpServer, startServer, } from './server/bootstrap.js';
2
2
  export type { ServerOptions } from './server/types.js';
package/dist/server.js CHANGED
@@ -1 +1 @@
1
- export { createServer, startServer } from './server/bootstrap.js';
1
+ export { createServer, startHttpServer, startServer, } from './server/bootstrap.js';
@@ -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 APPLY_PATCH_TOOL: ToolContract;
3
4
  export declare function registerApplyPatchTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -8,7 +8,8 @@ import { validateExistingPath } from '../lib/path-validation.js';
8
8
  import { ApplyPatchInputSchema, ApplyPatchOutputSchema } from '../schemas.js';
9
9
  import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
10
10
  import { registerToolTaskIfAvailable } from './task-support.js';
11
- const APPLY_PATCH_TOOL = {
11
+ export const APPLY_PATCH_TOOL = {
12
+ name: 'apply_patch',
12
13
  title: 'Apply Patch',
13
14
  description: 'Apply a unified diff patch to a file. ' +
14
15
  'Generate the patch with `diff_files`, then validate with `dryRun: true` before writing. ' +
@@ -16,6 +17,7 @@ const APPLY_PATCH_TOOL = {
16
17
  inputSchema: ApplyPatchInputSchema,
17
18
  outputSchema: ApplyPatchOutputSchema,
18
19
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
20
+ gotchas: ['Patch must include valid hunk headers; use `dryRun=true` first.'],
19
21
  };
20
22
  function assertPatchTargetSizeWithinLimit(filePath, size, maxFileSize) {
21
23
  if (size <= maxFileSize)
@@ -69,8 +71,7 @@ export function registerApplyPatchTool(server, options = {}) {
69
71
  run: (signal) => handleApplyPatch(args, signal),
70
72
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
71
73
  });
72
- const validatedHandler = withValidatedArgs(ApplyPatchInputSchema, handler);
73
- const wrappedHandler = wrapToolHandler(validatedHandler, {
74
+ const wrappedHandler = wrapToolHandler(handler, {
74
75
  guard: options.isInitialized,
75
76
  progressMessage: (args) => {
76
77
  const name = path.basename(args.path);
@@ -90,7 +91,8 @@ export function registerApplyPatchTool(server, options = {}) {
90
91
  return `🛠 apply_patch: ${name} • applied`;
91
92
  },
92
93
  });
93
- if (registerToolTaskIfAvailable(server, 'apply_patch', APPLY_PATCH_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
94
+ const validatedHandler = withValidatedArgs(ApplyPatchInputSchema, wrappedHandler);
95
+ if (registerToolTaskIfAvailable(server, 'apply_patch', APPLY_PATCH_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
94
96
  return;
95
- server.registerTool('apply_patch', withDefaultIcons({ ...APPLY_PATCH_TOOL }, options.iconInfo), wrappedHandler);
97
+ server.registerTool('apply_patch', withDefaultIcons({ ...APPLY_PATCH_TOOL }, options.iconInfo), validatedHandler);
96
98
  }
@@ -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 CALCULATE_HASH_TOOL: ToolContract;
3
4
  export declare function registerCalculateHashTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -12,12 +12,16 @@ import { CalculateHashInputSchema, CalculateHashOutputSchema, } from '../schemas
12
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
- const CALCULATE_HASH_TOOL = {
15
+ export const CALCULATE_HASH_TOOL = {
16
+ name: 'calculate_hash',
16
17
  title: 'Calculate Hash',
17
18
  description: 'Calculate SHA-256 hash of a file or directory.',
18
19
  inputSchema: CalculateHashInputSchema,
19
20
  outputSchema: CalculateHashOutputSchema,
20
21
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
22
+ nuances: [
23
+ 'Directory hashing respects root `.gitignore` and sorts paths for stable output.',
24
+ ],
21
25
  };
22
26
  async function hashFile(filePath, encoding, signal) {
23
27
  assertNotAborted(signal);
@@ -204,11 +208,11 @@ export function registerCalculateHashTool(server, options = {}) {
204
208
  },
205
209
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
206
210
  });
207
- const validatedHandler = withValidatedArgs(CalculateHashInputSchema, handler);
208
- const wrappedHandler = wrapToolHandler(validatedHandler, {
211
+ const wrappedHandler = wrapToolHandler(handler, {
209
212
  guard: options.isInitialized,
210
213
  });
211
- if (registerToolTaskIfAvailable(server, 'calculate_hash', CALCULATE_HASH_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
214
+ const validatedHandler = withValidatedArgs(CalculateHashInputSchema, wrappedHandler);
215
+ if (registerToolTaskIfAvailable(server, 'calculate_hash', CALCULATE_HASH_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
212
216
  return;
213
- server.registerTool('calculate_hash', withDefaultIcons({ ...CALCULATE_HASH_TOOL }, options.iconInfo), wrappedHandler);
217
+ server.registerTool('calculate_hash', withDefaultIcons({ ...CALCULATE_HASH_TOOL }, options.iconInfo), validatedHandler);
214
218
  }
@@ -0,0 +1,41 @@
1
+ import type { ZodType } from 'zod';
2
+ export interface ToolContract {
3
+ /**
4
+ * The unique name of the tool (e.g., "read", "grep").
5
+ * This name is used in registration and client calls.
6
+ */
7
+ name: string;
8
+ /**
9
+ * A short human-readable title for documentation (e.g., "Read File").
10
+ */
11
+ title: string;
12
+ /**
13
+ * A detailed description of what the tool does.
14
+ */
15
+ description: string;
16
+ /**
17
+ * Zod schema for the tool's input arguments.
18
+ */
19
+ inputSchema: ZodType;
20
+ /**
21
+ * Zod schema for the tool's output result (optional).
22
+ */
23
+ outputSchema?: ZodType;
24
+ /**
25
+ * Optional annotations for tool behavior hints.
26
+ */
27
+ annotations?: {
28
+ readOnlyHint?: boolean;
29
+ idempotentHint?: boolean;
30
+ destructiveHint?: boolean;
31
+ openWorldHint?: boolean;
32
+ };
33
+ /**
34
+ * Specific usage nuances or edge cases for documentation.
35
+ */
36
+ nuances?: string[];
37
+ /**
38
+ * Common pitfalls or warnings for documentation.
39
+ */
40
+ gotchas?: string[];
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -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 CREATE_DIRECTORY_TOOL: ToolContract;
3
4
  export declare function registerCreateDirectoryTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -6,7 +6,8 @@ import { validatePathForWrite } from '../lib/path-validation.js';
6
6
  import { CreateDirectoryInputSchema, CreateDirectoryOutputSchema, } from '../schemas.js';
7
7
  import { buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, IDEMPOTENT_WRITE_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
8
  import { registerToolTaskIfAvailable } from './task-support.js';
9
- const CREATE_DIRECTORY_TOOL = {
9
+ export const CREATE_DIRECTORY_TOOL = {
10
+ name: 'mkdir',
10
11
  title: 'Create Directory',
11
12
  description: 'Create a new directory at the specified path (recursive)',
12
13
  inputSchema: CreateDirectoryInputSchema,
@@ -30,8 +31,7 @@ export function registerCreateDirectoryTool(server, options = {}) {
30
31
  run: (signal) => handleCreateDirectory(args, signal),
31
32
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
32
33
  });
33
- const validatedHandler = withValidatedArgs(CreateDirectoryInputSchema, handler);
34
- const wrappedHandler = wrapToolHandler(validatedHandler, {
34
+ const wrappedHandler = wrapToolHandler(handler, {
35
35
  guard: options.isInitialized,
36
36
  progressMessage: (args) => {
37
37
  const name = path.basename(args.path) || args.path;
@@ -44,7 +44,8 @@ export function registerCreateDirectoryTool(server, options = {}) {
44
44
  return `🛠 mkdir: ${name} • created`;
45
45
  },
46
46
  });
47
- if (registerToolTaskIfAvailable(server, 'mkdir', CREATE_DIRECTORY_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
47
+ const validatedHandler = withValidatedArgs(CreateDirectoryInputSchema, wrappedHandler);
48
+ if (registerToolTaskIfAvailable(server, 'mkdir', CREATE_DIRECTORY_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
48
49
  return;
49
- server.registerTool('mkdir', withDefaultIcons({ ...CREATE_DIRECTORY_TOOL }, options.iconInfo), wrappedHandler);
50
+ server.registerTool('mkdir', withDefaultIcons({ ...CREATE_DIRECTORY_TOOL }, options.iconInfo), validatedHandler);
50
51
  }
@@ -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 DELETE_FILE_TOOL: ToolContract;
3
4
  export declare function registerDeleteFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -6,12 +6,16 @@ import { validatePathForWrite } from '../lib/path-validation.js';
6
6
  import { DeleteFileInputSchema, DeleteFileOutputSchema } 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 DELETE_FILE_TOOL = {
9
+ export const DELETE_FILE_TOOL = {
10
+ name: 'rm',
10
11
  title: 'Delete File',
11
12
  description: 'Delete a file or directory.',
12
13
  inputSchema: DeleteFileInputSchema,
13
14
  outputSchema: DeleteFileOutputSchema,
14
15
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
16
+ gotchas: [
17
+ 'Non-empty directory delete requires `recursive=true`; else returns actionable input error.',
18
+ ],
15
19
  };
16
20
  async function handleDeleteFile(args, signal) {
17
21
  const validPath = await validatePathForWrite(args.path, signal);
@@ -74,8 +78,7 @@ export function registerDeleteFileTool(server, options = {}) {
74
78
  return buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path);
75
79
  },
76
80
  });
77
- const validatedHandler = withValidatedArgs(DeleteFileInputSchema, handler);
78
- const wrappedHandler = wrapToolHandler(validatedHandler, {
81
+ const wrappedHandler = wrapToolHandler(handler, {
79
82
  guard: options.isInitialized,
80
83
  progressMessage: (args) => `🛠 rm: ${path.basename(args.path)}`,
81
84
  completionMessage: (args, result) => {
@@ -85,7 +88,8 @@ export function registerDeleteFileTool(server, options = {}) {
85
88
  return `🛠 rm: ${name} • deleted`;
86
89
  },
87
90
  });
88
- if (registerToolTaskIfAvailable(server, 'rm', DELETE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
91
+ const validatedHandler = withValidatedArgs(DeleteFileInputSchema, wrappedHandler);
92
+ if (registerToolTaskIfAvailable(server, 'rm', DELETE_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized))
89
93
  return;
90
- server.registerTool('rm', withDefaultIcons({ ...DELETE_FILE_TOOL }, options.iconInfo), wrappedHandler);
94
+ server.registerTool('rm', withDefaultIcons({ ...DELETE_FILE_TOOL }, options.iconInfo), validatedHandler);
91
95
  }
@@ -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 DIFF_FILES_TOOL: ToolContract;
3
4
  export declare function registerDiffFilesTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -7,7 +7,8 @@ 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
9
  import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
10
- const DIFF_FILES_TOOL = {
10
+ export const DIFF_FILES_TOOL = {
11
+ name: 'diff_files',
11
12
  title: 'Diff Files',
12
13
  description: 'Generate a unified diff between two files. ' +
13
14
  'Output feeds directly into `apply_patch`. ' +
@@ -15,6 +16,7 @@ const DIFF_FILES_TOOL = {
15
16
  inputSchema: DiffFilesInputSchema,
16
17
  outputSchema: DiffFilesOutputSchema,
17
18
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
19
+ gotchas: ['`isIdentical=true` means no hunks (`@@`) and empty diff.'],
18
20
  };
19
21
  function assertDiffFileSizeWithinLimit(filePath, size, maxFileSize) {
20
22
  if (size <= maxFileSize)
@@ -80,8 +82,7 @@ export function registerDiffFilesTool(server, options = {}) {
80
82
  run: (signal) => handleDiffFiles(args, signal, options.resourceStore),
81
83
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.original),
82
84
  });
83
- const validatedHandler = withValidatedArgs(DiffFilesInputSchema, handler);
84
- server.registerTool('diff_files', withDefaultIcons({ ...DIFF_FILES_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
85
+ const wrappedHandler = wrapToolHandler(handler, {
85
86
  guard: options.isInitialized,
86
87
  progressMessage: (args) => {
87
88
  const name1 = path.basename(args.original);
@@ -101,5 +102,7 @@ export function registerDiffFilesTool(server, options = {}) {
101
102
  const hunks = (sc.diff?.match(/@@/g) ?? []).length;
102
103
  return `🕮 diff_files: ${n1} ⟷ ${n2} • ${hunks} hunk${hunks !== 1 ? 's' : ''}`;
103
104
  },
104
- }));
105
+ });
106
+ const validatedHandler = withValidatedArgs(DiffFilesInputSchema, wrappedHandler);
107
+ server.registerTool('diff_files', withDefaultIcons({ ...DIFF_FILES_TOOL }, options.iconInfo), validatedHandler);
105
108
  }
@@ -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 EDIT_FILE_TOOL: ToolContract;
3
4
  export declare function registerEditFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
@@ -5,7 +5,8 @@ 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
7
  import { buildToolErrorResponse, buildToolResponse, DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, executeToolWithDiagnostics, withDefaultIcons, withValidatedArgs, wrapToolHandler, } from './shared.js';
8
- const EDIT_FILE_TOOL = {
8
+ export const EDIT_FILE_TOOL = {
9
+ name: 'edit',
9
10
  title: 'Edit File',
10
11
  description: 'Edit a file by replacing text. Sequentially applies a list of string replacements. ' +
11
12
  'Replaces the first occurrence of each `oldText`. ' +
@@ -14,6 +15,12 @@ const EDIT_FILE_TOOL = {
14
15
  inputSchema: EditFileInputSchema,
15
16
  outputSchema: EditFileOutputSchema,
16
17
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
18
+ nuances: [
19
+ 'Apply sequential literal replacements (first occurrence per edit).',
20
+ ],
21
+ gotchas: [
22
+ '`oldText` must match exactly; unmatched items are reported in `unmatchedEdits`.',
23
+ ],
17
24
  };
18
25
  function applyEdits(content, edits) {
19
26
  let newContent = content;
@@ -65,9 +72,14 @@ async function handleEditFile(args, signal) {
65
72
  if (appliedEdits > 0) {
66
73
  await atomicWriteFile(validPath, newContent, { encoding: 'utf-8', signal });
67
74
  }
75
+ const unmatchedNote = unmatchedEdits.length > 0
76
+ ? ` — ${unmatchedEdits.length} unmatched: [${unmatchedEdits
77
+ .map((s) => JSON.stringify(s.length > 40 ? `${s.slice(0, 40)}\u2026` : s))
78
+ .join(', ')}]`
79
+ : '';
68
80
  const message = appliedEdits === 0
69
- ? `No edits applied to ${args.path}`
70
- : `Successfully applied ${appliedEdits} edits to ${args.path}`;
81
+ ? `No edits applied to ${args.path}${unmatchedNote}`
82
+ : `Successfully applied ${appliedEdits} edits to ${args.path}${unmatchedNote}`;
71
83
  return buildToolResponse(message, structured);
72
84
  }
73
85
  export function registerEditFileTool(server, options = {}) {
@@ -79,8 +91,7 @@ export function registerEditFileTool(server, options = {}) {
79
91
  run: (signal) => handleEditFile(args, signal),
80
92
  onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
81
93
  });
82
- const validatedHandler = withValidatedArgs(EditFileInputSchema, handler);
83
- server.registerTool('edit', withDefaultIcons({ ...EDIT_FILE_TOOL }, options.iconInfo), wrapToolHandler(validatedHandler, {
94
+ const wrappedHandler = wrapToolHandler(handler, {
84
95
  guard: options.isInitialized,
85
96
  progressMessage: (args) => {
86
97
  const name = path.basename(args.path);
@@ -98,5 +109,7 @@ export function registerEditFileTool(server, options = {}) {
98
109
  }
99
110
  return `🛠 edit: ${name} • [${sc.appliedEdits ?? 0} edits]`;
100
111
  },
101
- }));
112
+ });
113
+ const validatedHandler = withValidatedArgs(EditFileInputSchema, wrappedHandler);
114
+ server.registerTool('edit', withDefaultIcons({ ...EDIT_FILE_TOOL }, options.iconInfo), validatedHandler);
102
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 LIST_DIRECTORY_TOOL: ToolContract;
3
4
  export declare function registerListDirectoryTool(server: McpServer, options?: ToolRegistrationOptions): void;