@j0hanz/filesystem-mcp 1.2.1 → 1.2.2
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/dist/cli.js +12 -4
- package/dist/lib/resource-store.js +1 -1
- package/dist/server.js +0 -29
- package/dist/tools/apply-patch.js +14 -1
- package/dist/tools/calculate-hash.js +45 -11
- package/dist/tools/create-directory.js +10 -1
- package/dist/tools/diff-files.js +13 -0
- package/dist/tools/edit-file.js +5 -5
- package/dist/tools/move-file.js +1 -1
- package/dist/tools/read-multiple.js +18 -1
- package/dist/tools/read.js +13 -0
- package/dist/tools/replace-in-files.js +41 -9
- package/dist/tools/search-content.js +56 -18
- package/dist/tools/search-files.js +53 -10
- package/dist/tools/shared.d.ts +2 -0
- package/dist/tools/shared.js +36 -7
- package/dist/tools/stat-many.js +19 -1
- package/dist/tools/stat.js +9 -0
- package/dist/tools/task-support.js +3 -3
- package/dist/tools/write-file.js +10 -1
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -3,11 +3,13 @@ import { getSystemErrorMessage, getSystemErrorName } from 'node:util';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { Command, CommanderError, InvalidArgumentError } from 'commander';
|
|
5
5
|
import packageJsonRaw from '../package.json' with { type: 'json' };
|
|
6
|
+
import { processInParallel } from './lib/fs-helpers.js';
|
|
6
7
|
import { getReservedDeviceNameForPath, isWindowsDriveRelativePath, normalizePath, } from './lib/path-validation.js';
|
|
7
8
|
import { isRecord } from './lib/type-guards.js';
|
|
8
9
|
const PackageJsonSchema = z.object({ version: z.string() });
|
|
9
10
|
const { version: SERVER_VERSION } = PackageJsonSchema.parse(packageJsonRaw);
|
|
10
11
|
const IS_WINDOWS = process.platform === 'win32';
|
|
12
|
+
const CLI_VALIDATE_CONCURRENCY = 8;
|
|
11
13
|
export class CliExitError extends Error {
|
|
12
14
|
exitCode;
|
|
13
15
|
constructor(message, exitCode) {
|
|
@@ -92,11 +94,17 @@ async function validateDirectoryPath(inputPath) {
|
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
async function normalizeCliDirectories(args) {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
const { results, errors } = await processInParallel([...args], validateDirectoryPath, CLI_VALIDATE_CONCURRENCY);
|
|
98
|
+
if (errors.length === 0) {
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
let first = errors[0];
|
|
102
|
+
for (const failure of errors) {
|
|
103
|
+
if (first && failure.index < first.index) {
|
|
104
|
+
first = failure;
|
|
105
|
+
}
|
|
98
106
|
}
|
|
99
|
-
|
|
107
|
+
throw first?.error ?? new Error('Failed to validate directories');
|
|
100
108
|
}
|
|
101
109
|
function parseAllowedDirArgument(value, previous) {
|
|
102
110
|
validateCliPath(value);
|
|
@@ -85,7 +85,7 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
85
85
|
function getText(uri) {
|
|
86
86
|
const existing = byUri.get(uri);
|
|
87
87
|
if (!existing) {
|
|
88
|
-
throw new McpError(ErrorCode.E_NOT_FOUND, `Resource not found: ${uri}
|
|
88
|
+
throw new McpError(ErrorCode.E_NOT_FOUND, `Resource not found: ${uri}. The cached result may have been evicted. Re-run the originating tool to regenerate.`);
|
|
89
89
|
}
|
|
90
90
|
return existing;
|
|
91
91
|
}
|
package/dist/server.js
CHANGED
|
@@ -300,35 +300,6 @@ export async function createServer(options = {}) {
|
|
|
300
300
|
registerGetHelpPrompt(server, serverInstructions, localIcon);
|
|
301
301
|
registerResultResources(server, resourceStore, localIcon);
|
|
302
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
303
|
registerAllTools(server, {
|
|
333
304
|
resourceStore,
|
|
334
305
|
isInitialized: () => rootsManager.isInitialized(),
|
|
@@ -73,7 +73,20 @@ export function registerApplyPatchTool(server, options = {}) {
|
|
|
73
73
|
guard: options.isInitialized,
|
|
74
74
|
progressMessage: (args) => {
|
|
75
75
|
const name = path.basename(args.path);
|
|
76
|
-
return
|
|
76
|
+
return args.dryRun
|
|
77
|
+
? `🛠 apply_patch: ${name} [dry run]`
|
|
78
|
+
: `🛠 apply_patch: ${name}`;
|
|
79
|
+
},
|
|
80
|
+
completionMessage: (args, result) => {
|
|
81
|
+
const name = path.basename(args.path);
|
|
82
|
+
if (result.isError)
|
|
83
|
+
return `🛠 apply_patch: ${name} • failed`;
|
|
84
|
+
const sc = result.structuredContent;
|
|
85
|
+
if (!sc.ok)
|
|
86
|
+
return `🛠 apply_patch: ${name} • failed`;
|
|
87
|
+
if (args.dryRun)
|
|
88
|
+
return `🛠 apply_patch: ${name} • dry run OK`;
|
|
89
|
+
return `🛠 apply_patch: ${name} • applied`;
|
|
77
90
|
},
|
|
78
91
|
});
|
|
79
92
|
if (registerToolTaskIfAvailable(server, 'apply_patch', APPLY_PATCH_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
@@ -153,20 +153,54 @@ 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
|
});
|
|
@@ -32,7 +32,16 @@ export function registerCreateDirectoryTool(server, options = {}) {
|
|
|
32
32
|
});
|
|
33
33
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
34
34
|
guard: options.isInitialized,
|
|
35
|
-
progressMessage: (args) =>
|
|
35
|
+
progressMessage: (args) => {
|
|
36
|
+
const name = path.basename(args.path) || args.path;
|
|
37
|
+
return `🛠 mkdir: ${name}`;
|
|
38
|
+
},
|
|
39
|
+
completionMessage: (args, result) => {
|
|
40
|
+
const name = path.basename(args.path) || args.path;
|
|
41
|
+
if (result.isError)
|
|
42
|
+
return `🛠 mkdir: ${name} • failed`;
|
|
43
|
+
return `🛠 mkdir: ${name} • created`;
|
|
44
|
+
},
|
|
36
45
|
});
|
|
37
46
|
if (registerToolTaskIfAvailable(server, 'mkdir', CREATE_DIRECTORY_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
38
47
|
return;
|
package/dist/tools/diff-files.js
CHANGED
|
@@ -87,5 +87,18 @@ export function registerDiffFilesTool(server, options = {}) {
|
|
|
87
87
|
const name2 = path.basename(args.modified);
|
|
88
88
|
return `🕮 diff_files: ${name1} ⟷ ${name2}`;
|
|
89
89
|
},
|
|
90
|
+
completionMessage: (args, result) => {
|
|
91
|
+
const n1 = path.basename(args.original);
|
|
92
|
+
const n2 = path.basename(args.modified);
|
|
93
|
+
if (result.isError)
|
|
94
|
+
return `🕮 diff_files: ${n1} ⟷ ${n2} • failed`;
|
|
95
|
+
const sc = result.structuredContent;
|
|
96
|
+
if (!sc.ok)
|
|
97
|
+
return `🕮 diff_files: ${n1} ⟷ ${n2} • failed`;
|
|
98
|
+
if (sc.isIdentical)
|
|
99
|
+
return `🕮 diff_files: ${n1} ⟷ ${n2} • identical`;
|
|
100
|
+
const hunks = (sc.diff?.match(/@@/g) ?? []).length;
|
|
101
|
+
return `🕮 diff_files: ${n1} ⟷ ${n2} • ${hunks} hunk${hunks !== 1 ? 's' : ''}`;
|
|
102
|
+
},
|
|
90
103
|
}));
|
|
91
104
|
}
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -83,19 +83,19 @@ export function registerEditFileTool(server, options = {}) {
|
|
|
83
83
|
guard: options.isInitialized,
|
|
84
84
|
progressMessage: (args) => {
|
|
85
85
|
const name = path.basename(args.path);
|
|
86
|
-
return `🛠 edit: ${name}
|
|
86
|
+
return `🛠 edit: ${name} [${args.edits.length} edits]`;
|
|
87
87
|
},
|
|
88
88
|
completionMessage: (args, result) => {
|
|
89
89
|
const name = path.basename(args.path);
|
|
90
90
|
if (result.isError)
|
|
91
|
-
return `🛠 edit: ${name}
|
|
91
|
+
return `🛠 edit: ${name} • Failed`;
|
|
92
92
|
const sc = result.structuredContent;
|
|
93
93
|
if (!sc.ok)
|
|
94
|
-
return `🛠 edit: ${name}
|
|
94
|
+
return `🛠 edit: ${name} • Failed`;
|
|
95
95
|
if (sc.lineRange) {
|
|
96
|
-
return `🛠 edit: ${name}
|
|
96
|
+
return `🛠 edit: ${name} • [${sc.lineRange[0]}-${sc.lineRange[1]}]`;
|
|
97
97
|
}
|
|
98
|
-
return `🛠 edit: ${name}
|
|
98
|
+
return `🛠 edit: ${name} • [${sc.appliedEdits ?? 0} edits]`;
|
|
99
99
|
},
|
|
100
100
|
}));
|
|
101
101
|
}
|
package/dist/tools/move-file.js
CHANGED
|
@@ -48,7 +48,7 @@ export function registerMoveFileTool(server, options = {}) {
|
|
|
48
48
|
});
|
|
49
49
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
50
50
|
guard: options.isInitialized,
|
|
51
|
-
progressMessage: (args) => `🛠 mv: ${path.basename(args.source)}
|
|
51
|
+
progressMessage: (args) => `🛠 mv: ${path.basename(args.source)} • ${path.basename(args.destination)}`,
|
|
52
52
|
});
|
|
53
53
|
if (registerToolTaskIfAvailable(server, 'mv', MOVE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
54
54
|
return;
|
|
@@ -123,7 +123,24 @@ export function registerReadMultipleFilesTool(server, options = {}) {
|
|
|
123
123
|
};
|
|
124
124
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
125
125
|
guard: options.isInitialized,
|
|
126
|
-
progressMessage: (args) =>
|
|
126
|
+
progressMessage: (args) => {
|
|
127
|
+
const first = path.basename(args.paths[0] ?? '');
|
|
128
|
+
const extra = args.paths.length > 1 ? `, ${path.basename(args.paths[1] ?? '')}…` : '';
|
|
129
|
+
return `🕮 read_many: ${args.paths.length} files [${first}${extra}]`;
|
|
130
|
+
},
|
|
131
|
+
completionMessage: (_args, result) => {
|
|
132
|
+
if (result.isError)
|
|
133
|
+
return `🕮 read_many • failed`;
|
|
134
|
+
const sc = result.structuredContent;
|
|
135
|
+
if (!sc.ok)
|
|
136
|
+
return `🕮 read_many • failed`;
|
|
137
|
+
const total = sc.summary?.total ?? 0;
|
|
138
|
+
const succeeded = sc.summary?.succeeded ?? 0;
|
|
139
|
+
const failed = sc.summary?.failed ?? 0;
|
|
140
|
+
if (failed)
|
|
141
|
+
return `🕮 read_many: ${succeeded}/${total} read, ${failed} failed`;
|
|
142
|
+
return `🕮 read_many: ${total} files read`;
|
|
143
|
+
},
|
|
127
144
|
});
|
|
128
145
|
if (registerToolTaskIfAvailable(server, 'read_many', READ_MULTIPLE_FILES_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
129
146
|
return;
|
package/dist/tools/read.js
CHANGED
|
@@ -90,6 +90,19 @@ export function registerReadFileTool(server, options = {}) {
|
|
|
90
90
|
}
|
|
91
91
|
return `🕮 read: ${name}`;
|
|
92
92
|
},
|
|
93
|
+
completionMessage: (args, result) => {
|
|
94
|
+
const name = path.basename(args.path);
|
|
95
|
+
if (result.isError)
|
|
96
|
+
return `🕮 read: ${name} • failed`;
|
|
97
|
+
const sc = result.structuredContent;
|
|
98
|
+
if (!sc.ok)
|
|
99
|
+
return `🕮 read: ${name} • failed`;
|
|
100
|
+
if (sc.hasMoreLines)
|
|
101
|
+
return `🕮 read: ${name} • truncated [${sc.totalLines ?? '?'} lines]`;
|
|
102
|
+
if (sc.startLine !== undefined)
|
|
103
|
+
return `🕮 read: ${name} • lines ${sc.startLine}–${sc.endLine ?? '?'}`;
|
|
104
|
+
return `🕮 read: ${name} • ${sc.totalLines ?? '?'} lines`;
|
|
105
|
+
},
|
|
93
106
|
});
|
|
94
107
|
if (registerToolTaskIfAvailable(server, 'read', READ_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
95
108
|
return;
|
|
@@ -231,18 +231,50 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
231
231
|
timedSignal: {},
|
|
232
232
|
...(args.path ? { context: { path: args.path } } : {}),
|
|
233
233
|
run: async (signal) => {
|
|
234
|
+
const dryLabel = args.dryRun ? ' [dry run]' : '';
|
|
235
|
+
const context = `"${args.searchPattern}" in ${args.filePattern}${dryLabel}`;
|
|
236
|
+
let progressCursor = 0;
|
|
234
237
|
notifyProgress(extra, {
|
|
235
238
|
current: 0,
|
|
236
|
-
message: `🛠 search_and_replace: ${
|
|
239
|
+
message: `🛠 search_and_replace: ${context}`,
|
|
237
240
|
});
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
241
|
+
const baseReporter = createProgressReporter(extra);
|
|
242
|
+
const progressWithMessage = ({ current, total, }) => {
|
|
243
|
+
if (current > progressCursor)
|
|
244
|
+
progressCursor = current;
|
|
245
|
+
baseReporter({
|
|
246
|
+
current,
|
|
247
|
+
...(total !== undefined ? { total } : {}),
|
|
248
|
+
message: `🛠 search_and_replace: "${args.searchPattern}" — ${current} files processed`,
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
try {
|
|
252
|
+
const result = await handleSearchAndReplace(args, signal, progressWithMessage);
|
|
253
|
+
const sc = result.structuredContent;
|
|
254
|
+
const finalCurrent = Math.max((sc.processedFiles ?? 0) + 1, progressCursor + 1);
|
|
255
|
+
const matchWord = (sc.matches ?? 0) === 1 ? 'match' : 'matches';
|
|
256
|
+
const fileWord = (sc.filesChanged ?? 0) === 1 ? 'file' : 'files';
|
|
257
|
+
let endSuffix = `${sc.matches ?? 0} ${matchWord} in ${sc.filesChanged ?? 0} ${fileWord}`;
|
|
258
|
+
if (sc.failedFiles)
|
|
259
|
+
endSuffix += `, ${sc.failedFiles} failed`;
|
|
260
|
+
if (sc.dryRun)
|
|
261
|
+
endSuffix += ' [dry run]';
|
|
262
|
+
notifyProgress(extra, {
|
|
263
|
+
current: finalCurrent,
|
|
264
|
+
total: finalCurrent,
|
|
265
|
+
message: `🛠 search_and_replace: ${context} • ${endSuffix}`,
|
|
266
|
+
});
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
271
|
+
notifyProgress(extra, {
|
|
272
|
+
current: finalCurrent,
|
|
273
|
+
total: finalCurrent,
|
|
274
|
+
message: `🛠 search_and_replace: ${context} • failed`,
|
|
275
|
+
});
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
246
278
|
},
|
|
247
279
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
|
|
248
280
|
});
|
|
@@ -202,29 +202,67 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
202
202
|
context: { path: args.path ?? '.' },
|
|
203
203
|
run: async (signal) => {
|
|
204
204
|
const normalizedArgs = SearchContentInputSchema.parse(args);
|
|
205
|
+
const scope = normalizedArgs.filePattern;
|
|
206
|
+
const { pattern } = normalizedArgs;
|
|
207
|
+
let progressCursor = 0;
|
|
205
208
|
notifyProgress(extra, {
|
|
206
209
|
current: 0,
|
|
207
|
-
message: `🔎︎ grep: ${
|
|
210
|
+
message: `🔎︎ grep: ${pattern} in ${scope}`,
|
|
208
211
|
});
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
const baseReporter = createProgressReporter(extra);
|
|
213
|
+
const progressWithMessage = ({ current, total, }) => {
|
|
214
|
+
if (current > progressCursor)
|
|
215
|
+
progressCursor = current;
|
|
216
|
+
const fileWord = current === 1 ? 'file' : 'files';
|
|
217
|
+
baseReporter({
|
|
218
|
+
current,
|
|
219
|
+
...(total !== undefined ? { total } : {}),
|
|
220
|
+
message: `🔎︎ grep: ${pattern} • ${current} ${fileWord} scanned`,
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
try {
|
|
224
|
+
const result = await handleSearchContent(normalizedArgs, signal, options.resourceStore, progressWithMessage);
|
|
225
|
+
const sc = result.structuredContent;
|
|
226
|
+
const count = sc.ok && sc.totalMatches ? sc.totalMatches : 0;
|
|
227
|
+
const filesMatched = sc.ok ? (sc.filesMatched ?? 0) : 0;
|
|
228
|
+
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
229
|
+
let suffix;
|
|
230
|
+
if (count === 0) {
|
|
231
|
+
suffix = `No matches in ${scope}`;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const matchWord = count === 1 ? 'match' : 'matches';
|
|
235
|
+
const fileInfo = filesMatched > 0
|
|
236
|
+
? ` in ${filesMatched} ${filesMatched === 1 ? 'file' : 'files'}`
|
|
237
|
+
: '';
|
|
238
|
+
suffix = `${count} ${matchWord}${fileInfo}`;
|
|
239
|
+
if (stoppedReason === 'timeout') {
|
|
240
|
+
suffix += ' [stopped — timeout]';
|
|
241
|
+
}
|
|
242
|
+
else if (stoppedReason === 'maxResults') {
|
|
243
|
+
suffix += ' [truncated — max results]';
|
|
244
|
+
}
|
|
245
|
+
else if (stoppedReason === 'maxFiles') {
|
|
246
|
+
suffix += ' [truncated — max files]';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const finalCurrent = Math.max((sc.filesScanned ?? 0) + 1, progressCursor + 1);
|
|
250
|
+
notifyProgress(extra, {
|
|
251
|
+
current: finalCurrent,
|
|
252
|
+
total: finalCurrent,
|
|
253
|
+
message: `🔎︎ grep: ${pattern} • ${suffix}`,
|
|
254
|
+
});
|
|
255
|
+
return result;
|
|
215
256
|
}
|
|
216
|
-
|
|
217
|
-
|
|
257
|
+
catch (error) {
|
|
258
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
259
|
+
notifyProgress(extra, {
|
|
260
|
+
current: finalCurrent,
|
|
261
|
+
total: finalCurrent,
|
|
262
|
+
message: `🔎︎ grep: ${pattern} in ${scope} • failed`,
|
|
263
|
+
});
|
|
264
|
+
throw error;
|
|
218
265
|
}
|
|
219
|
-
else {
|
|
220
|
-
suffix = `${count} matches`;
|
|
221
|
-
}
|
|
222
|
-
const finalCurrent = (sc.filesScanned ?? 0) + 1;
|
|
223
|
-
notifyProgress(extra, {
|
|
224
|
-
current: finalCurrent,
|
|
225
|
-
message: `🔎︎ grep: ${normalizedArgs.pattern} ➟ ${suffix}`,
|
|
226
|
-
});
|
|
227
|
-
return result;
|
|
228
266
|
},
|
|
229
267
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path ?? '.'),
|
|
230
268
|
});
|
|
@@ -89,19 +89,62 @@ export function registerSearchFilesTool(server, options = {}) {
|
|
|
89
89
|
timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
|
|
90
90
|
context: { path: args.path ?? '.' },
|
|
91
91
|
run: async (signal) => {
|
|
92
|
+
const scope = args.path ?? '.';
|
|
93
|
+
const { pattern } = args;
|
|
94
|
+
let progressCursor = 0;
|
|
92
95
|
notifyProgress(extra, {
|
|
93
96
|
current: 0,
|
|
94
|
-
message: `🔎︎ find: ${
|
|
97
|
+
message: `🔎︎ find: ${pattern} in ${scope}`,
|
|
95
98
|
});
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
const baseReporter = createProgressReporter(extra);
|
|
100
|
+
const progressWithMessage = ({ current, total, }) => {
|
|
101
|
+
if (current > progressCursor)
|
|
102
|
+
progressCursor = current;
|
|
103
|
+
const fileWord = current === 1 ? 'file' : 'files';
|
|
104
|
+
baseReporter({
|
|
105
|
+
current,
|
|
106
|
+
...(total !== undefined ? { total } : {}),
|
|
107
|
+
message: `🔎︎ find: ${pattern} — ${current} ${fileWord} scanned`,
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
const result = await handleSearchFiles(args, signal, progressWithMessage);
|
|
112
|
+
const sc = result.structuredContent;
|
|
113
|
+
const count = sc.ok ? (sc.totalMatches ?? 0) : 0;
|
|
114
|
+
const stoppedReason = sc.ok ? sc.stoppedReason : undefined;
|
|
115
|
+
let suffix;
|
|
116
|
+
if (count === 0) {
|
|
117
|
+
suffix = `No matches in ${scope}`;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
suffix = `${count} ${count === 1 ? 'match' : 'matches'}`;
|
|
121
|
+
if (stoppedReason === 'timeout') {
|
|
122
|
+
suffix += ' [stopped — timeout]';
|
|
123
|
+
}
|
|
124
|
+
else if (stoppedReason === 'maxResults') {
|
|
125
|
+
suffix += ' [truncated — max results]';
|
|
126
|
+
}
|
|
127
|
+
else if (stoppedReason === 'maxFiles') {
|
|
128
|
+
suffix += ' [truncated — max files]';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const finalCurrent = Math.max((sc.filesScanned ?? 0) + 1, progressCursor + 1);
|
|
132
|
+
notifyProgress(extra, {
|
|
133
|
+
current: finalCurrent,
|
|
134
|
+
total: finalCurrent,
|
|
135
|
+
message: `🔎︎ find: ${pattern} • ${suffix}`,
|
|
136
|
+
});
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const finalCurrent = Math.max(progressCursor + 1, 1);
|
|
141
|
+
notifyProgress(extra, {
|
|
142
|
+
current: finalCurrent,
|
|
143
|
+
total: finalCurrent,
|
|
144
|
+
message: `🔎︎ find: ${pattern} in ${scope} • failed`,
|
|
145
|
+
});
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
105
148
|
},
|
|
106
149
|
onError: (error) => buildToolErrorResponse(error, ErrorCode.E_INVALID_PATTERN, args.path),
|
|
107
150
|
});
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export declare const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS: {
|
|
|
19
19
|
readonly idempotentHint: true;
|
|
20
20
|
readonly openWorldHint: false;
|
|
21
21
|
};
|
|
22
|
+
export declare function shouldStripStructuredOutput(): boolean;
|
|
23
|
+
export declare function maybeStripStructuredContentFromResult<T extends object>(result: T): T;
|
|
22
24
|
type ResourceEntry = ReturnType<ResourceStore['putText']>;
|
|
23
25
|
export declare function maybeExternalizeTextContent(resourceStore: ResourceStore | undefined, content: string, params: {
|
|
24
26
|
name: string;
|
package/dist/tools/shared.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getAllowedDirectories } from '../lib/path-validation.js';
|
|
|
5
5
|
const MAX_INLINE_CONTENT_CHARS = 20_000;
|
|
6
6
|
const MAX_INLINE_PREVIEW_CHARS = 4_000;
|
|
7
7
|
const PROGRESS_RATE_LIMIT_MS = 50;
|
|
8
|
+
const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes']);
|
|
8
9
|
export const READ_ONLY_TOOL_ANNOTATIONS = {
|
|
9
10
|
readOnlyHint: true,
|
|
10
11
|
idempotentHint: true,
|
|
@@ -20,6 +21,30 @@ export const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS = {
|
|
|
20
21
|
idempotentHint: true,
|
|
21
22
|
openWorldHint: false,
|
|
22
23
|
};
|
|
24
|
+
export function shouldStripStructuredOutput() {
|
|
25
|
+
const value = process.env['FS_CONTEXT_STRIP_STRUCTURED'];
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
return false;
|
|
28
|
+
return TRUE_ENV_VALUES.has(value.trim().toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
export function maybeStripStructuredContentFromResult(result) {
|
|
31
|
+
if (!shouldStripStructuredOutput())
|
|
32
|
+
return result;
|
|
33
|
+
if (!Object.hasOwn(result, 'structuredContent'))
|
|
34
|
+
return result;
|
|
35
|
+
const rest = { ...result };
|
|
36
|
+
delete rest['structuredContent'];
|
|
37
|
+
return rest;
|
|
38
|
+
}
|
|
39
|
+
function maybeStripOutputSchema(tool) {
|
|
40
|
+
if (!shouldStripStructuredOutput())
|
|
41
|
+
return tool;
|
|
42
|
+
if (!Object.hasOwn(tool, 'outputSchema'))
|
|
43
|
+
return tool;
|
|
44
|
+
const mutable = { ...tool };
|
|
45
|
+
delete mutable['outputSchema'];
|
|
46
|
+
return mutable;
|
|
47
|
+
}
|
|
23
48
|
function buildTextPreview(text) {
|
|
24
49
|
if (text.length <= MAX_INLINE_PREVIEW_CHARS)
|
|
25
50
|
return text;
|
|
@@ -71,13 +96,14 @@ function canSendProgress(extra) {
|
|
|
71
96
|
extra.sendNotification !== undefined);
|
|
72
97
|
}
|
|
73
98
|
export function withDefaultIcons(tool, iconInfo) {
|
|
74
|
-
if (!iconInfo)
|
|
75
|
-
return tool;
|
|
99
|
+
if (!iconInfo) {
|
|
100
|
+
return maybeStripOutputSchema(tool);
|
|
101
|
+
}
|
|
76
102
|
const existingIcons = tool.icons;
|
|
77
103
|
if (existingIcons && existingIcons.length > 0) {
|
|
78
|
-
return tool;
|
|
104
|
+
return maybeStripOutputSchema(tool);
|
|
79
105
|
}
|
|
80
|
-
|
|
106
|
+
const withIcons = {
|
|
81
107
|
...tool,
|
|
82
108
|
icons: [
|
|
83
109
|
{
|
|
@@ -86,6 +112,7 @@ export function withDefaultIcons(tool, iconInfo) {
|
|
|
86
112
|
},
|
|
87
113
|
],
|
|
88
114
|
};
|
|
115
|
+
return maybeStripOutputSchema(withIcons);
|
|
89
116
|
}
|
|
90
117
|
export function buildFileInfoPayload(info) {
|
|
91
118
|
return {
|
|
@@ -247,7 +274,7 @@ export function wrapToolHandler(handler, options) {
|
|
|
247
274
|
return async (args, extra) => {
|
|
248
275
|
const resolvedExtra = extra ?? {};
|
|
249
276
|
if (options.guard && !options.guard()) {
|
|
250
|
-
return buildNotInitializedResult();
|
|
277
|
+
return maybeStripStructuredContentFromResult(buildNotInitializedResult());
|
|
251
278
|
}
|
|
252
279
|
if (options.progressMessage) {
|
|
253
280
|
const message = options.progressMessage(args);
|
|
@@ -255,9 +282,11 @@ export function wrapToolHandler(handler, options) {
|
|
|
255
282
|
const completionFn = completionMessage
|
|
256
283
|
? (result) => completionMessage(args, result)
|
|
257
284
|
: undefined;
|
|
258
|
-
|
|
285
|
+
const result = await withProgress(message, resolvedExtra, () => handler(args, resolvedExtra), completionFn);
|
|
286
|
+
return maybeStripStructuredContentFromResult(result);
|
|
259
287
|
}
|
|
260
|
-
|
|
288
|
+
const result = await handler(args, resolvedExtra);
|
|
289
|
+
return maybeStripStructuredContentFromResult(result);
|
|
261
290
|
};
|
|
262
291
|
}
|
|
263
292
|
export function resolvePathOrRoot(pathValue) {
|
package/dist/tools/stat-many.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
1
2
|
import { formatBytes, joinLines } from '../config.js';
|
|
2
3
|
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
|
|
3
4
|
import { ErrorCode } from '../lib/errors.js';
|
|
@@ -74,7 +75,24 @@ export function registerGetMultipleFileInfoTool(server, options = {}) {
|
|
|
74
75
|
};
|
|
75
76
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
76
77
|
guard: options.isInitialized,
|
|
77
|
-
progressMessage: (args) =>
|
|
78
|
+
progressMessage: (args) => {
|
|
79
|
+
const first = path.basename(args.paths[0] ?? '');
|
|
80
|
+
const extra = args.paths.length > 1 ? `, ${path.basename(args.paths[1] ?? '')}…` : '';
|
|
81
|
+
return `🕮 stat_many: ${args.paths.length} paths [${first}${extra}]`;
|
|
82
|
+
},
|
|
83
|
+
completionMessage: (_args, result) => {
|
|
84
|
+
if (result.isError)
|
|
85
|
+
return `🕮 stat_many • failed`;
|
|
86
|
+
const sc = result.structuredContent;
|
|
87
|
+
if (!sc.ok)
|
|
88
|
+
return `🕮 stat_many • failed`;
|
|
89
|
+
const total = sc.summary?.total ?? 0;
|
|
90
|
+
const succeeded = sc.summary?.succeeded ?? 0;
|
|
91
|
+
const failed = sc.summary?.failed ?? 0;
|
|
92
|
+
if (failed)
|
|
93
|
+
return `🕮 stat_many: ${succeeded}/${total} OK, ${failed} failed`;
|
|
94
|
+
return `🕮 stat_many: ${total} OK`;
|
|
95
|
+
},
|
|
78
96
|
});
|
|
79
97
|
if (registerToolTaskIfAvailable(server, 'stat_many', GET_MULTIPLE_FILE_INFO_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
80
98
|
return;
|
package/dist/tools/stat.js
CHANGED
|
@@ -48,5 +48,14 @@ export function registerGetFileInfoTool(server, options = {}) {
|
|
|
48
48
|
server.registerTool('stat', withDefaultIcons({ ...GET_FILE_INFO_TOOL }, options.iconInfo), wrapToolHandler(handler, {
|
|
49
49
|
guard: options.isInitialized,
|
|
50
50
|
progressMessage: (args) => `🕮 stat: ${path.basename(args.path)}`,
|
|
51
|
+
completionMessage: (args, result) => {
|
|
52
|
+
const name = path.basename(args.path);
|
|
53
|
+
if (result.isError)
|
|
54
|
+
return `🕮 stat: ${name} • failed`;
|
|
55
|
+
const sc = result.structuredContent;
|
|
56
|
+
if (!sc.ok || !sc.info)
|
|
57
|
+
return `🕮 stat: ${name} • failed`;
|
|
58
|
+
return `🕮 stat: ${sc.info.name} [${sc.info.type}, ${formatBytes(sc.info.size)}]`;
|
|
59
|
+
},
|
|
51
60
|
}));
|
|
52
61
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { ErrorCode, McpError } from '../lib/errors.js';
|
|
3
3
|
import { isRecord } from '../lib/type-guards.js';
|
|
4
|
-
import { buildToolErrorResponse, withDefaultIcons } from './shared.js';
|
|
4
|
+
import { buildToolErrorResponse, maybeStripStructuredContentFromResult, withDefaultIcons, } from './shared.js';
|
|
5
5
|
function isExperimentalTaskRegistration(value) {
|
|
6
6
|
if (!value || typeof value !== 'object')
|
|
7
7
|
return false;
|
|
@@ -191,13 +191,13 @@ async function tryStoreTaskResult(taskStore, taskId, status, result) {
|
|
|
191
191
|
}
|
|
192
192
|
async function runTaskInBackground(run, args, extra, taskStore, taskId) {
|
|
193
193
|
try {
|
|
194
|
-
const result = await run(args, extra);
|
|
194
|
+
const result = maybeStripStructuredContentFromResult(await run(args, extra));
|
|
195
195
|
const status = isErrorResult(result) ? 'failed' : 'completed';
|
|
196
196
|
await tryStoreTaskResult(taskStore, taskId, status, result);
|
|
197
197
|
await notifyTaskStatusIfPossible(extra, taskStore, taskId);
|
|
198
198
|
}
|
|
199
199
|
catch (error) {
|
|
200
|
-
const fallback = buildToolErrorResponse(error, ErrorCode.E_UNKNOWN);
|
|
200
|
+
const fallback = maybeStripStructuredContentFromResult(buildToolErrorResponse(error, ErrorCode.E_UNKNOWN));
|
|
201
201
|
try {
|
|
202
202
|
await tryStoreTaskResult(taskStore, taskId, 'failed', fallback);
|
|
203
203
|
await notifyTaskStatusIfPossible(extra, taskStore, taskId);
|
package/dist/tools/write-file.js
CHANGED
|
@@ -36,7 +36,16 @@ export function registerWriteFileTool(server, options = {}) {
|
|
|
36
36
|
});
|
|
37
37
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
38
38
|
guard: options.isInitialized,
|
|
39
|
-
progressMessage: (args) => `🛠 write: ${path.basename(args.path)}`,
|
|
39
|
+
progressMessage: (args) => `🛠 write: ${path.basename(args.path)} [${args.content.length} chars]`,
|
|
40
|
+
completionMessage: (args, result) => {
|
|
41
|
+
const name = path.basename(args.path);
|
|
42
|
+
if (result.isError)
|
|
43
|
+
return `🛠 write: ${name} • failed`;
|
|
44
|
+
const sc = result.structuredContent;
|
|
45
|
+
if (!sc.ok)
|
|
46
|
+
return `🛠 write: ${name} • failed`;
|
|
47
|
+
return `🛠 write: ${name} • ${sc.bytesWritten ?? 0} bytes`;
|
|
48
|
+
},
|
|
40
49
|
});
|
|
41
50
|
if (registerToolTaskIfAvailable(server, 'write', WRITE_FILE_TOOL, wrappedHandler, options.iconInfo, options.isInitialized))
|
|
42
51
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@j0hanz/filesystem-mcp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"mcpName": "io.github.j0hanz/filesystem-mcp",
|
|
5
5
|
"description": "MCP Server that enables LLMs to interact with the local filesystem.",
|
|
6
6
|
"type": "module",
|
|
@@ -31,12 +31,14 @@
|
|
|
31
31
|
"start": "node dist/index.js",
|
|
32
32
|
"format": "prettier --write .",
|
|
33
33
|
"type-check": "node scripts/tasks.mjs type-check",
|
|
34
|
+
"type-check:src": "node node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
|
|
35
|
+
"type-check:tests": "node node_modules/typescript/bin/tsc -p tsconfig.test.json --noEmit",
|
|
34
36
|
"type-check:diagnostics": "tsc --noEmit --extendedDiagnostics",
|
|
35
37
|
"type-check:trace": "node -e \"require('fs').rmSync('.ts-trace',{recursive:true,force:true})\" && tsc --noEmit --generateTrace .ts-trace",
|
|
36
38
|
"lint": "eslint .",
|
|
37
39
|
"lint:fix": "eslint . --fix",
|
|
38
40
|
"test": "node scripts/tasks.mjs test",
|
|
39
|
-
"test:fast": "node --test --import tsx/esm src/__tests__/**/*.test.ts",
|
|
41
|
+
"test:fast": "node --test --import tsx/esm src/__tests__/**/*.test.ts node-tests/**/*.test.ts",
|
|
40
42
|
"test:coverage": "node scripts/tasks.mjs test --coverage",
|
|
41
43
|
"knip": "knip",
|
|
42
44
|
"knip:fix": "knip --fix",
|