@j0hanz/filesystem-mcp 1.13.2 → 1.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -145
- package/dist/cli.js +2 -2
- package/dist/completions.js +54 -51
- package/dist/config.d.ts +13 -14
- package/dist/config.js +12 -12
- package/dist/index.js +1 -1
- package/dist/lib/abort.d.ts +7 -0
- package/dist/lib/abort.js +81 -0
- package/dist/lib/constants.d.ts +3 -1
- package/dist/lib/constants.js +8 -2
- package/dist/lib/errors.d.ts +7 -3
- package/dist/lib/errors.js +64 -41
- package/dist/lib/file-operations/core.d.ts +3 -3
- package/dist/lib/file-operations/core.js +23 -20
- package/dist/lib/file-operations/metadata.d.ts +2 -2
- package/dist/lib/file-operations/metadata.js +69 -22
- package/dist/lib/file-operations/search.d.ts +0 -1
- package/dist/lib/file-operations/search.js +87 -95
- package/dist/lib/file-operations/traversal.js +13 -15
- package/dist/lib/fs-helpers.d.ts +3 -10
- package/dist/lib/fs-helpers.js +29 -108
- package/dist/lib/globs.d.ts +2 -0
- package/dist/lib/globs.js +19 -0
- package/dist/lib/logger.d.ts +28 -0
- package/dist/lib/logger.js +91 -0
- package/dist/lib/observability.d.ts +7 -0
- package/dist/lib/observability.js +19 -9
- package/dist/lib/paths.js +55 -55
- package/dist/lib/resource-store.js +4 -4
- package/dist/lib/utils.d.ts +0 -12
- package/dist/lib/utils.js +0 -13
- package/dist/lib/zod-codecs.d.ts +2 -0
- package/dist/lib/zod-codecs.js +18 -0
- package/dist/pkg-info.d.ts +1 -0
- package/dist/pkg-info.js +2 -2
- package/dist/prompts.js +3 -3
- package/dist/resources/generated-instructions.js +41 -41
- package/dist/resources/tool-catalog.js +33 -58
- package/dist/resources/tool-info.d.ts +0 -1
- package/dist/resources/tool-info.js +44 -67
- package/dist/resources/workflows.js +47 -19
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +4 -4
- package/dist/schemas.d.ts +185 -465
- package/dist/schemas.js +174 -206
- package/dist/server/bootstrap.d.ts +12 -11
- package/dist/server/bootstrap.js +95 -86
- package/dist/server/roots-manager.d.ts +5 -2
- package/dist/server/roots-manager.js +9 -7
- package/dist/server/task-store.d.ts +10 -0
- package/dist/server/task-store.js +73 -0
- package/dist/tools/apply-patch.js +39 -20
- package/dist/tools/calculate-hash.js +14 -27
- package/dist/tools/create-directory.js +11 -9
- package/dist/tools/delete-file.js +19 -19
- package/dist/tools/diff-files.js +16 -18
- package/dist/tools/edit-file.js +11 -5
- package/dist/tools/list-directory.js +16 -21
- package/dist/tools/move-file.js +105 -100
- package/dist/tools/read-multiple.js +15 -10
- package/dist/tools/read.js +6 -7
- package/dist/tools/replace-in-files.js +76 -115
- package/dist/tools/roots.js +3 -7
- package/dist/tools/search-content.js +158 -203
- package/dist/tools/search-files.js +59 -50
- package/dist/tools/shared.d.ts +10 -0
- package/dist/tools/shared.js +105 -36
- package/dist/tools/stat-many.js +15 -9
- package/dist/tools/stat.js +6 -6
- package/dist/tools/task-support.d.ts +10 -9
- package/dist/tools/task-support.js +94 -23
- package/dist/tools/tree.js +4 -4
- package/dist/tools/write-file.js +11 -12
- package/package.json +10 -9
package/dist/lib/paths.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises';
|
|
2
|
-
import * as os from 'node:os';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
5
|
-
import {
|
|
2
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
3
|
+
import { homedir, platform } from 'node:os';
|
|
4
|
+
import { dirname, isAbsolute, join, normalize, parse, posix, relative, resolve, sep, win32, } from 'node:path';
|
|
6
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { assertNotAborted, withAbort } from './abort.js';
|
|
7
7
|
import { SENSITIVE_FILE_ALLOWLIST, SENSITIVE_FILE_DENYLIST, } from './constants.js';
|
|
8
8
|
import { ErrorCode, isAbortError, isNodeError, McpError } from './errors.js';
|
|
9
|
-
import {
|
|
9
|
+
import { Logger } from './logger.js';
|
|
10
10
|
const WINDOWS_PATH_SEPARATOR = '\\';
|
|
11
11
|
const POSIX_PATH_SEPARATOR = '/';
|
|
12
12
|
export function toPosixPath(value) {
|
|
@@ -20,7 +20,7 @@ const HOME_PREFIX_LENGTH = 2;
|
|
|
20
20
|
const CHAR_CODE_SPACE = 32;
|
|
21
21
|
const CHAR_CODE_DOT = 46;
|
|
22
22
|
function normalizePathForMatch(input) {
|
|
23
|
-
return toPosixPath(
|
|
23
|
+
return toPosixPath(normalize(input));
|
|
24
24
|
}
|
|
25
25
|
function normalizeForMatch(input) {
|
|
26
26
|
const normalized = normalizePathForMatch(input);
|
|
@@ -84,7 +84,7 @@ function matchesAnyGlobs(globs, candidates) {
|
|
|
84
84
|
return false;
|
|
85
85
|
for (const candidate of candidates) {
|
|
86
86
|
for (const glob of globs) {
|
|
87
|
-
if (
|
|
87
|
+
if (posix.matchesGlob(candidate, glob))
|
|
88
88
|
return true;
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -100,7 +100,7 @@ export function isSensitivePath(requestedPath, resolvedPath) {
|
|
|
100
100
|
? normalizeForMatch(resolvedPath)
|
|
101
101
|
: undefined;
|
|
102
102
|
const pathCandidates = uniquePair(normalizedRequested, normalizedResolved);
|
|
103
|
-
const nameCandidates = uniquePair(
|
|
103
|
+
const nameCandidates = uniquePair(posix.basename(normalizedRequested), normalizedResolved ? posix.basename(normalizedResolved) : undefined);
|
|
104
104
|
if (matchesAnyGlobs(ALLOW_PATTERNS.pathGlobs, pathCandidates) ||
|
|
105
105
|
matchesAnyGlobs(ALLOW_PATTERNS.nameGlobs, nameCandidates)) {
|
|
106
106
|
return false;
|
|
@@ -111,11 +111,11 @@ export function isSensitivePath(requestedPath, resolvedPath) {
|
|
|
111
111
|
export function assertAllowedFileAccess(requestedPath, resolvedPath) {
|
|
112
112
|
if (!isSensitivePath(requestedPath, resolvedPath))
|
|
113
113
|
return;
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
Logger.warn(`Access denied: sensitive file blocked by policy (${requestedPath})`);
|
|
115
|
+
throw new McpError(ErrorCode.ACCESS_DENIED, 'Sensitive file blocked. Set FS_CONTEXT_ALLOW_SENSITIVE=1 to override.', requestedPath);
|
|
116
116
|
}
|
|
117
|
-
const HOMEDIR =
|
|
118
|
-
const PATH_SEPARATOR =
|
|
117
|
+
const HOMEDIR = homedir();
|
|
118
|
+
const PATH_SEPARATOR = sep;
|
|
119
119
|
const DRIVE_LETTER_REGEX = /^[A-Za-z]:/;
|
|
120
120
|
const WINDOWS_DRIVE_REL_REGEX = /^[A-Za-z]:$/u;
|
|
121
121
|
const LEADING_SEPARATORS_RE = /^[/\\]+/;
|
|
@@ -160,11 +160,11 @@ function expandHome(filepath) {
|
|
|
160
160
|
return HOMEDIR;
|
|
161
161
|
// Accept both "~/" and "~\\" for cross-platform UX.
|
|
162
162
|
if (filepath.startsWith('~/') || filepath.startsWith('~\\')) {
|
|
163
|
-
// Avoid `
|
|
163
|
+
// Avoid `join(HOMEDIR, "/foo")` resetting to the filesystem root.
|
|
164
164
|
const rest = filepath
|
|
165
165
|
.slice(HOME_PREFIX_LENGTH)
|
|
166
166
|
.replace(LEADING_SEPARATORS_RE, '');
|
|
167
|
-
return rest.length === 0 ? HOMEDIR :
|
|
167
|
+
return rest.length === 0 ? HOMEDIR : join(HOMEDIR, rest);
|
|
168
168
|
}
|
|
169
169
|
return filepath;
|
|
170
170
|
}
|
|
@@ -175,7 +175,7 @@ function expandHome(filepath) {
|
|
|
175
175
|
* - Lowercases Windows drive letter for stable comparisons.
|
|
176
176
|
*/
|
|
177
177
|
export function normalizePath(p) {
|
|
178
|
-
const resolved =
|
|
178
|
+
const resolved = resolve(expandHome(p));
|
|
179
179
|
if (IS_WINDOWS && DRIVE_LETTER_REGEX.test(resolved)) {
|
|
180
180
|
return resolved.charAt(0).toLowerCase() + resolved.slice(1);
|
|
181
181
|
}
|
|
@@ -194,8 +194,8 @@ function rethrowIfAborted(error) {
|
|
|
194
194
|
function isSamePath(left, right) {
|
|
195
195
|
if (left === right)
|
|
196
196
|
return true;
|
|
197
|
-
const leftResolved = normalizeCaseForComparison(
|
|
198
|
-
const rightResolved = normalizeCaseForComparison(
|
|
197
|
+
const leftResolved = normalizeCaseForComparison(resolve(left));
|
|
198
|
+
const rightResolved = normalizeCaseForComparison(resolve(right));
|
|
199
199
|
return leftResolved === rightResolved;
|
|
200
200
|
}
|
|
201
201
|
function stripTrailingSeparator(normalized) {
|
|
@@ -211,7 +211,7 @@ function normalizeAllowedDirectory(dir) {
|
|
|
211
211
|
if (trimmed.length === 0)
|
|
212
212
|
return '';
|
|
213
213
|
const normalized = normalizePath(trimmed);
|
|
214
|
-
const { root } =
|
|
214
|
+
const { root } = parse(normalized);
|
|
215
215
|
// Keep filesystem roots as-is ("/", "c:\\", "\\\\server\\share\\").
|
|
216
216
|
if (isFileSystemRootPath(normalized, root)) {
|
|
217
217
|
return root;
|
|
@@ -274,14 +274,12 @@ function isPathInsideDirectory(normalizedDirectory, normalizedCandidate) {
|
|
|
274
274
|
const candidate = normalizeForComparison(normalizedCandidate);
|
|
275
275
|
if (root === candidate)
|
|
276
276
|
return true;
|
|
277
|
-
const
|
|
278
|
-
if (
|
|
277
|
+
const rel = relative(root, candidate);
|
|
278
|
+
if (rel.length === 0)
|
|
279
279
|
return true;
|
|
280
|
-
if (
|
|
280
|
+
if (rel === '..')
|
|
281
281
|
return false;
|
|
282
|
-
return
|
|
283
|
-
!relative.startsWith('../') &&
|
|
284
|
-
!path.isAbsolute(relative));
|
|
282
|
+
return !rel.startsWith('..\\') && !rel.startsWith('../') && !isAbsolute(rel);
|
|
285
283
|
}
|
|
286
284
|
export function isPathWithinDirectories(normalizedPath, allowedDirs) {
|
|
287
285
|
for (const allowedDir of allowedDirs) {
|
|
@@ -293,7 +291,7 @@ export function isPathWithinDirectories(normalizedPath, allowedDirs) {
|
|
|
293
291
|
async function resolveRealPath(normalized, signal) {
|
|
294
292
|
try {
|
|
295
293
|
assertNotAborted(signal);
|
|
296
|
-
const realPath = await withAbort(
|
|
294
|
+
const realPath = await withAbort(realpath(normalized), signal);
|
|
297
295
|
return normalizeAllowedDirectory(realPath);
|
|
298
296
|
}
|
|
299
297
|
catch (error) {
|
|
@@ -327,12 +325,12 @@ export async function setAllowedDirectoriesResolved(dirs, signal) {
|
|
|
327
325
|
}
|
|
328
326
|
function ensureNonEmptyPath(requestedPath) {
|
|
329
327
|
if (!requestedPath || requestedPath.trim().length === 0) {
|
|
330
|
-
throw new McpError(ErrorCode.
|
|
328
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Path cannot be empty or whitespace', requestedPath);
|
|
331
329
|
}
|
|
332
330
|
}
|
|
333
331
|
function ensureNoNullBytes(requestedPath) {
|
|
334
332
|
if (requestedPath.includes('\0')) {
|
|
335
|
-
throw new McpError(ErrorCode.
|
|
333
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Path contains null bytes', requestedPath);
|
|
336
334
|
}
|
|
337
335
|
}
|
|
338
336
|
function getReservedDeviceName(segment) {
|
|
@@ -371,31 +369,31 @@ function ensureNoReservedWindowsNames(requestedPath) {
|
|
|
371
369
|
const reserved = getReservedDeviceNameForPath(requestedPath);
|
|
372
370
|
if (!reserved)
|
|
373
371
|
return;
|
|
374
|
-
throw new McpError(ErrorCode.
|
|
372
|
+
throw new McpError(ErrorCode.INVALID_INPUT, `Windows reserved device name not allowed: ${reserved}`, requestedPath);
|
|
375
373
|
}
|
|
376
374
|
export function isWindowsDriveRelativePath(requestedPath) {
|
|
377
375
|
if (!IS_WINDOWS)
|
|
378
376
|
return false;
|
|
379
|
-
const parsed =
|
|
377
|
+
const parsed = win32.parse(requestedPath);
|
|
380
378
|
if (!WINDOWS_DRIVE_REL_REGEX.test(parsed.root))
|
|
381
379
|
return false;
|
|
382
|
-
return !
|
|
380
|
+
return !win32.isAbsolute(requestedPath);
|
|
383
381
|
}
|
|
384
382
|
function ensureNoWindowsDriveRelativePath(requestedPath) {
|
|
385
383
|
if (!isWindowsDriveRelativePath(requestedPath))
|
|
386
384
|
return;
|
|
387
|
-
throw new McpError(ErrorCode.
|
|
385
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Drive-relative path not allowed. Use C:\\path instead of C:path.', requestedPath);
|
|
388
386
|
}
|
|
389
387
|
function resolveRequestedPath(requestedPath) {
|
|
390
388
|
const expanded = expandHome(requestedPath);
|
|
391
|
-
if (!
|
|
389
|
+
if (!isAbsolute(expanded)) {
|
|
392
390
|
const roots = getAllowedDirectoriesForRelativeResolution();
|
|
393
391
|
if (roots.length > 1) {
|
|
394
|
-
throw new McpError(ErrorCode.
|
|
392
|
+
throw new McpError(ErrorCode.INVALID_INPUT, 'Ambiguous relative path with multiple roots. Use an absolute path.', requestedPath);
|
|
395
393
|
}
|
|
396
394
|
const baseDir = roots[0];
|
|
397
395
|
if (baseDir) {
|
|
398
|
-
return normalizePath(
|
|
396
|
+
return normalizePath(resolve(baseDir, expanded));
|
|
399
397
|
}
|
|
400
398
|
}
|
|
401
399
|
return normalizePath(expanded);
|
|
@@ -409,24 +407,24 @@ function validateRequestedPath(requestedPath) {
|
|
|
409
407
|
}
|
|
410
408
|
const NODE_ERROR_MAP = {
|
|
411
409
|
ENOENT: {
|
|
412
|
-
code: ErrorCode.
|
|
413
|
-
message: (
|
|
410
|
+
code: ErrorCode.NOT_FOUND,
|
|
411
|
+
message: () => 'Path does not exist',
|
|
414
412
|
},
|
|
415
413
|
EACCES: {
|
|
416
|
-
code: ErrorCode.
|
|
417
|
-
message: (
|
|
414
|
+
code: ErrorCode.PERMISSION_DENIED,
|
|
415
|
+
message: () => 'Permission denied',
|
|
418
416
|
},
|
|
419
417
|
EPERM: {
|
|
420
|
-
code: ErrorCode.
|
|
421
|
-
message: (
|
|
418
|
+
code: ErrorCode.PERMISSION_DENIED,
|
|
419
|
+
message: () => 'Permission denied',
|
|
422
420
|
},
|
|
423
421
|
ELOOP: {
|
|
424
|
-
code: ErrorCode.
|
|
425
|
-
message: (
|
|
422
|
+
code: ErrorCode.SYMLINK_NOT_ALLOWED,
|
|
423
|
+
message: () => 'Too many symbolic links (circular reference)',
|
|
426
424
|
},
|
|
427
425
|
ENAMETOOLONG: {
|
|
428
|
-
code: ErrorCode.
|
|
429
|
-
message: (
|
|
426
|
+
code: ErrorCode.INVALID_INPUT,
|
|
427
|
+
message: () => 'Path name too long',
|
|
430
428
|
},
|
|
431
429
|
};
|
|
432
430
|
function buildAllowedDirectoriesHint() {
|
|
@@ -448,26 +446,28 @@ function toMcpError(requestedPath, error) {
|
|
|
448
446
|
else if (typeof error === 'string') {
|
|
449
447
|
originalMessage = error;
|
|
450
448
|
}
|
|
451
|
-
return new McpError(ErrorCode.
|
|
449
|
+
return new McpError(ErrorCode.NOT_FOUND, 'Path is not accessible', requestedPath, { originalCode: code, originalMessage }, error);
|
|
452
450
|
}
|
|
453
451
|
function toAccessDeniedWithHint(requestedPath, resolvedPath, normalizedResolved) {
|
|
454
452
|
const suggestion = buildAllowedDirectoriesHint();
|
|
455
|
-
return new McpError(ErrorCode.
|
|
453
|
+
return new McpError(ErrorCode.ACCESS_DENIED, `Outside allowed directories. ${suggestion}`, requestedPath, { resolvedPath, normalizedResolvedPath: normalizedResolved });
|
|
456
454
|
}
|
|
457
455
|
function ensureWithinAllowedDirectories(options) {
|
|
458
456
|
const { normalizedPath, requestedPath, allowedDirs, details } = options;
|
|
459
457
|
if (isPathWithinDirectories(normalizedPath, allowedDirs))
|
|
460
458
|
return;
|
|
461
459
|
if (allowedDirs.length === 0) {
|
|
462
|
-
|
|
460
|
+
Logger.warn('Access denied: no allowed directories configured');
|
|
461
|
+
throw new McpError(ErrorCode.ACCESS_DENIED, 'No allowed directories configured. Use --allow-cwd or configure roots.', requestedPath, details);
|
|
463
462
|
}
|
|
464
|
-
|
|
463
|
+
Logger.warn(`Access denied: path outside allowed directories (${requestedPath})`);
|
|
464
|
+
throw new McpError(ErrorCode.ACCESS_DENIED, 'Outside allowed directories', requestedPath, details);
|
|
465
465
|
}
|
|
466
466
|
async function resolveRealPathOrThrow(options) {
|
|
467
467
|
const { requestedPath, normalizedRequested, signal } = options;
|
|
468
468
|
try {
|
|
469
469
|
assertNotAborted(signal);
|
|
470
|
-
return await withAbort(
|
|
470
|
+
return await withAbort(realpath(normalizedRequested), signal);
|
|
471
471
|
}
|
|
472
472
|
catch (error) {
|
|
473
473
|
rethrowIfAborted(error);
|
|
@@ -494,7 +494,7 @@ function ensureResolvedPathAllowed(options) {
|
|
|
494
494
|
async function statPathOrThrow(requestedPath, resolvedPath, signal) {
|
|
495
495
|
try {
|
|
496
496
|
assertNotAborted(signal);
|
|
497
|
-
return await withAbort(
|
|
497
|
+
return await withAbort(stat(resolvedPath), signal);
|
|
498
498
|
}
|
|
499
499
|
catch (error) {
|
|
500
500
|
rethrowIfAborted(error);
|
|
@@ -507,7 +507,7 @@ async function resolveNearestExistingRealPathOrThrow(options) {
|
|
|
507
507
|
for (;;) {
|
|
508
508
|
try {
|
|
509
509
|
assertNotAborted(signal);
|
|
510
|
-
return await withAbort(
|
|
510
|
+
return await withAbort(realpath(current), signal);
|
|
511
511
|
}
|
|
512
512
|
catch (error) {
|
|
513
513
|
rethrowIfAborted(error);
|
|
@@ -515,7 +515,7 @@ async function resolveNearestExistingRealPathOrThrow(options) {
|
|
|
515
515
|
if (code !== 'ENOENT') {
|
|
516
516
|
throw toMcpError(requestedPath, error);
|
|
517
517
|
}
|
|
518
|
-
const parent =
|
|
518
|
+
const parent = dirname(current);
|
|
519
519
|
if (parent === current) {
|
|
520
520
|
throw toMcpError(requestedPath, error);
|
|
521
521
|
}
|
|
@@ -554,7 +554,7 @@ export async function validateExistingDirectory(requestedPath, signal) {
|
|
|
554
554
|
const details = await validateExistingPathDetailsInternal(requestedPath, signal);
|
|
555
555
|
const stats = await statPathOrThrow(requestedPath, details.resolvedPath, signal);
|
|
556
556
|
if (!stats.isDirectory()) {
|
|
557
|
-
throw new McpError(ErrorCode.
|
|
557
|
+
throw new McpError(ErrorCode.NOT_DIRECTORY, 'Not a directory', requestedPath);
|
|
558
558
|
}
|
|
559
559
|
return details.resolvedPath;
|
|
560
560
|
}
|
|
@@ -581,7 +581,7 @@ function isFileRoot(root) {
|
|
|
581
581
|
async function maybeAddRealPath(normalizedPath, validDirs, signal) {
|
|
582
582
|
try {
|
|
583
583
|
assertNotAborted(signal);
|
|
584
|
-
const realPath = await withAbort(
|
|
584
|
+
const realPath = await withAbort(realpath(normalizedPath), signal);
|
|
585
585
|
const normalizedReal = normalizePath(realPath);
|
|
586
586
|
if (!isSamePath(normalizedReal, normalizedPath)) {
|
|
587
587
|
validDirs.push(normalizedReal);
|
|
@@ -596,7 +596,7 @@ async function resolveRootDirectory(root, signal) {
|
|
|
596
596
|
const dirPath = fileURLToPath(root.uri);
|
|
597
597
|
const normalizedPath = normalizePath(dirPath);
|
|
598
598
|
assertNotAborted(signal);
|
|
599
|
-
const stats = await withAbort(
|
|
599
|
+
const stats = await withAbort(stat(normalizedPath), signal);
|
|
600
600
|
if (!stats.isDirectory())
|
|
601
601
|
return null;
|
|
602
602
|
return normalizedPath;
|
|
@@ -94,7 +94,7 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
94
94
|
bytes: entryBytes,
|
|
95
95
|
reason: 'entry_too_large',
|
|
96
96
|
});
|
|
97
|
-
throw new McpError(ErrorCode.
|
|
97
|
+
throw new McpError(ErrorCode.TOO_LARGE, `Resource too large to cache (${entryBytes} bytes).`);
|
|
98
98
|
}
|
|
99
99
|
const contentHash = computeSha256(params.text);
|
|
100
100
|
const indexKey = buildIndexKey(mimeType, contentHash);
|
|
@@ -146,7 +146,7 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
146
146
|
bytes: entry.size,
|
|
147
147
|
reason: 'evicted_immediately',
|
|
148
148
|
});
|
|
149
|
-
throw new McpError(ErrorCode.
|
|
149
|
+
throw new McpError(ErrorCode.TOO_LARGE, 'Cache full: entry evicted.');
|
|
150
150
|
}
|
|
151
151
|
return entry;
|
|
152
152
|
}
|
|
@@ -158,7 +158,7 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
158
158
|
uri,
|
|
159
159
|
reason: 'not_found',
|
|
160
160
|
});
|
|
161
|
-
throw new McpError(ErrorCode.
|
|
161
|
+
throw new McpError(ErrorCode.NOT_FOUND, `Resource not found: ${uri}. Re-run the tool to regenerate.`);
|
|
162
162
|
}
|
|
163
163
|
if (isExpired(existing)) {
|
|
164
164
|
removeEntry(uri, 'expired');
|
|
@@ -167,7 +167,7 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
167
167
|
uri,
|
|
168
168
|
reason: 'expired',
|
|
169
169
|
});
|
|
170
|
-
throw new McpError(ErrorCode.
|
|
170
|
+
throw new McpError(ErrorCode.NOT_FOUND, `Resource expired: ${uri}. Re-run the tool to regenerate.`);
|
|
171
171
|
}
|
|
172
172
|
publishResourceStoreDiagnostics({
|
|
173
173
|
phase: 'cache_hit',
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -5,15 +5,3 @@ export declare function debounce<Args extends unknown[]>(func: (...args: Args) =
|
|
|
5
5
|
};
|
|
6
6
|
export declare function mergeOptions<T extends object>(defaults: T, overrides: Partial<T>): T;
|
|
7
7
|
export declare function omitOptionKeys<T extends object, K extends keyof T>(input: T, keys: readonly K[]): Omit<T, K>;
|
|
8
|
-
interface ProgressPayload {
|
|
9
|
-
current: number;
|
|
10
|
-
total?: number;
|
|
11
|
-
}
|
|
12
|
-
type ProgressCallback = ((progress: ProgressPayload) => void) | undefined;
|
|
13
|
-
interface PeriodicProgressOptions {
|
|
14
|
-
total?: number;
|
|
15
|
-
throttleModulo?: number;
|
|
16
|
-
force?: boolean;
|
|
17
|
-
}
|
|
18
|
-
export declare function reportPeriodicProgress(onProgress: ProgressCallback, current: number, options?: PeriodicProgressOptions): void;
|
|
19
|
-
export {};
|
package/dist/lib/utils.js
CHANGED
|
@@ -35,16 +35,3 @@ export function omitOptionKeys(input, keys) {
|
|
|
35
35
|
}
|
|
36
36
|
return output;
|
|
37
37
|
}
|
|
38
|
-
export function reportPeriodicProgress(onProgress, current, options = {}) {
|
|
39
|
-
if (!onProgress || current === 0)
|
|
40
|
-
return;
|
|
41
|
-
const throttleModulo = options.throttleModulo ?? 1;
|
|
42
|
-
const force = options.force ?? false;
|
|
43
|
-
if (!force && throttleModulo > 1 && current % throttleModulo !== 0) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
onProgress({
|
|
47
|
-
current,
|
|
48
|
-
...(options.total !== undefined ? { total: options.total } : {}),
|
|
49
|
-
});
|
|
50
|
-
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function createBase64JsonCodec(schema) {
|
|
3
|
+
return z.codec(z.string(), schema, {
|
|
4
|
+
decode: (value) => {
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf-8'));
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
throw new Error('Invalid base64url-encoded JSON payload.', {
|
|
11
|
+
cause: error,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return parsed;
|
|
15
|
+
},
|
|
16
|
+
encode: (value) => Buffer.from(JSON.stringify(value)).toString('base64url'),
|
|
17
|
+
});
|
|
18
|
+
}
|
package/dist/pkg-info.d.ts
CHANGED
package/dist/pkg-info.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import packageJsonRaw from '../package.json' with { type: 'json' };
|
|
3
|
-
const PkgInfoSchema = z.
|
|
3
|
+
const PkgInfoSchema = z.looseObject({
|
|
4
4
|
name: z.string(),
|
|
5
5
|
version: z.string(),
|
|
6
6
|
description: z.string().optional(),
|
|
7
|
-
homepage: z.
|
|
7
|
+
homepage: z.url().optional(),
|
|
8
8
|
});
|
|
9
9
|
export const pkgInfo = PkgInfoSchema.parse(packageJsonRaw);
|
package/dist/prompts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { ErrorCode as SdkErrorCode, McpError as SdkMcpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
1
2
|
import { z } from 'zod';
|
|
2
|
-
import { ErrorCode, McpError } from './lib/errors.js';
|
|
3
3
|
import { buildToolInfo, getSortedToolContracts, } from './resources/tool-info.js';
|
|
4
4
|
import { withDefaultIcons } from './tools/shared.js';
|
|
5
5
|
const HELP_PROMPT_NAME = 'get-help';
|
|
@@ -123,11 +123,11 @@ export function registerGetToolHelpPrompt(server, iconInfo) {
|
|
|
123
123
|
}, ({ name }) => {
|
|
124
124
|
const toolName = findKnownToolName(name);
|
|
125
125
|
if (!toolName) {
|
|
126
|
-
throw new
|
|
126
|
+
throw new SdkMcpError(SdkErrorCode.InvalidParams, `Unknown tool: ${name}`);
|
|
127
127
|
}
|
|
128
128
|
const toolInfo = buildToolInfo(toolName);
|
|
129
129
|
if (!toolInfo) {
|
|
130
|
-
throw new
|
|
130
|
+
throw new SdkMcpError(SdkErrorCode.InvalidParams, `Unknown tool: ${toolName}`);
|
|
131
131
|
}
|
|
132
132
|
return {
|
|
133
133
|
description: GET_TOOL_HELP_PROMPT_DESCRIPTION,
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
|
|
2
|
-
import { buildCoreContextPack, formatToolNameList, getSharedConstraints,
|
|
2
|
+
import { buildCoreContextPack, formatToolNameList, getSharedConstraints, 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
|
-
}
|
|
7
4
|
function buildToolsOverview() {
|
|
8
5
|
const rows = [
|
|
9
6
|
['Navigate', pickAvailableToolNames(['roots', 'ls', 'tree', 'find'])],
|
|
@@ -31,58 +28,61 @@ function buildToolsOverview() {
|
|
|
31
28
|
.join('\n');
|
|
32
29
|
}
|
|
33
30
|
function buildInstructionsHeader() {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
</role>
|
|
31
|
+
return `## Role
|
|
32
|
+
|
|
33
|
+
Secure filesystem agent. Operate strictly within allowed roots. Resolve paths before acting — never assume.
|
|
34
|
+
|
|
35
|
+
## Tools Overview
|
|
40
36
|
|
|
41
|
-
<tools_overview>
|
|
42
37
|
| Category | Tools |
|
|
43
|
-
|
|
38
|
+
| -------- | ----- |
|
|
39
|
+
|
|
44
40
|
${buildToolsOverview()}
|
|
45
|
-
</tools_overview>
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
## Resources
|
|
43
|
+
|
|
44
|
+
| URI | Purpose |
|
|
45
|
+
| -------------------------------- | -------------------------------------------------------------- |
|
|
46
|
+
| \`internal://instructions\` | Full usage reference (this document) |
|
|
47
|
+
| \`internal://tool-catalog\` | Tool routing, data flow, and selection guide |
|
|
48
|
+
| \`internal://workflows\` | Step-by-step execution sequences |
|
|
49
|
+
| \`internal://tool-info/{name}\` | Per-tool contract (e.g. \`internal://tool-info/read\`) |
|
|
50
|
+
| \`filesystem-mcp://result/{id}\` | Cached large output — fetch via \`resources/read\` immediately |
|
|
51
|
+
| \`filesystem-mcp://metrics\` | Per-tool call count, error rate, and avg duration |
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
## Task Protocol
|
|
54
|
+
|
|
55
|
+
- Check \`execution.taskSupport\` before sending task metadata.
|
|
56
|
+
- \`forbidden\` (default): Do not send \`task\`.
|
|
57
|
+
- \`optional\`: Send \`task\` only when durable polling or deferred retrieval is needed.
|
|
58
|
+
- \`required\`: Always send \`task\`.
|
|
59
|
+
- Poll status via \`tasks/get\`, retrieve final payload via \`tasks/result\`.
|
|
60
|
+
- Pass \`_meta.progressToken\` in \`tools/call\` to receive \`notifications/progress\`.
|
|
64
61
|
`;
|
|
65
62
|
}
|
|
66
|
-
const INSTRUCTIONS_FOOTER =
|
|
63
|
+
const INSTRUCTIONS_FOOTER = `## Constraints
|
|
64
|
+
|
|
67
65
|
${getSharedConstraints()
|
|
68
66
|
.map((c) => `- ${c}`)
|
|
69
67
|
.join('\n')}
|
|
70
|
-
</constraints>
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
## Error Recovery
|
|
70
|
+
|
|
71
|
+
| Error Code | Action |
|
|
72
|
+
| ------------------- | ------------------------------------------------------------------------- |
|
|
73
|
+
| \`ACCESS_DENIED\` | Run \`roots\` to list allowed directories, retry with a valid path. |
|
|
74
|
+
| \`NOT_FOUND\` | Run \`ls\` or \`find\` to verify the path. |
|
|
75
|
+
| \`TOO_LARGE\` | Use \`head\`/\`tail\`, line ranges, or split across \`read_many\`. |
|
|
76
|
+
| \`TIMEOUT\` | Reduce scope, depth, or maxResults. |
|
|
77
|
+
| \`INVALID_INPUT\` | Re-read tool contract via \`internal://tool-info/{name}\`. |
|
|
78
78
|
`;
|
|
79
79
|
function formatToolSection(tool) {
|
|
80
80
|
const parts = [`### ${tool.name}\n${tool.description}`];
|
|
81
81
|
if (tool.nuances && tool.nuances.length > 0) {
|
|
82
|
-
parts.push(...tool.nuances.map((n) => `-
|
|
82
|
+
parts.push(...tool.nuances.map((n) => `- ${n}`));
|
|
83
83
|
}
|
|
84
84
|
if (tool.gotchas && tool.gotchas.length > 0) {
|
|
85
|
-
parts.push(...tool.gotchas.map((g) => `-
|
|
85
|
+
parts.push(...tool.gotchas.map((g) => `- ⚠ ${g}`));
|
|
86
86
|
}
|
|
87
87
|
return parts.join('\n');
|
|
88
88
|
}
|
|
@@ -94,9 +94,9 @@ export function buildServerInstructions() {
|
|
|
94
94
|
'',
|
|
95
95
|
buildToolCatalogDetailsOnly(),
|
|
96
96
|
'',
|
|
97
|
-
'
|
|
97
|
+
'## Tool Reference',
|
|
98
|
+
'',
|
|
98
99
|
toolSections,
|
|
99
|
-
'</tool_reference>',
|
|
100
100
|
'',
|
|
101
101
|
buildWorkflowGuide(),
|
|
102
102
|
'',
|