@j0hanz/filesystem-mcp 1.2.1 → 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.
- package/README.md +11 -0
- package/dist/cli.js +14 -8
- package/dist/completions.js +15 -6
- package/dist/lib/file-operations/glob-engine.js +3 -9
- package/dist/lib/file-operations/read-multiple-files.js +4 -23
- package/dist/lib/file-operations/search-content.js +11 -18
- package/dist/lib/observability.js +4 -4
- package/dist/lib/path-validation.js +22 -9
- package/dist/lib/resource-store.js +1 -1
- package/dist/pkg-info.d.ts +6 -0
- package/dist/pkg-info.js +9 -0
- package/dist/schemas.d.ts +28 -28
- package/dist/schemas.js +26 -26
- package/dist/server/bootstrap.d.ts +4 -0
- package/dist/server/bootstrap.js +117 -0
- package/dist/server/capabilities.d.ts +10 -0
- package/dist/server/capabilities.js +40 -0
- package/dist/server/logging.d.ts +7 -0
- package/dist/server/logging.js +41 -0
- package/dist/server/roots-manager.d.ts +19 -0
- package/dist/server/roots-manager.js +173 -0
- package/dist/server/types.d.ts +4 -0
- package/dist/server/types.js +1 -0
- package/dist/server.d.ts +2 -8
- package/dist/server.js +1 -346
- package/dist/tools/apply-patch.js +17 -3
- package/dist/tools/calculate-hash.js +48 -13
- package/dist/tools/create-directory.js +13 -3
- package/dist/tools/delete-file.js +9 -2
- package/dist/tools/diff-files.js +16 -2
- package/dist/tools/edit-file.js +8 -7
- package/dist/tools/list-directory.js +13 -2
- package/dist/tools/move-file.js +11 -3
- package/dist/tools/read-multiple.js +21 -3
- package/dist/tools/read.js +16 -2
- package/dist/tools/replace-in-files.js +44 -11
- package/dist/tools/roots.js +12 -2
- package/dist/tools/search-content.js +62 -29
- package/dist/tools/search-files.js +60 -13
- package/dist/tools/shared.d.ts +5 -1
- package/dist/tools/shared.js +55 -8
- package/dist/tools/stat-many.js +22 -3
- package/dist/tools/stat.js +12 -2
- package/dist/tools/task-support.js +60 -13
- package/dist/tools/tree.js +15 -2
- package/dist/tools/write-file.js +13 -3
- package/package.json +4 -2
package/dist/server.js
CHANGED
|
@@ -1,346 +1 @@
|
|
|
1
|
-
|
|
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
|
-
{
|
|
304
|
-
const stripStructured = process.env['FS_CONTEXT_STRIP_STRUCTURED'] !== '0';
|
|
305
|
-
if (stripStructured) {
|
|
306
|
-
const typedServer = server;
|
|
307
|
-
const origReg = typedServer.registerTool.bind(server);
|
|
308
|
-
typedServer.registerTool = (...regArgs) => {
|
|
309
|
-
// Strip outputSchema so SDK won't require structuredContent
|
|
310
|
-
if (regArgs.length >= 2 &&
|
|
311
|
-
regArgs[1] &&
|
|
312
|
-
typeof regArgs[1] === 'object') {
|
|
313
|
-
const config = { ...regArgs[1] };
|
|
314
|
-
delete config['outputSchema'];
|
|
315
|
-
regArgs[1] = config;
|
|
316
|
-
}
|
|
317
|
-
const handlerIdx = regArgs.length - 1;
|
|
318
|
-
const origHandler = regArgs[handlerIdx];
|
|
319
|
-
if (typeof origHandler !== 'function')
|
|
320
|
-
return origReg(...regArgs);
|
|
321
|
-
regArgs[handlerIdx] = async (...hArgs) => {
|
|
322
|
-
const r = await origHandler(...hArgs);
|
|
323
|
-
if (!r || typeof r !== 'object')
|
|
324
|
-
return r;
|
|
325
|
-
const record = r;
|
|
326
|
-
return Object.fromEntries(Object.entries(record).filter(([key]) => key !== 'structuredContent'));
|
|
327
|
-
};
|
|
328
|
-
return origReg(...regArgs);
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
registerAllTools(server, {
|
|
333
|
-
resourceStore,
|
|
334
|
-
isInitialized: () => rootsManager.isInitialized(),
|
|
335
|
-
...(localIcon ? { iconInfo: localIcon } : {}),
|
|
336
|
-
});
|
|
337
|
-
return server;
|
|
338
|
-
}
|
|
339
|
-
export async function startServer(server) {
|
|
340
|
-
const transport = new StdioServerTransport();
|
|
341
|
-
const rootsManager = getRootsManager(server);
|
|
342
|
-
rootsManager.registerHandlers(server);
|
|
343
|
-
await rootsManager.recomputeAllowedDirectories();
|
|
344
|
-
await server.connect(transport);
|
|
345
|
-
rootsManager.logMissingDirectoriesIfNeeded(server);
|
|
346
|
-
}
|
|
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,11 +69,25 @@ 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
|
|
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);
|
|
76
|
-
return
|
|
77
|
+
return args.dryRun
|
|
78
|
+
? `🛠 apply_patch: ${name} [dry run]`
|
|
79
|
+
: `🛠 apply_patch: ${name}`;
|
|
80
|
+
},
|
|
81
|
+
completionMessage: (args, result) => {
|
|
82
|
+
const name = path.basename(args.path);
|
|
83
|
+
if (result.isError)
|
|
84
|
+
return `🛠 apply_patch: ${name} • failed`;
|
|
85
|
+
const sc = result.structuredContent;
|
|
86
|
+
if (!sc.ok)
|
|
87
|
+
return `🛠 apply_patch: ${name} • failed`;
|
|
88
|
+
if (args.dryRun)
|
|
89
|
+
return `🛠 apply_patch: ${name} • dry run OK`;
|
|
90
|
+
return `🛠 apply_patch: ${name} • applied`;
|
|
77
91
|
},
|
|
78
92
|
});
|
|
79
93
|
if (registerToolTaskIfAvailable(server, 'apply_patch', APPLY_PATCH_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
@@ -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 = {
|
|
@@ -153,24 +153,59 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
153
153
|
timedSignal: {},
|
|
154
154
|
context: { path: args.path },
|
|
155
155
|
run: async (signal) => {
|
|
156
|
+
const baseName = path.basename(args.path);
|
|
157
|
+
let progressCursor = 0;
|
|
156
158
|
notifyProgress(extra, {
|
|
157
159
|
current: 0,
|
|
158
|
-
message: `🕮 calculate_hash: ${
|
|
160
|
+
message: `🕮 calculate_hash: ${baseName}`,
|
|
159
161
|
});
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
162
|
+
const baseReporter = createProgressReporter(extra);
|
|
163
|
+
const progressWithMessage = ({ current, total, }) => {
|
|
164
|
+
if (current > progressCursor)
|
|
165
|
+
progressCursor = current;
|
|
166
|
+
const fileWord = current === 1 ? 'file' : 'files';
|
|
167
|
+
baseReporter({
|
|
168
|
+
current,
|
|
169
|
+
...(total !== undefined ? { total } : {}),
|
|
170
|
+
message: `🕮 calculate_hash: ${baseName} — ${current} ${fileWord} hashed`,
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
try {
|
|
174
|
+
const result = await handleCalculateHash(args, signal, progressWithMessage);
|
|
175
|
+
const sc = result.structuredContent;
|
|
176
|
+
const totalFiles = sc.ok ? (sc.fileCount ?? 1) : 1;
|
|
177
|
+
const finalCurrent = Math.max(totalFiles + 1, progressCursor + 1);
|
|
178
|
+
let suffix;
|
|
179
|
+
if (!sc.ok) {
|
|
180
|
+
suffix = 'failed';
|
|
181
|
+
}
|
|
182
|
+
else if (sc.fileCount !== undefined && sc.fileCount > 1) {
|
|
183
|
+
suffix = `${sc.fileCount} files • ${(sc.hash ?? '').slice(0, 8)}...`;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
suffix = `${(sc.hash ?? '').slice(0, 8)}...`;
|
|
187
|
+
}
|
|
188
|
+
notifyProgress(extra, {
|
|
189
|
+
current: finalCurrent,
|
|
190
|
+
total: finalCurrent,
|
|
191
|
+
message: `🕮 calculate_hash: ${baseName} • ${suffix}`,
|
|
192
|
+
});
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
197
|
+
notifyProgress(extra, {
|
|
198
|
+
current: finalCurrent,
|
|
199
|
+
total: finalCurrent,
|
|
200
|
+
message: `🕮 calculate_hash: ${baseName} • failed`,
|
|
201
|
+
});
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
170
204
|
},
|
|
171
205
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
|
|
172
206
|
});
|
|
173
|
-
const
|
|
207
|
+
const validatedHandler = withValidatedArgs(CalculateHashInputSchema, handler);
|
|
208
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
174
209
|
guard: options.isInitialized,
|
|
175
210
|
});
|
|
176
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,9 +30,19 @@ 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
|
|
33
|
+
const validatedHandler = withValidatedArgs(CreateDirectoryInputSchema, handler);
|
|
34
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
34
35
|
guard: options.isInitialized,
|
|
35
|
-
progressMessage: (args) =>
|
|
36
|
+
progressMessage: (args) => {
|
|
37
|
+
const name = path.basename(args.path) || args.path;
|
|
38
|
+
return `🛠 mkdir: ${name}`;
|
|
39
|
+
},
|
|
40
|
+
completionMessage: (args, result) => {
|
|
41
|
+
const name = path.basename(args.path) || args.path;
|
|
42
|
+
if (result.isError)
|
|
43
|
+
return `🛠 mkdir: ${name} • failed`;
|
|
44
|
+
return `🛠 mkdir: ${name} • created`;
|
|
45
|
+
},
|
|
36
46
|
});
|
|
37
47
|
if (registerToolTaskIfAvailable(server, 'mkdir', CREATE_DIRECTORY_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
38
48
|
return;
|
|
@@ -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
|
|
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;
|
package/dist/tools/diff-files.js
CHANGED
|
@@ -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,12 +80,26 @@ 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
|
-
|
|
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);
|
|
87
88
|
const name2 = path.basename(args.modified);
|
|
88
89
|
return `🕮 diff_files: ${name1} ⟷ ${name2}`;
|
|
89
90
|
},
|
|
91
|
+
completionMessage: (args, result) => {
|
|
92
|
+
const n1 = path.basename(args.original);
|
|
93
|
+
const n2 = path.basename(args.modified);
|
|
94
|
+
if (result.isError)
|
|
95
|
+
return `🕮 diff_files: ${n1} ⟷ ${n2} • failed`;
|
|
96
|
+
const sc = result.structuredContent;
|
|
97
|
+
if (!sc.ok)
|
|
98
|
+
return `🕮 diff_files: ${n1} ⟷ ${n2} • failed`;
|
|
99
|
+
if (sc.isIdentical)
|
|
100
|
+
return `🕮 diff_files: ${n1} ⟷ ${n2} • identical`;
|
|
101
|
+
const hunks = (sc.diff?.match(/@@/g) ?? []).length;
|
|
102
|
+
return `🕮 diff_files: ${n1} ⟷ ${n2} • ${hunks} hunk${hunks !== 1 ? 's' : ''}`;
|
|
103
|
+
},
|
|
90
104
|
}));
|
|
91
105
|
}
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -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,23 +79,24 @@ 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
|
-
|
|
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);
|
|
86
|
-
return `🛠 edit: ${name}
|
|
87
|
+
return `🛠 edit: ${name} [${args.edits.length} edits]`;
|
|
87
88
|
},
|
|
88
89
|
completionMessage: (args, result) => {
|
|
89
90
|
const name = path.basename(args.path);
|
|
90
91
|
if (result.isError)
|
|
91
|
-
return `🛠 edit: ${name}
|
|
92
|
+
return `🛠 edit: ${name} • failed`;
|
|
92
93
|
const sc = result.structuredContent;
|
|
93
94
|
if (!sc.ok)
|
|
94
|
-
return `🛠 edit: ${name}
|
|
95
|
+
return `🛠 edit: ${name} • failed`;
|
|
95
96
|
if (sc.lineRange) {
|
|
96
|
-
return `🛠 edit: ${name}
|
|
97
|
+
return `🛠 edit: ${name} • [${sc.lineRange[0]}-${sc.lineRange[1]}]`;
|
|
97
98
|
}
|
|
98
|
-
return `🛠 edit: ${name}
|
|
99
|
+
return `🛠 edit: ${name} • [${sc.appliedEdits ?? 0} edits]`;
|
|
99
100
|
},
|
|
100
101
|
}));
|
|
101
102
|
}
|
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/tools/move-file.js
CHANGED
|
@@ -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
|
|
49
|
+
const validatedHandler = withValidatedArgs(MoveFileInputSchema, handler);
|
|
50
|
+
const wrappedHandler = wrapToolHandler(validatedHandler, {
|
|
50
51
|
guard: options.isInitialized,
|
|
51
|
-
progressMessage: (args) => `🛠 mv: ${path.basename(args.source)}
|
|
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;
|