@j0hanz/filesystem-mcp 1.11.0 → 1.12.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.
- package/dist/completions.js +25 -0
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/file-operations/metadata.js +71 -142
- package/dist/lib/file-operations/search.js +119 -19
- package/dist/lib/file-operations/traversal.js +45 -102
- package/dist/lib/fs-helpers.js +175 -158
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.js +58 -0
- package/dist/resources/generated-instructions.js +11 -3
- package/dist/resources/tool-catalog.js +49 -3
- package/dist/resources/tool-info.d.ts +1 -0
- package/dist/resources/tool-info.js +139 -8
- package/dist/resources.js +2 -0
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.js +10 -4
- package/dist/server/bootstrap.js +41 -5
- package/dist/tools/apply-patch.js +30 -33
- package/dist/tools/calculate-hash.js +21 -24
- package/dist/tools/contract.d.ts +1 -1
- package/dist/tools/create-directory.js +3 -3
- package/dist/tools/delete-file.js +2 -2
- package/dist/tools/diff-files.js +32 -20
- package/dist/tools/edit-file.d.ts +4 -1
- package/dist/tools/edit-file.js +172 -104
- package/dist/tools/list-directory.js +105 -10
- package/dist/tools/move-file.js +3 -3
- package/dist/tools/read-multiple.d.ts +1 -1
- package/dist/tools/read-multiple.js +123 -99
- package/dist/tools/read.js +104 -76
- package/dist/tools/replace-in-files.d.ts +5 -2
- package/dist/tools/replace-in-files.js +142 -76
- package/dist/tools/roots.js +1 -1
- package/dist/tools/search-content.js +249 -218
- package/dist/tools/shared.js +39 -7
- package/dist/tools/stat-many.js +12 -19
- package/dist/tools/stat.js +1 -1
- package/dist/tools/task-support.d.ts +7 -0
- package/dist/tools/task-support.js +140 -101
- package/dist/tools/write-file.js +4 -4
- package/dist/tools.js +2 -2
- package/package.json +1 -1
package/dist/lib/fs-helpers.js
CHANGED
|
@@ -1,15 +1,66 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
1
53
|
import * as fsp from 'node:fs/promises';
|
|
2
54
|
import * as path from 'node:path';
|
|
3
55
|
import { isUtf8 } from 'node:buffer';
|
|
4
56
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { Writable } from 'node:stream';
|
|
6
|
-
import { pipeline } from 'node:stream/promises';
|
|
7
57
|
import { BINARY_CHECK_BUFFER_SIZE, KNOWN_BINARY_EXTENSIONS, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from './constants.js';
|
|
8
58
|
import { ErrorCode, McpError, normalizeUnknownError } from './errors.js';
|
|
9
59
|
import { assertAllowedFileAccess, validateExistingPath } from './paths.js';
|
|
10
60
|
function createAbortError(message = 'Operation aborted') {
|
|
11
61
|
return new DOMException(message, 'AbortError');
|
|
12
62
|
}
|
|
63
|
+
const READ_ONLY_FILE_FLAG = 'r';
|
|
13
64
|
const SHARED_NOOP_SIGNAL = new AbortController().signal;
|
|
14
65
|
function normalizeAbortReason(reason, message) {
|
|
15
66
|
if (reason instanceof Error)
|
|
@@ -55,39 +106,22 @@ function getAbortError(signal, message) {
|
|
|
55
106
|
export function withAbort(promise, signal) {
|
|
56
107
|
if (!signal)
|
|
57
108
|
return promise;
|
|
58
|
-
|
|
109
|
+
signal.throwIfAborted();
|
|
59
110
|
return new Promise((resolve, reject) => {
|
|
60
|
-
let settled = false;
|
|
61
|
-
const finish = (run) => {
|
|
62
|
-
if (settled)
|
|
63
|
-
return;
|
|
64
|
-
settled = true;
|
|
65
|
-
signal.removeEventListener('abort', onAbort);
|
|
66
|
-
run();
|
|
67
|
-
};
|
|
68
111
|
const onAbort = () => {
|
|
69
|
-
|
|
70
|
-
reject(getAbortError(signal));
|
|
71
|
-
});
|
|
112
|
+
reject(getAbortError(signal));
|
|
72
113
|
};
|
|
73
|
-
signal.
|
|
74
|
-
try {
|
|
75
|
-
signal.throwIfAborted();
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
114
|
+
if (signal.aborted) {
|
|
78
115
|
onAbort();
|
|
79
116
|
return;
|
|
80
117
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
finish(() => {
|
|
89
|
-
reject(normalizeUnknownError(error));
|
|
90
|
-
});
|
|
118
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
119
|
+
promise.then((value) => {
|
|
120
|
+
signal.removeEventListener('abort', onAbort);
|
|
121
|
+
resolve(value);
|
|
122
|
+
}, (error) => {
|
|
123
|
+
signal.removeEventListener('abort', onAbort);
|
|
124
|
+
reject(normalizeUnknownError(error));
|
|
91
125
|
});
|
|
92
126
|
});
|
|
93
127
|
}
|
|
@@ -102,12 +136,12 @@ export function createTimedAbortSignal(baseSignal, timeoutMs) {
|
|
|
102
136
|
};
|
|
103
137
|
}
|
|
104
138
|
if (baseSignal) {
|
|
105
|
-
return
|
|
139
|
+
return { signal: baseSignal, cleanup: () => { } };
|
|
106
140
|
}
|
|
107
141
|
if (timeoutSignal) {
|
|
108
142
|
return { signal: timeoutSignal, cleanup: () => { } };
|
|
109
143
|
}
|
|
110
|
-
return
|
|
144
|
+
return { signal: SHARED_NOOP_SIGNAL, cleanup: () => { } };
|
|
111
145
|
}
|
|
112
146
|
export async function withTimedAbortSignal(baseSignal, timeoutMs, run) {
|
|
113
147
|
const { signal, cleanup } = createTimedAbortSignal(baseSignal, timeoutMs);
|
|
@@ -118,12 +152,6 @@ export async function withTimedAbortSignal(baseSignal, timeoutMs, run) {
|
|
|
118
152
|
cleanup();
|
|
119
153
|
}
|
|
120
154
|
}
|
|
121
|
-
function createNoopSignal() {
|
|
122
|
-
return { signal: SHARED_NOOP_SIGNAL, cleanup: () => { } };
|
|
123
|
-
}
|
|
124
|
-
function createForwardedSignal(baseSignal) {
|
|
125
|
-
return { signal: baseSignal, cleanup: () => { } };
|
|
126
|
-
}
|
|
127
155
|
function createParallelAbortError() {
|
|
128
156
|
return createAbortError();
|
|
129
157
|
}
|
|
@@ -193,20 +221,8 @@ function hasKnownBinaryExtension(filePath) {
|
|
|
193
221
|
const ext = path.extname(filePath).toLowerCase();
|
|
194
222
|
return KNOWN_BINARY_EXTENSIONS.has(ext);
|
|
195
223
|
}
|
|
196
|
-
async function
|
|
197
|
-
|
|
198
|
-
return fn(existingHandle);
|
|
199
|
-
}
|
|
200
|
-
const effectivePath = await validateExistingPath(filePath, signal);
|
|
201
|
-
const handle = await withAbort(fsp.open(effectivePath, 'r'), signal);
|
|
202
|
-
try {
|
|
203
|
-
return await fn(handle);
|
|
204
|
-
}
|
|
205
|
-
finally {
|
|
206
|
-
await handle.close().catch((error) => {
|
|
207
|
-
console.error('Failed to close file handle:', error);
|
|
208
|
-
});
|
|
209
|
-
}
|
|
224
|
+
async function openReadableFileHandle(filePath, signal) {
|
|
225
|
+
return withAbort(fsp.open(filePath, READ_ONLY_FILE_FLAG), signal);
|
|
210
226
|
}
|
|
211
227
|
async function readProbe(handle, signal) {
|
|
212
228
|
const buffer = Buffer.alloc(BINARY_CHECK_BUFFER_SIZE);
|
|
@@ -222,13 +238,29 @@ function hasUtf16Bom(slice) {
|
|
|
222
238
|
(slice[0] === 0xfe && slice[1] === 0xff)));
|
|
223
239
|
}
|
|
224
240
|
export async function isProbablyBinary(filePath, existingHandle, signal) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
241
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
242
|
+
try {
|
|
243
|
+
if (hasKnownBinaryExtension(filePath)) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
if (existingHandle) {
|
|
247
|
+
const slice = await readProbe(existingHandle, signal);
|
|
248
|
+
return isBinarySlice(slice);
|
|
249
|
+
}
|
|
250
|
+
const effectivePath = await validateExistingPath(filePath, signal);
|
|
251
|
+
const handle = __addDisposableResource(env_1, await openReadableFileHandle(effectivePath, signal), true);
|
|
229
252
|
const slice = await readProbe(handle, signal);
|
|
230
253
|
return isBinarySlice(slice);
|
|
231
|
-
}
|
|
254
|
+
}
|
|
255
|
+
catch (e_1) {
|
|
256
|
+
env_1.error = e_1;
|
|
257
|
+
env_1.hasError = true;
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
const result_1 = __disposeResources(env_1);
|
|
261
|
+
if (result_1)
|
|
262
|
+
await result_1;
|
|
263
|
+
}
|
|
232
264
|
}
|
|
233
265
|
function isBinarySlice(slice) {
|
|
234
266
|
if (slice.length === 0)
|
|
@@ -316,42 +348,35 @@ const STREAM_CHUNK_SIZE = 64 * 1024;
|
|
|
316
348
|
function createTooLargeError(bytesRead, maxSize, requestedPath) {
|
|
317
349
|
return new McpError(ErrorCode.E_TOO_LARGE, `File exceeds maximum size (${bytesRead} > ${maxSize}): ${requestedPath}`, requestedPath, { size: bytesRead, maxSize });
|
|
318
350
|
}
|
|
319
|
-
class BufferCollector extends Writable {
|
|
320
|
-
#chunks = [];
|
|
321
|
-
#totalSize = 0;
|
|
322
|
-
#maxSize;
|
|
323
|
-
#requestedPath;
|
|
324
|
-
constructor(maxSize, requestedPath) {
|
|
325
|
-
super({ autoDestroy: true });
|
|
326
|
-
this.#maxSize = maxSize;
|
|
327
|
-
this.#requestedPath = requestedPath;
|
|
328
|
-
}
|
|
329
|
-
_write(chunk, _encoding, callback) {
|
|
330
|
-
const buffer = Buffer.isBuffer(chunk)
|
|
331
|
-
? chunk
|
|
332
|
-
: Buffer.from(chunk, _encoding);
|
|
333
|
-
this.#totalSize += buffer.length;
|
|
334
|
-
if (this.#totalSize > this.#maxSize) {
|
|
335
|
-
callback(createTooLargeError(this.#totalSize, this.#maxSize, this.#requestedPath));
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
this.#chunks.push(buffer);
|
|
339
|
-
callback();
|
|
340
|
-
}
|
|
341
|
-
getResult() {
|
|
342
|
-
return Buffer.concat(this.#chunks, this.#totalSize);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
351
|
async function readFileBufferWithLimit(handle, maxSize, requestedPath, signal) {
|
|
346
352
|
const stream = handle.createReadStream({
|
|
347
353
|
start: 0,
|
|
348
354
|
highWaterMark: STREAM_CHUNK_SIZE,
|
|
349
355
|
autoClose: false,
|
|
350
356
|
emitClose: false,
|
|
357
|
+
signal,
|
|
351
358
|
});
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
359
|
+
const chunks = [];
|
|
360
|
+
let totalSize = 0;
|
|
361
|
+
try {
|
|
362
|
+
for await (const chunk of stream) {
|
|
363
|
+
const buffer = Buffer.isBuffer(chunk)
|
|
364
|
+
? chunk
|
|
365
|
+
: Buffer.from(chunk);
|
|
366
|
+
totalSize += buffer.length;
|
|
367
|
+
if (totalSize > maxSize) {
|
|
368
|
+
stream.destroy();
|
|
369
|
+
throw createTooLargeError(totalSize, maxSize, requestedPath);
|
|
370
|
+
}
|
|
371
|
+
chunks.push(buffer);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
if (!stream.destroyed) {
|
|
376
|
+
stream.destroy();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return Buffer.concat(chunks, totalSize);
|
|
355
380
|
}
|
|
356
381
|
async function headFile(handle, numLines, encoding = 'utf-8', maxBytesRead, signal) {
|
|
357
382
|
assertNotAborted(signal);
|
|
@@ -499,15 +524,24 @@ async function assertNotBinary(validPath, filePath, normalized) {
|
|
|
499
524
|
return;
|
|
500
525
|
throw new McpError(ErrorCode.E_INVALID_INPUT, `Binary file detected: ${filePath}. Refusing to read as text.`, filePath);
|
|
501
526
|
}
|
|
502
|
-
function
|
|
503
|
-
if (
|
|
504
|
-
|
|
527
|
+
function assertSizeWithinLimit(size, maxSize, filePath) {
|
|
528
|
+
if (size <= maxSize)
|
|
529
|
+
return;
|
|
530
|
+
throw new McpError(ErrorCode.E_TOO_LARGE, `File too large: ${size} bytes (max: ${maxSize} bytes). Use head parameter to preview the first N lines.`, filePath, { size, maxSize });
|
|
531
|
+
}
|
|
532
|
+
function requireReadOption(normalized, key, filePath) {
|
|
533
|
+
const value = normalized[key];
|
|
534
|
+
if (value !== undefined) {
|
|
535
|
+
return value;
|
|
505
536
|
}
|
|
506
|
-
|
|
537
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, `Missing ${key} option`, filePath);
|
|
507
538
|
}
|
|
508
|
-
function
|
|
539
|
+
async function executeHeadRead(context) {
|
|
540
|
+
const head = requireReadOption(context.normalized, 'head', context.filePath);
|
|
541
|
+
const readOptions = buildReadContentOptions(context.normalized);
|
|
542
|
+
const { content, truncated, linesRead, hasMoreLines } = await readHeadContent(context.handle, head, readOptions);
|
|
509
543
|
return {
|
|
510
|
-
path: validPath,
|
|
544
|
+
path: context.validPath,
|
|
511
545
|
content,
|
|
512
546
|
truncated,
|
|
513
547
|
readMode: 'head',
|
|
@@ -516,24 +550,27 @@ function buildHeadResult(validPath, content, truncated, head, linesRead, hasMore
|
|
|
516
550
|
hasMoreLines,
|
|
517
551
|
};
|
|
518
552
|
}
|
|
519
|
-
function
|
|
520
|
-
const
|
|
521
|
-
|
|
553
|
+
async function executeRangeRead(context) {
|
|
554
|
+
const startLine = requireReadOption(context.normalized, 'startLine', context.filePath);
|
|
555
|
+
const { endLine } = context.normalized;
|
|
556
|
+
const readOptions = buildReadContentOptions(context.normalized);
|
|
557
|
+
const { content, truncated, linesRead, hasMoreLines } = await readRangeContent(context.handle, startLine, endLine, readOptions);
|
|
558
|
+
return {
|
|
559
|
+
path: context.validPath,
|
|
522
560
|
content,
|
|
523
561
|
truncated,
|
|
524
562
|
readMode: 'range',
|
|
525
563
|
startLine,
|
|
564
|
+
...(endLine !== undefined ? { endLine } : {}),
|
|
526
565
|
linesRead,
|
|
527
566
|
hasMoreLines,
|
|
528
567
|
};
|
|
529
|
-
if (endLine !== undefined) {
|
|
530
|
-
result.endLine = endLine;
|
|
531
|
-
}
|
|
532
|
-
return result;
|
|
533
568
|
}
|
|
534
|
-
function
|
|
569
|
+
async function executeFullRead(context) {
|
|
570
|
+
assertSizeWithinLimit(context.stats.size, context.normalized.maxSize, context.filePath);
|
|
571
|
+
const { content, totalLines } = await readFullContent(context.handle, context.normalized.encoding, context.normalized.maxSize, context.filePath, context.normalized.signal);
|
|
535
572
|
return {
|
|
536
|
-
path: validPath,
|
|
573
|
+
path: context.validPath,
|
|
537
574
|
content,
|
|
538
575
|
truncated: false,
|
|
539
576
|
totalLines,
|
|
@@ -542,9 +579,12 @@ function buildFullResult(validPath, content, totalLines) {
|
|
|
542
579
|
hasMoreLines: false,
|
|
543
580
|
};
|
|
544
581
|
}
|
|
545
|
-
function
|
|
582
|
+
async function executeTailRead(context) {
|
|
583
|
+
const tail = requireReadOption(context.normalized, 'tail', context.filePath);
|
|
584
|
+
const readOptions = buildReadContentOptions(context.normalized);
|
|
585
|
+
const { content, truncated, linesRead, hasMoreLines } = await readTailContent(context.handle, tail, readOptions);
|
|
546
586
|
return {
|
|
547
|
-
path: validPath,
|
|
587
|
+
path: context.validPath,
|
|
548
588
|
content,
|
|
549
589
|
truncated,
|
|
550
590
|
readMode: 'tail',
|
|
@@ -553,51 +593,15 @@ function buildTailResult(validPath, content, truncated, tail, linesRead, hasMore
|
|
|
553
593
|
hasMoreLines,
|
|
554
594
|
};
|
|
555
595
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
return buildHeadResult(validPath, content, truncated, head, linesRead, hasMoreLines);
|
|
566
|
-
}
|
|
567
|
-
async function readRangeResult(handle, validPath, filePath, normalized) {
|
|
568
|
-
const { startLine, endLine } = normalized;
|
|
569
|
-
if (startLine === undefined) {
|
|
570
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Missing startLine option', filePath);
|
|
571
|
-
}
|
|
572
|
-
const readOptions = buildReadContentOptions(normalized);
|
|
573
|
-
const { content, truncated, linesRead, hasMoreLines } = await readRangeContent(handle, startLine, endLine, readOptions);
|
|
574
|
-
return buildRangeResult(validPath, content, truncated, startLine, endLine, linesRead, hasMoreLines);
|
|
575
|
-
}
|
|
576
|
-
async function readFullResult(handle, validPath, filePath, stats, normalized) {
|
|
577
|
-
assertSizeWithinLimit(stats.size, normalized.maxSize, filePath);
|
|
578
|
-
const { content, totalLines } = await readFullContent(handle, normalized.encoding, normalized.maxSize, filePath, normalized.signal);
|
|
579
|
-
return buildFullResult(validPath, content, totalLines);
|
|
580
|
-
}
|
|
581
|
-
async function readTailResult(handle, validPath, filePath, normalized) {
|
|
582
|
-
if (normalized.tail === undefined) {
|
|
583
|
-
throw new McpError(ErrorCode.E_INVALID_INPUT, 'Missing tail option', filePath);
|
|
584
|
-
}
|
|
585
|
-
const readOptions = buildReadContentOptions(normalized);
|
|
586
|
-
const { content, truncated, linesRead, hasMoreLines } = await readTailContent(handle, normalized.tail, readOptions);
|
|
587
|
-
return buildTailResult(validPath, content, truncated, normalized.tail, linesRead, hasMoreLines);
|
|
588
|
-
}
|
|
589
|
-
async function readByMode(handle, validPath, filePath, stats, normalized) {
|
|
590
|
-
const mode = resolveReadMode(normalized);
|
|
591
|
-
if (mode === 'head') {
|
|
592
|
-
return readHeadResult(handle, validPath, filePath, normalized);
|
|
593
|
-
}
|
|
594
|
-
if (mode === 'tail') {
|
|
595
|
-
return readTailResult(handle, validPath, filePath, normalized);
|
|
596
|
-
}
|
|
597
|
-
if (mode === 'range') {
|
|
598
|
-
return readRangeResult(handle, validPath, filePath, normalized);
|
|
599
|
-
}
|
|
600
|
-
return readFullResult(handle, validPath, filePath, stats, normalized);
|
|
596
|
+
const READ_MODE_HANDLERS = {
|
|
597
|
+
head: executeHeadRead,
|
|
598
|
+
range: executeRangeRead,
|
|
599
|
+
full: executeFullRead,
|
|
600
|
+
tail: executeTailRead,
|
|
601
|
+
};
|
|
602
|
+
async function readByMode(context) {
|
|
603
|
+
const mode = resolveReadMode(context.normalized);
|
|
604
|
+
return READ_MODE_HANDLERS[mode](context);
|
|
601
605
|
}
|
|
602
606
|
function assertFileStats(filePath, stats) {
|
|
603
607
|
if (!stats.isFile()) {
|
|
@@ -605,19 +609,32 @@ function assertFileStats(filePath, stats) {
|
|
|
605
609
|
}
|
|
606
610
|
}
|
|
607
611
|
async function readFileWithStatsInternal(filePath, validPath, stats, normalized) {
|
|
608
|
-
|
|
609
|
-
assertAllowedFileAccess(filePath, validPath);
|
|
610
|
-
assertFileStats(filePath, stats);
|
|
611
|
-
if (normalized.skipBinary) {
|
|
612
|
-
await assertNotBinary(validPath, filePath, normalized);
|
|
613
|
-
}
|
|
614
|
-
assertNotAborted(normalized.signal);
|
|
615
|
-
const handle = await withAbort(fsp.open(validPath, 'r'), normalized.signal);
|
|
612
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
616
613
|
try {
|
|
617
|
-
|
|
614
|
+
assertNotAborted(normalized.signal);
|
|
615
|
+
assertAllowedFileAccess(filePath, validPath);
|
|
616
|
+
assertFileStats(filePath, stats);
|
|
617
|
+
if (normalized.skipBinary) {
|
|
618
|
+
await assertNotBinary(validPath, filePath, normalized);
|
|
619
|
+
}
|
|
620
|
+
assertNotAborted(normalized.signal);
|
|
621
|
+
const handle = __addDisposableResource(env_2, await openReadableFileHandle(validPath, normalized.signal), true);
|
|
622
|
+
return await readByMode({
|
|
623
|
+
handle,
|
|
624
|
+
validPath,
|
|
625
|
+
filePath,
|
|
626
|
+
stats,
|
|
627
|
+
normalized,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
catch (e_2) {
|
|
631
|
+
env_2.error = e_2;
|
|
632
|
+
env_2.hasError = true;
|
|
618
633
|
}
|
|
619
634
|
finally {
|
|
620
|
-
|
|
635
|
+
const result_2 = __disposeResources(env_2);
|
|
636
|
+
if (result_2)
|
|
637
|
+
await result_2;
|
|
621
638
|
}
|
|
622
639
|
}
|
|
623
640
|
export async function readFileWithStats(filePath, validPath, stats, options = {}) {
|
|
@@ -636,7 +653,7 @@ export async function atomicWriteFile(filePath, content, options = {}) {
|
|
|
636
653
|
const tempPath = `${filePath}.${randomUUID()}.tmp`;
|
|
637
654
|
try {
|
|
638
655
|
assertNotAborted(signal);
|
|
639
|
-
await
|
|
656
|
+
await fsp.writeFile(tempPath, content, { encoding, signal });
|
|
640
657
|
await withAbort(fsp.rename(tempPath, filePath), signal);
|
|
641
658
|
}
|
|
642
659
|
catch (error) {
|
package/dist/prompts.d.ts
CHANGED
|
@@ -3,3 +3,4 @@ import { type IconInfo } from './tools/shared.js';
|
|
|
3
3
|
export declare function registerGetHelpPrompt(server: McpServer, instructions: string, iconInfo?: IconInfo): void;
|
|
4
4
|
export declare function registerCompareFilesPrompt(server: McpServer, iconInfo?: IconInfo): void;
|
|
5
5
|
export declare function registerAnalyzePathPrompt(server: McpServer, iconInfo?: IconInfo): void;
|
|
6
|
+
export declare function registerGetToolHelpPrompt(server: McpServer, iconInfo?: IconInfo): void;
|
package/dist/prompts.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { ErrorCode, McpError } from './lib/errors.js';
|
|
3
|
+
import { buildToolInfo, getSortedToolContracts, } from './resources/tool-info.js';
|
|
2
4
|
import { withDefaultIcons } from './tools/shared.js';
|
|
3
5
|
const HELP_PROMPT_NAME = 'get-help';
|
|
4
6
|
const HELP_PROMPT_TITLE = 'Get Help';
|
|
@@ -9,6 +11,9 @@ const COMPARE_FILES_PROMPT_DESCRIPTION = 'Generate a workflow for comparing two
|
|
|
9
11
|
const ANALYZE_PATH_PROMPT_NAME = 'analyze-path';
|
|
10
12
|
const ANALYZE_PATH_PROMPT_TITLE = 'Analyze Path';
|
|
11
13
|
const ANALYZE_PATH_PROMPT_DESCRIPTION = 'Generate a workflow for analyzing a file or directory using stat, read, and tree.';
|
|
14
|
+
const GET_TOOL_HELP_PROMPT_NAME = 'get-tool-help';
|
|
15
|
+
const GET_TOOL_HELP_PROMPT_TITLE = 'Get Tool Help';
|
|
16
|
+
const GET_TOOL_HELP_PROMPT_DESCRIPTION = 'Return a prompt with the authoritative contract for a specific filesystem-mcp tool.';
|
|
12
17
|
function filterInstructionsByTopic(instructions, topic) {
|
|
13
18
|
const normalized = topic.trim().toLowerCase();
|
|
14
19
|
if (!normalized)
|
|
@@ -24,6 +29,12 @@ function filterInstructionsByTopic(instructions, topic) {
|
|
|
24
29
|
.join(', ');
|
|
25
30
|
return `Section '${topic}' not found. Available: ${available}\n\n${instructions}`;
|
|
26
31
|
}
|
|
32
|
+
function findKnownToolName(rawName) {
|
|
33
|
+
const normalized = rawName.trim().toLowerCase();
|
|
34
|
+
if (!normalized)
|
|
35
|
+
return undefined;
|
|
36
|
+
return getSortedToolContracts().find((contract) => contract.name.toLowerCase() === normalized)?.name;
|
|
37
|
+
}
|
|
27
38
|
export function registerGetHelpPrompt(server, instructions, iconInfo) {
|
|
28
39
|
const baseConfig = withDefaultIcons({ title: HELP_PROMPT_TITLE, description: HELP_PROMPT_DESCRIPTION }, iconInfo);
|
|
29
40
|
server.registerPrompt(HELP_PROMPT_NAME, {
|
|
@@ -97,3 +108,50 @@ export function registerAnalyzePathPrompt(server, iconInfo) {
|
|
|
97
108
|
],
|
|
98
109
|
}));
|
|
99
110
|
}
|
|
111
|
+
export function registerGetToolHelpPrompt(server, iconInfo) {
|
|
112
|
+
server.registerPrompt(GET_TOOL_HELP_PROMPT_NAME, {
|
|
113
|
+
...withDefaultIcons({
|
|
114
|
+
title: GET_TOOL_HELP_PROMPT_TITLE,
|
|
115
|
+
description: GET_TOOL_HELP_PROMPT_DESCRIPTION,
|
|
116
|
+
}, iconInfo),
|
|
117
|
+
argsSchema: {
|
|
118
|
+
name: z
|
|
119
|
+
.string()
|
|
120
|
+
.min(1)
|
|
121
|
+
.describe('Tool name from tools/list or internal://tool-info/{name}.'),
|
|
122
|
+
},
|
|
123
|
+
}, ({ name }) => {
|
|
124
|
+
const toolName = findKnownToolName(name);
|
|
125
|
+
if (!toolName) {
|
|
126
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, `Unknown tool: ${name}`);
|
|
127
|
+
}
|
|
128
|
+
const toolInfo = buildToolInfo(toolName);
|
|
129
|
+
if (!toolInfo) {
|
|
130
|
+
throw new McpError(ErrorCode.E_INVALID_INPUT, `Unknown tool: ${toolName}`);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
description: GET_TOOL_HELP_PROMPT_DESCRIPTION,
|
|
134
|
+
messages: [
|
|
135
|
+
{
|
|
136
|
+
role: 'user',
|
|
137
|
+
content: {
|
|
138
|
+
type: 'text',
|
|
139
|
+
text: `Use the embedded contract for \`${toolName}\` as the authoritative reference. ` +
|
|
140
|
+
'Summarize when to use it, its key constraints, and the safest next action.',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
role: 'user',
|
|
145
|
+
content: {
|
|
146
|
+
type: 'resource',
|
|
147
|
+
resource: {
|
|
148
|
+
uri: `internal://tool-info/${toolName}`,
|
|
149
|
+
mimeType: 'text/markdown',
|
|
150
|
+
text: toolInfo,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
|
|
2
|
-
import { buildCoreContextPack, formatToolNameList, getSharedConstraints, getTaskCapableToolNames, getToolContracts, pickAvailableToolNames, } from './tool-info.js';
|
|
2
|
+
import { buildCoreContextPack, formatToolNameList, getSharedConstraints, getTaskCapableToolNames, getTaskToolNamesBySupport, getToolContracts, pickAvailableToolNames, } from './tool-info.js';
|
|
3
3
|
import { buildWorkflowGuide } from './workflows.js';
|
|
4
|
+
function formatTaskModeLine(label, names) {
|
|
5
|
+
return `${label}: ${names.length > 0 ? formatToolNameList(names) : 'none'}.`;
|
|
6
|
+
}
|
|
4
7
|
function buildToolsOverview() {
|
|
5
8
|
const rows = [
|
|
6
9
|
['Navigate', pickAvailableToolNames(['roots', 'ls', 'tree', 'find'])],
|
|
@@ -29,6 +32,8 @@ function buildToolsOverview() {
|
|
|
29
32
|
}
|
|
30
33
|
function buildInstructionsHeader() {
|
|
31
34
|
const taskCapable = formatToolNameList(getTaskCapableToolNames());
|
|
35
|
+
const optionalTaskTools = getTaskToolNamesBySupport('optional');
|
|
36
|
+
const requiredTaskTools = getTaskToolNamesBySupport('required');
|
|
32
37
|
return `<role>
|
|
33
38
|
Filesystem agent. Scope: allowed roots only. Discover paths before acting — never guess.
|
|
34
39
|
</role>
|
|
@@ -49,9 +54,12 @@ ${buildToolsOverview()}
|
|
|
49
54
|
</resources>
|
|
50
55
|
|
|
51
56
|
<task_protocol>
|
|
52
|
-
Task execution:
|
|
57
|
+
Task execution: Call task-capable tools inline by default; add \`task\` only when durable polling or deferred results are needed.
|
|
58
|
+
Task results: When a task is requested, poll via \`tasks/get\`, then retrieve the final payload via \`tasks/result\`.
|
|
53
59
|
Progress: Pass \`_meta.progressToken\` in \`tools/call\` to receive \`notifications/progress\`.
|
|
54
|
-
Task-capable: ${taskCapable}.
|
|
60
|
+
Task-capable: ${taskCapable || 'none'}.
|
|
61
|
+
${formatTaskModeLine('Optional task mode', optionalTaskTools)}
|
|
62
|
+
${formatTaskModeLine('Required task mode', requiredTaskTools)}
|
|
55
63
|
</task_protocol>
|
|
56
64
|
`;
|
|
57
65
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildCoreContextPack, pickAvailableToolNames } from './tool-info.js';
|
|
1
|
+
import { buildCoreContextPack, getTaskCapableToolNames, pickAvailableToolNames, } from './tool-info.js';
|
|
2
2
|
function buildCrossToolDataFlow() {
|
|
3
3
|
const flows = [];
|
|
4
4
|
if (pickAvailableToolNames(['find', 'read']).length === 2) {
|
|
@@ -14,13 +14,59 @@ function buildCrossToolDataFlow() {
|
|
|
14
14
|
return flows.join('\n');
|
|
15
15
|
}
|
|
16
16
|
function buildCatalogGuide() {
|
|
17
|
-
|
|
17
|
+
const taskCapable = getTaskCapableToolNames();
|
|
18
|
+
return (`<tool_selection_guide>
|
|
19
|
+
## Primitive Routing
|
|
20
|
+
|
|
21
|
+
- ` +
|
|
22
|
+
'`tools`' +
|
|
23
|
+
`: model-controlled operations that inspect or mutate the allowed filesystem.
|
|
24
|
+
- ` +
|
|
25
|
+
'`resources`' +
|
|
26
|
+
`: application-driven context such as ` +
|
|
27
|
+
'`internal://instructions`' +
|
|
28
|
+
`, ` +
|
|
29
|
+
'`internal://tool-info/{name}`' +
|
|
30
|
+
`, and cached ` +
|
|
31
|
+
'`filesystem-mcp://result/{id}`' +
|
|
32
|
+
` output.
|
|
33
|
+
- ` +
|
|
34
|
+
'`prompts`' +
|
|
35
|
+
`: user-controlled workflow templates for help, comparison, and guided inspection.
|
|
36
|
+
- ` +
|
|
37
|
+
'`completion`' +
|
|
38
|
+
`: argument suggestions for prompts and resource templates; not a discovery mechanism.
|
|
39
|
+
|
|
18
40
|
## Cross-Tool Data Flow
|
|
19
41
|
|
|
20
42
|
\`\`\`
|
|
21
43
|
${buildCrossToolDataFlow()}
|
|
22
44
|
\`\`\`
|
|
23
45
|
|
|
46
|
+
## Result Contract
|
|
47
|
+
|
|
48
|
+
- Successful tools return ` +
|
|
49
|
+
'`content`' +
|
|
50
|
+
` and usually ` +
|
|
51
|
+
'`structuredContent`' +
|
|
52
|
+
`.
|
|
53
|
+
- Tool/business failures return ` +
|
|
54
|
+
'`isError: true`' +
|
|
55
|
+
` inside the tool result, not a JSON-RPC protocol error.
|
|
56
|
+
- When a tool returns ` +
|
|
57
|
+
'`resourceUri`' +
|
|
58
|
+
` or a ` +
|
|
59
|
+
'`resource_link`' +
|
|
60
|
+
`, follow it with ` +
|
|
61
|
+
'`resources/read`' +
|
|
62
|
+
` immediately.
|
|
63
|
+
|
|
64
|
+
## Task Mode Routing
|
|
65
|
+
|
|
66
|
+
- Inline first for fast operations.
|
|
67
|
+
- Add task mode only when the caller needs durable polling, deferred retrieval, or cancellation after the initial response.
|
|
68
|
+
- Task-capable tools: ${taskCapable.length > 0 ? taskCapable.map((name) => `\`${name}\``).join(', ') : 'none'}.
|
|
69
|
+
|
|
24
70
|
## Search Strategy
|
|
25
71
|
|
|
26
72
|
- \`find\`: glob file discovery.
|
|
@@ -47,7 +93,7 @@ ${buildCrossToolDataFlow()}
|
|
|
47
93
|
- \`apply_patch\` accepts unified diffs - single-file or multi-file.
|
|
48
94
|
- Multi-file: \`path\` is base directory; each file is best-effort with per-file \`results[]\`.
|
|
49
95
|
</tool_selection_guide>
|
|
50
|
-
|
|
96
|
+
`);
|
|
51
97
|
}
|
|
52
98
|
export function buildToolCatalog() {
|
|
53
99
|
return `${buildCoreContextPack()}\n\n${buildCatalogGuide()}`;
|
|
@@ -4,6 +4,7 @@ export declare function getSortedToolContracts(): ToolContract[];
|
|
|
4
4
|
export declare function pickAvailableToolNames(names: readonly string[]): string[];
|
|
5
5
|
export declare function formatToolNameList(names: readonly string[]): string;
|
|
6
6
|
export declare function getTaskCapableToolNames(): string[];
|
|
7
|
+
export declare function getTaskToolNamesBySupport(taskSupport: Extract<ToolContract['taskSupport'], 'optional' | 'required'>): string[];
|
|
7
8
|
export declare function buildCoreContextPack(): string;
|
|
8
9
|
export declare function getSharedConstraints(): string[];
|
|
9
10
|
export declare function buildToolInfo(name: string): string | undefined;
|