@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.
Files changed (42) hide show
  1. package/dist/completions.js +25 -0
  2. package/dist/lib/constants.d.ts +2 -0
  3. package/dist/lib/constants.js +2 -0
  4. package/dist/lib/file-operations/metadata.js +71 -142
  5. package/dist/lib/file-operations/search.js +119 -19
  6. package/dist/lib/file-operations/traversal.js +45 -102
  7. package/dist/lib/fs-helpers.js +175 -158
  8. package/dist/prompts.d.ts +1 -0
  9. package/dist/prompts.js +58 -0
  10. package/dist/resources/generated-instructions.js +11 -3
  11. package/dist/resources/tool-catalog.js +49 -3
  12. package/dist/resources/tool-info.d.ts +1 -0
  13. package/dist/resources/tool-info.js +139 -8
  14. package/dist/resources.js +2 -0
  15. package/dist/schemas.d.ts +2 -0
  16. package/dist/schemas.js +10 -4
  17. package/dist/server/bootstrap.js +41 -5
  18. package/dist/tools/apply-patch.js +30 -33
  19. package/dist/tools/calculate-hash.js +21 -24
  20. package/dist/tools/contract.d.ts +1 -1
  21. package/dist/tools/create-directory.js +3 -3
  22. package/dist/tools/delete-file.js +2 -2
  23. package/dist/tools/diff-files.js +32 -20
  24. package/dist/tools/edit-file.d.ts +4 -1
  25. package/dist/tools/edit-file.js +172 -104
  26. package/dist/tools/list-directory.js +105 -10
  27. package/dist/tools/move-file.js +3 -3
  28. package/dist/tools/read-multiple.d.ts +1 -1
  29. package/dist/tools/read-multiple.js +123 -99
  30. package/dist/tools/read.js +104 -76
  31. package/dist/tools/replace-in-files.d.ts +5 -2
  32. package/dist/tools/replace-in-files.js +142 -76
  33. package/dist/tools/roots.js +1 -1
  34. package/dist/tools/search-content.js +249 -218
  35. package/dist/tools/shared.js +39 -7
  36. package/dist/tools/stat-many.js +12 -19
  37. package/dist/tools/stat.js +1 -1
  38. package/dist/tools/task-support.d.ts +7 -0
  39. package/dist/tools/task-support.js +140 -101
  40. package/dist/tools/write-file.js +4 -4
  41. package/dist/tools.js +2 -2
  42. package/package.json +1 -1
@@ -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
- assertNotAborted(signal);
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
- finish(() => {
70
- reject(getAbortError(signal));
71
- });
112
+ reject(getAbortError(signal));
72
113
  };
73
- signal.addEventListener('abort', onAbort, { once: true });
74
- try {
75
- signal.throwIfAborted();
76
- }
77
- catch {
114
+ if (signal.aborted) {
78
115
  onAbort();
79
116
  return;
80
117
  }
81
- promise
82
- .then((value) => {
83
- finish(() => {
84
- resolve(value);
85
- });
86
- })
87
- .catch((error) => {
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 createForwardedSignal(baseSignal);
139
+ return { signal: baseSignal, cleanup: () => { } };
106
140
  }
107
141
  if (timeoutSignal) {
108
142
  return { signal: timeoutSignal, cleanup: () => { } };
109
143
  }
110
- return createNoopSignal();
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 withFileHandle(filePath, fn, existingHandle, signal) {
197
- if (existingHandle) {
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
- if (hasKnownBinaryExtension(filePath)) {
226
- return true;
227
- }
228
- return withFileHandle(filePath, async (handle) => {
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
- }, existingHandle, signal);
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 collector = new BufferCollector(maxSize, requestedPath);
353
- await pipeline(stream, collector, { signal });
354
- return collector.getResult();
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 requireHead(normalized, filePath) {
503
- if (normalized.head === undefined) {
504
- throw new McpError(ErrorCode.E_INVALID_INPUT, 'Missing head option', filePath);
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
- return normalized.head;
537
+ throw new McpError(ErrorCode.E_INVALID_INPUT, `Missing ${key} option`, filePath);
507
538
  }
508
- function buildHeadResult(validPath, content, truncated, head, linesRead, hasMoreLines) {
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 buildRangeResult(validPath, content, truncated, startLine, endLine, linesRead, hasMoreLines) {
520
- const result = {
521
- path: validPath,
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 buildFullResult(validPath, content, totalLines) {
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 buildTailResult(validPath, content, truncated, tail, linesRead, hasMoreLines) {
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
- function assertSizeWithinLimit(size, maxSize, filePath) {
557
- if (size <= maxSize)
558
- return;
559
- 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 });
560
- }
561
- async function readHeadResult(handle, validPath, filePath, normalized) {
562
- const head = requireHead(normalized, filePath);
563
- const readOptions = buildReadContentOptions(normalized);
564
- const { content, truncated, linesRead, hasMoreLines } = await readHeadContent(handle, head, readOptions);
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
- assertNotAborted(normalized.signal);
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
- return await readByMode(handle, validPath, filePath, stats, normalized);
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
- await handle.close();
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 withAbort(fsp.writeFile(tempPath, content, { encoding, signal }), signal);
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: Tools returning a task ID must be polled via \`tasks/get\`, then retrieved via \`tasks/result\`.
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
- return `<tool_selection_guide>
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;