@j0hanz/filesystem-mcp 1.6.1 → 1.7.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/README.md +19 -17
- package/dist/lib/errors.js +20 -16
- package/dist/lib/file-operations/glob-engine.js +37 -20
- package/dist/lib/fs-helpers.js +7 -8
- package/dist/lib/path-policy.js +30 -14
- package/dist/prompts.js +1 -1
- package/dist/resources/generated-instructions.js +23 -22
- package/dist/resources/tool-catalog.js +2 -2
- package/dist/resources/tool-info.js +1 -1
- package/dist/resources/workflows.js +2 -3
- package/dist/resources.js +3 -3
- package/dist/server/bootstrap.js +1 -6
- package/dist/server/capabilities.js +4 -1
- package/dist/server/roots-manager.d.ts +2 -0
- package/dist/server/roots-manager.js +24 -6
- package/dist/tools/calculate-hash.js +6 -22
- package/dist/tools/read-multiple.js +34 -43
- package/dist/tools/replace-in-files.js +52 -56
- package/dist/tools/search-content.js +6 -27
- package/dist/tools/shared.d.ts +22 -0
- package/dist/tools/shared.js +69 -8
- package/dist/tools/stat-many.js +6 -24
- package/dist/tools/task-support.js +19 -0
- package/package.json +1 -3
package/README.md
CHANGED
|
@@ -474,10 +474,14 @@ Replace text in all files matching a glob. Replaces **all** occurrences per file
|
|
|
474
474
|
|
|
475
475
|
### Resources
|
|
476
476
|
|
|
477
|
-
| URI | Description
|
|
478
|
-
| :----------------------------- |
|
|
479
|
-
| `internal://instructions` | Usage guidance for models
|
|
480
|
-
| `
|
|
477
|
+
| URI | Description | MIME Type |
|
|
478
|
+
| :----------------------------- | :---------------------------------- | :----------------- |
|
|
479
|
+
| `internal://instructions` | Usage guidance for models | `text/markdown` |
|
|
480
|
+
| `internal://tool-catalog` | Tool routing and data-flow guide | `text/markdown` |
|
|
481
|
+
| `internal://workflows` | Explore/search/edit/patch workflows | `text/markdown` |
|
|
482
|
+
| `internal://tool-info/{name}` | Per-tool nuances and gotchas | `text/markdown` |
|
|
483
|
+
| `filesystem-mcp://metrics` | Live per-tool metrics snapshot | `application/json` |
|
|
484
|
+
| `filesystem-mcp://result/{id}` | Ephemeral cached large tool output | varies |
|
|
481
485
|
|
|
482
486
|
When a tool response includes a `resource_link`/`resourceUri`, treat it as authoritative for full payload retrieval and call `resources/read` with that URI.
|
|
483
487
|
|
|
@@ -565,7 +569,7 @@ Set `FS_CONTEXT_STRIP_STRUCTURED=1` to strip `structuredContent` from tool resul
|
|
|
565
569
|
- **Input limits**: Paths are bounded to 4,096 characters; patterns to 1,000 characters.
|
|
566
570
|
- **Atomic writes**: File writes use an atomic write-then-rename strategy to prevent partial writes.
|
|
567
571
|
- **Docker**: The container runs as a non-root user (`mcp`).
|
|
568
|
-
- **HTTP host binding**: The HTTP transport binds to `127.0.0.1` by default. Setting `FILESYSTEM_MCP_HTTP_HOST=0.0.0.0` binds to all network interfaces and exposes the server externally — only do this behind a trusted reverse proxy with `
|
|
572
|
+
- **HTTP host binding**: The HTTP transport binds to `127.0.0.1` by default. Setting `FILESYSTEM_MCP_HTTP_HOST=0.0.0.0` binds to all network interfaces and exposes the server externally — only do this behind a trusted reverse proxy with `FILESYSTEM_MCP_API_KEY` configured.
|
|
569
573
|
|
|
570
574
|
> [!IMPORTANT]
|
|
571
575
|
> All diagnostic output goes to `stderr`. Tool handlers must never write to `stdout`, as doing so would corrupt the stdio transport.
|
|
@@ -580,18 +584,16 @@ npm ci
|
|
|
580
584
|
|
|
581
585
|
### Scripts
|
|
582
586
|
|
|
583
|
-
| Script
|
|
584
|
-
|
|
|
585
|
-
| `dev`
|
|
586
|
-
| `dev:run`
|
|
587
|
-
| `build`
|
|
588
|
-
| `test`
|
|
589
|
-
| `
|
|
590
|
-
| `
|
|
591
|
-
| `
|
|
592
|
-
| `
|
|
593
|
-
| `format` | `prettier --write .` | Format all files |
|
|
594
|
-
| `type-check` | `node scripts/tasks.mjs type-check` | TypeScript type checking |
|
|
587
|
+
| Script | Command | Purpose |
|
|
588
|
+
| :----------- | :---------------------------------- | :---------------------------------- |
|
|
589
|
+
| `dev` | `tsc --watch` | Watch-mode TypeScript compilation |
|
|
590
|
+
| `dev:run` | `node --watch dist/index.js` | Run built server with file watching |
|
|
591
|
+
| `build` | `node scripts/tasks.mjs build` | Production build |
|
|
592
|
+
| `test` | `node scripts/tasks.mjs test` | Run full test suite |
|
|
593
|
+
| `lint` | `eslint .` | Lint source files |
|
|
594
|
+
| `lint:fix` | `eslint . --fix` | Auto-fix lint issues |
|
|
595
|
+
| `format` | `prettier --write .` | Format all files |
|
|
596
|
+
| `type-check` | `node scripts/tasks.mjs type-check` | TypeScript type checking |
|
|
595
597
|
|
|
596
598
|
### MCP Inspector
|
|
597
599
|
|
package/dist/lib/errors.js
CHANGED
|
@@ -125,15 +125,16 @@ function walkErrorChain(error, visitor) {
|
|
|
125
125
|
let current = error;
|
|
126
126
|
const visited = new Set();
|
|
127
127
|
while (current !== undefined && current !== null && !visited.has(current)) {
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
const visitedResult = visitor(current);
|
|
129
|
+
if (visitedResult !== undefined)
|
|
130
|
+
return visitedResult;
|
|
130
131
|
if (!isNativeError(current))
|
|
131
132
|
break;
|
|
132
133
|
visited.add(current);
|
|
133
134
|
const next = current.cause;
|
|
134
135
|
current = next;
|
|
135
136
|
}
|
|
136
|
-
return
|
|
137
|
+
return undefined;
|
|
137
138
|
}
|
|
138
139
|
function isAbortErrorSingle(error) {
|
|
139
140
|
if (!isNativeError(error))
|
|
@@ -144,7 +145,7 @@ function isAbortErrorSingle(error) {
|
|
|
144
145
|
return code === 'ABORT_ERR';
|
|
145
146
|
}
|
|
146
147
|
export function isAbortError(error) {
|
|
147
|
-
return walkErrorChain(error, isAbortErrorSingle);
|
|
148
|
+
return (walkErrorChain(error, (candidate) => isAbortErrorSingle(candidate) ? true : undefined) === true);
|
|
148
149
|
}
|
|
149
150
|
function isTimeoutErrorSingle(error) {
|
|
150
151
|
if (!isNativeError(error))
|
|
@@ -158,7 +159,7 @@ function isTimeoutErrorSingle(error) {
|
|
|
158
159
|
return message.includes('timed out') || message.includes('timeout');
|
|
159
160
|
}
|
|
160
161
|
export function isTimeoutLikeError(error) {
|
|
161
|
-
return walkErrorChain(error, isTimeoutErrorSingle);
|
|
162
|
+
return (walkErrorChain(error, (candidate) => isTimeoutErrorSingle(candidate) ? true : undefined) === true);
|
|
162
163
|
}
|
|
163
164
|
export class McpError extends Error {
|
|
164
165
|
code;
|
|
@@ -224,17 +225,20 @@ function classifyMessageError(error) {
|
|
|
224
225
|
return undefined;
|
|
225
226
|
}
|
|
226
227
|
function classifyError(error) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
228
|
+
let timeoutCode;
|
|
229
|
+
let fallbackCode;
|
|
230
|
+
const terminalCode = walkErrorChain(error, (candidate) => {
|
|
231
|
+
if (isAbortErrorSingle(candidate)) {
|
|
232
|
+
return ErrorCode.E_CANCELLED;
|
|
233
|
+
}
|
|
234
|
+
if (timeoutCode === undefined && isTimeoutErrorSingle(candidate)) {
|
|
235
|
+
timeoutCode = ErrorCode.E_TIMEOUT;
|
|
236
|
+
}
|
|
237
|
+
fallbackCode ??=
|
|
238
|
+
getDirectErrorCode(candidate) ?? classifyMessageError(candidate);
|
|
239
|
+
return undefined;
|
|
240
|
+
});
|
|
241
|
+
return terminalCode ?? timeoutCode ?? fallbackCode ?? ErrorCode.E_UNKNOWN;
|
|
238
242
|
}
|
|
239
243
|
export function createDetailedError(error, path, additionalDetails) {
|
|
240
244
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -106,40 +106,57 @@ function buildHiddenPatterns(normalizedPattern, maxDepth) {
|
|
|
106
106
|
function shouldUseGlobDirents(options) {
|
|
107
107
|
return !options.stats && !options.followSymbolicLinks;
|
|
108
108
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
throw new TypeError('globEntries: options must be an object');
|
|
113
|
-
}
|
|
114
|
-
const o = unknownOptions;
|
|
115
|
-
if (typeof o.cwd !== 'string') {
|
|
116
|
-
throw new TypeError('globEntries: options.cwd must be a string');
|
|
109
|
+
function assertOptionString(options, key) {
|
|
110
|
+
if (typeof options[key] !== 'string') {
|
|
111
|
+
throw new TypeError(`globEntries: options.${key} must be a string`);
|
|
117
112
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (!Array.isArray(o.excludePatterns)) {
|
|
113
|
+
}
|
|
114
|
+
function assertExcludePatternsOption(options) {
|
|
115
|
+
if (!Array.isArray(options.excludePatterns)) {
|
|
122
116
|
throw new TypeError('globEntries: options.excludePatterns must be an array');
|
|
123
117
|
}
|
|
124
|
-
for (const
|
|
125
|
-
if (typeof
|
|
118
|
+
for (const pattern of options.excludePatterns) {
|
|
119
|
+
if (typeof pattern !== 'string') {
|
|
126
120
|
throw new TypeError('globEntries: options.excludePatterns must contain only strings');
|
|
127
121
|
}
|
|
128
122
|
}
|
|
123
|
+
}
|
|
124
|
+
function assertBooleanOptions(options) {
|
|
129
125
|
for (const key of GLOB_BOOLEAN_OPTION_KEYS) {
|
|
130
|
-
if (typeof
|
|
126
|
+
if (typeof options[key] !== 'boolean') {
|
|
131
127
|
throw new TypeError(`globEntries: options.${key} must be a boolean`);
|
|
132
128
|
}
|
|
133
129
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
}
|
|
131
|
+
function assertOptionalMaxDepth(options) {
|
|
132
|
+
const { maxDepth } = options;
|
|
133
|
+
if (maxDepth === undefined)
|
|
134
|
+
return;
|
|
135
|
+
if (typeof maxDepth !== 'number' || !Number.isFinite(maxDepth)) {
|
|
136
|
+
throw new TypeError('globEntries: options.maxDepth must be a finite number');
|
|
138
137
|
}
|
|
139
|
-
|
|
138
|
+
}
|
|
139
|
+
function assertOptionalSuppressErrors(options) {
|
|
140
|
+
const { suppressErrors } = options;
|
|
141
|
+
if (suppressErrors === undefined)
|
|
142
|
+
return;
|
|
143
|
+
if (typeof suppressErrors !== 'boolean') {
|
|
140
144
|
throw new TypeError('globEntries: options.suppressErrors must be a boolean');
|
|
141
145
|
}
|
|
142
146
|
}
|
|
147
|
+
function assertOptionsShape(options) {
|
|
148
|
+
const unknownOptions = options;
|
|
149
|
+
if (unknownOptions === null || typeof unknownOptions !== 'object') {
|
|
150
|
+
throw new TypeError('globEntries: options must be an object');
|
|
151
|
+
}
|
|
152
|
+
const o = unknownOptions;
|
|
153
|
+
assertOptionString(o, 'cwd');
|
|
154
|
+
assertOptionString(o, 'pattern');
|
|
155
|
+
assertExcludePatternsOption(o);
|
|
156
|
+
assertBooleanOptions(o);
|
|
157
|
+
assertOptionalMaxDepth(o);
|
|
158
|
+
assertOptionalSuppressErrors(o);
|
|
159
|
+
}
|
|
143
160
|
function normalizeOptions(options) {
|
|
144
161
|
const cwd = path.resolve(options.cwd);
|
|
145
162
|
const normalizedPattern = normalizePattern(options.pattern, options.baseNameMatch);
|
package/dist/lib/fs-helpers.js
CHANGED
|
@@ -120,23 +120,22 @@ function createParallelAbortError() {
|
|
|
120
120
|
return createAbortError();
|
|
121
121
|
}
|
|
122
122
|
export async function processInParallel(items, processor, concurrency = PARALLEL_CONCURRENCY, signal) {
|
|
123
|
-
|
|
123
|
+
const itemCount = items.length;
|
|
124
|
+
if (itemCount === 0)
|
|
124
125
|
return { results: [], errors: [] };
|
|
125
126
|
const effectiveConcurrency = normalizeConcurrency(concurrency);
|
|
126
127
|
// Pre-allocate slots by index to guarantee input-order output.
|
|
127
|
-
const resultSlots = new Array(
|
|
128
|
+
const resultSlots = new Array(itemCount);
|
|
128
129
|
const errors = [];
|
|
129
130
|
if (signal?.aborted)
|
|
130
131
|
throw createParallelAbortError();
|
|
131
132
|
let nextIndex = 0;
|
|
132
133
|
const next = async () => {
|
|
133
|
-
while (nextIndex <
|
|
134
|
+
while (nextIndex < itemCount) {
|
|
134
135
|
if (signal?.aborted)
|
|
135
136
|
throw createParallelAbortError();
|
|
136
|
-
const index = nextIndex
|
|
137
|
-
|
|
138
|
-
if (index >= items.length)
|
|
139
|
-
break;
|
|
137
|
+
const index = nextIndex;
|
|
138
|
+
nextIndex += 1;
|
|
140
139
|
const item = items[index];
|
|
141
140
|
try {
|
|
142
141
|
const result = await processor(item);
|
|
@@ -154,7 +153,7 @@ export async function processInParallel(items, processor, concurrency = PARALLEL
|
|
|
154
153
|
}
|
|
155
154
|
}
|
|
156
155
|
};
|
|
157
|
-
const workerCount = Math.min(
|
|
156
|
+
const workerCount = Math.min(itemCount, effectiveConcurrency);
|
|
158
157
|
const workers = new Array(workerCount);
|
|
159
158
|
for (let index = 0; index < workerCount; index += 1) {
|
|
160
159
|
workers[index] = next();
|
package/dist/lib/path-policy.js
CHANGED
|
@@ -36,45 +36,61 @@ function compilePatterns(patterns) {
|
|
|
36
36
|
const normalized = normalizeForMatch(pattern);
|
|
37
37
|
const matchesPath = normalized.includes('/');
|
|
38
38
|
compiled.push({
|
|
39
|
-
raw: normalized,
|
|
40
39
|
globs: matchesPath ? compilePatternGlobs(normalized) : [normalized],
|
|
41
40
|
matchesPath,
|
|
42
41
|
});
|
|
43
42
|
}
|
|
44
43
|
return compiled;
|
|
45
44
|
}
|
|
46
|
-
|
|
47
|
-
const
|
|
45
|
+
function toPatternSet(patterns) {
|
|
46
|
+
const pathGlobs = new Set();
|
|
47
|
+
const nameGlobs = new Set();
|
|
48
|
+
for (const pattern of patterns) {
|
|
49
|
+
const target = pattern.matchesPath ? pathGlobs : nameGlobs;
|
|
50
|
+
for (const glob of pattern.globs) {
|
|
51
|
+
target.add(glob);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
pathGlobs: [...pathGlobs],
|
|
56
|
+
nameGlobs: [...nameGlobs],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const DENY_PATTERNS = toPatternSet(compilePatterns(SENSITIVE_FILE_DENYLIST));
|
|
60
|
+
const ALLOW_PATTERNS = toPatternSet(compilePatterns(SENSITIVE_FILE_ALLOWLIST));
|
|
48
61
|
function uniquePair(primary, secondary) {
|
|
49
62
|
if (!secondary || secondary === primary)
|
|
50
63
|
return [primary];
|
|
51
64
|
return [primary, secondary];
|
|
52
65
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
66
|
+
function matchesAnyGlobs(globs, candidates) {
|
|
67
|
+
if (globs.length === 0 || candidates.length === 0)
|
|
68
|
+
return false;
|
|
69
|
+
for (const candidate of candidates) {
|
|
70
|
+
for (const glob of globs) {
|
|
71
|
+
if (path.posix.matchesGlob(candidate, glob))
|
|
72
|
+
return true;
|
|
61
73
|
}
|
|
62
74
|
}
|
|
63
75
|
return false;
|
|
64
76
|
}
|
|
65
77
|
export function isSensitivePath(requestedPath, resolvedPath) {
|
|
66
|
-
if (DENY_PATTERNS.length === 0
|
|
78
|
+
if (DENY_PATTERNS.pathGlobs.length === 0 &&
|
|
79
|
+
DENY_PATTERNS.nameGlobs.length === 0) {
|
|
67
80
|
return false;
|
|
81
|
+
}
|
|
68
82
|
const normalizedRequested = normalizeForMatch(requestedPath);
|
|
69
83
|
const normalizedResolved = resolvedPath
|
|
70
84
|
? normalizeForMatch(resolvedPath)
|
|
71
85
|
: undefined;
|
|
72
86
|
const pathCandidates = uniquePair(normalizedRequested, normalizedResolved);
|
|
73
87
|
const nameCandidates = uniquePair(path.posix.basename(normalizedRequested), normalizedResolved ? path.posix.basename(normalizedResolved) : undefined);
|
|
74
|
-
if (
|
|
88
|
+
if (matchesAnyGlobs(ALLOW_PATTERNS.pathGlobs, pathCandidates) ||
|
|
89
|
+
matchesAnyGlobs(ALLOW_PATTERNS.nameGlobs, nameCandidates)) {
|
|
75
90
|
return false;
|
|
76
91
|
}
|
|
77
|
-
return
|
|
92
|
+
return (matchesAnyGlobs(DENY_PATTERNS.pathGlobs, pathCandidates) ||
|
|
93
|
+
matchesAnyGlobs(DENY_PATTERNS.nameGlobs, nameCandidates));
|
|
78
94
|
}
|
|
79
95
|
export function assertAllowedFileAccess(requestedPath, resolvedPath) {
|
|
80
96
|
if (!isSensitivePath(requestedPath, resolvedPath))
|
package/dist/prompts.js
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { withDefaultIcons } from './tools/shared.js';
|
|
3
3
|
const HELP_PROMPT_NAME = 'get-help';
|
|
4
4
|
const HELP_PROMPT_TITLE = 'Get Help';
|
|
5
|
-
const HELP_PROMPT_DESCRIPTION = '
|
|
5
|
+
const HELP_PROMPT_DESCRIPTION = 'Retrieve the full filesystem-mcp XML usage guide.';
|
|
6
6
|
function filterInstructionsByTopic(instructions, topic) {
|
|
7
7
|
const normalized = topic.trim().toLowerCase();
|
|
8
8
|
if (!normalized)
|
|
@@ -1,46 +1,48 @@
|
|
|
1
1
|
import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
|
|
2
2
|
import { buildCoreContextPack, getSharedConstraints, getToolContracts, } from './tool-info.js';
|
|
3
3
|
import { buildWorkflowGuide } from './workflows.js';
|
|
4
|
-
const INSTRUCTIONS_HEADER =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## TOOLS
|
|
4
|
+
const INSTRUCTIONS_HEADER = `<role>
|
|
5
|
+
Expert filesystem agent. Operate ONLY within allowed roots. Always discover before acting — never guess paths.
|
|
6
|
+
</role>
|
|
9
7
|
|
|
8
|
+
<tools_overview>
|
|
10
9
|
| Category | Tools |
|
|
11
10
|
|----------|-------|
|
|
12
11
|
| Navigate | \`roots\`, \`ls\`, \`tree\`, \`find\` |
|
|
13
12
|
| Inspect | \`stat\`, \`stat_many\`, \`grep\`, \`calculate_hash\` |
|
|
14
13
|
| Read | \`read\`, \`read_many\`, \`diff_files\` |
|
|
15
14
|
| Write | \`mkdir\`, \`write\`, \`edit\`, \`mv\`, \`rm\`, \`apply_patch\`, \`search_and_replace\` |
|
|
15
|
+
</tools_overview>
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- \`
|
|
20
|
-
- \`
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
<resources>
|
|
18
|
+
- \`internal://instructions\`: Full server usage guide.
|
|
19
|
+
- \`internal://tool-catalog\`: Tool routing and cross-tool data-flow guide.
|
|
20
|
+
- \`internal://workflows\`: Standard operating sequences (explore/search/edit/patch).
|
|
21
|
+
- \`internal://tool-info/{name}\`: Per-tool details (nuances/gotchas), e.g. \`internal://tool-info/read\`.
|
|
22
|
+
- \`filesystem-mcp://result/{id}\`: Large output cache. Call \`resources/read\` immediately if \`resourceUri\` is returned.
|
|
23
|
+
- \`filesystem-mcp://metrics\`: Live per-tool stats.
|
|
24
|
+
</resources>
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
<task_protocol>
|
|
27
|
+
Async execution: provide \`_meta.progressToken\` in \`tools/call\`, poll \`tasks/get\`, call \`tasks/result\`.
|
|
25
28
|
Task-capable: \`find\`, \`tree\`, \`read\`, \`read_many\`, \`stat_many\`, \`grep\`, \`mkdir\`, \`write\`, \`mv\`, \`rm\`, \`calculate_hash\`, \`apply_patch\`, \`search_and_replace\`.
|
|
26
|
-
|
|
29
|
+
</task_protocol>
|
|
27
30
|
`;
|
|
28
|
-
const INSTRUCTIONS_FOOTER =
|
|
29
|
-
## CONSTRAINTS
|
|
30
|
-
|
|
31
|
+
const INSTRUCTIONS_FOOTER = `<constraints>
|
|
31
32
|
${getSharedConstraints()
|
|
32
33
|
.map((c) => `- ${c}`)
|
|
33
34
|
.join('\n')}
|
|
35
|
+
</constraints>
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
<error_handling>
|
|
37
38
|
- \`E_ACCESS_DENIED\` → Call \`roots\`; use allowed path.
|
|
38
39
|
- \`E_NOT_FOUND\` → Call \`ls\`/\`find\`; verify spelling.
|
|
39
40
|
- \`E_TOO_LARGE\` → Use range/head or \`read_many\`.
|
|
40
41
|
- \`E_TIMEOUT\` → Reduce scope or result limits.
|
|
42
|
+
</error_handling>
|
|
41
43
|
`;
|
|
42
44
|
function formatToolSection(tool) {
|
|
43
|
-
const parts = [
|
|
45
|
+
const parts = [`### ${tool.name}\n${tool.description}`];
|
|
44
46
|
if (tool.nuances && tool.nuances.length > 0) {
|
|
45
47
|
parts.push(...tool.nuances.map((n) => `» ${n}`));
|
|
46
48
|
}
|
|
@@ -57,13 +59,12 @@ export function buildServerInstructions() {
|
|
|
57
59
|
'',
|
|
58
60
|
buildToolCatalogDetailsOnly(),
|
|
59
61
|
'',
|
|
60
|
-
'
|
|
61
|
-
'',
|
|
62
|
+
'<tool_reference>',
|
|
62
63
|
toolSections,
|
|
64
|
+
'</tool_reference>',
|
|
63
65
|
'',
|
|
64
66
|
buildWorkflowGuide(),
|
|
65
67
|
'',
|
|
66
|
-
'---',
|
|
67
68
|
INSTRUCTIONS_FOOTER,
|
|
68
69
|
].join('\n');
|
|
69
70
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { buildCoreContextPack } from './tool-info.js';
|
|
2
|
-
const CATALOG_GUIDE =
|
|
3
|
-
|
|
2
|
+
const CATALOG_GUIDE = `<tool_selection_guide>
|
|
4
3
|
## Cross-Tool Data Flow
|
|
5
4
|
|
|
6
5
|
\`\`\`
|
|
@@ -25,6 +24,7 @@ diff_files (patch text) -> apply_patch.patch
|
|
|
25
24
|
- Always generate a patch with \`diff_files\` first.
|
|
26
25
|
- Always use \`dryRun: true\` with \`apply_patch\` to verify changes.
|
|
27
26
|
- \`apply_patch\` works on unified diff format.
|
|
27
|
+
</tool_selection_guide>
|
|
28
28
|
`;
|
|
29
29
|
export function buildToolCatalog() {
|
|
30
30
|
return `${buildCoreContextPack()}\n\n${CATALOG_GUIDE}`;
|
|
@@ -33,7 +33,7 @@ export function buildCoreContextPack() {
|
|
|
33
33
|
const annotations = e.annotations ? ` ${e.annotations.join(' ')}` : '';
|
|
34
34
|
return `| \`${e.name}\` | ${e.description}${annotations} |`;
|
|
35
35
|
});
|
|
36
|
-
return
|
|
36
|
+
return `<core_context>\n| Tool | Purpose |\n|------|---------|\n${rows.join('\n')}\n</core_context>`;
|
|
37
37
|
}
|
|
38
38
|
export function getSharedConstraints() {
|
|
39
39
|
return [
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export function buildWorkflowGuide() {
|
|
2
|
-
return
|
|
3
|
-
|
|
2
|
+
return `<workflows>
|
|
4
3
|
### A: EXPLORE
|
|
5
4
|
Use when: navigating an unfamiliar directory or reading file content.
|
|
6
5
|
1. \`roots\` (List allowed paths).
|
|
@@ -30,5 +29,5 @@ Use when: applying structured diffs produced by \`diff_files\`.
|
|
|
30
29
|
2. \`apply_patch\` (dryRun: true).
|
|
31
30
|
3. \`apply_patch\` (dryRun: false).
|
|
32
31
|
> **Tip:** Pass \`diff_files\` output directly into \`apply_patch\`.
|
|
33
|
-
|
|
32
|
+
</workflows>`;
|
|
34
33
|
}
|
package/dist/resources.js
CHANGED
|
@@ -21,7 +21,7 @@ const TOOL_INFO_RESOURCE_NAME = 'filesystem-mcp-tool-info';
|
|
|
21
21
|
const TOOL_INFO_RESOURCE_DESCRIPTION = 'Per-tool contract details, nuances, and gotchas. Read internal://tool-info/{name} with a tool name such as "read", "ls", or "grep".';
|
|
22
22
|
const INSTRUCTIONS_RESOURCE_NAME = 'filesystem-mcp-instructions';
|
|
23
23
|
const INSTRUCTIONS_RESOURCE_URI = 'internal://instructions';
|
|
24
|
-
const INSTRUCTIONS_RESOURCE_DESCRIPTION = '
|
|
24
|
+
const INSTRUCTIONS_RESOURCE_DESCRIPTION = 'Comprehensive rules and guidelines for filesystem-mcp usage.';
|
|
25
25
|
const RESULT_RESOURCE_NAME = 'filesystem-mcp-result';
|
|
26
26
|
const RESULT_RESOURCE_DESCRIPTION = 'Ephemeral cached tool output exposed as an MCP resource. Not guaranteed to be listed via resources/list.';
|
|
27
27
|
const METRICS_RESOURCE_NAME = 'filesystem-mcp-metrics';
|
|
@@ -29,10 +29,10 @@ const METRICS_RESOURCE_URI = 'filesystem-mcp://metrics';
|
|
|
29
29
|
const METRICS_RESOURCE_DESCRIPTION = 'Live per-tool call/error/avgDurationMs metrics snapshot.';
|
|
30
30
|
const CATALOG_RESOURCE_NAME = 'filesystem-mcp-catalog';
|
|
31
31
|
const CATALOG_RESOURCE_URI = 'internal://tool-catalog';
|
|
32
|
-
const CATALOG_RESOURCE_DESCRIPTION = '
|
|
32
|
+
const CATALOG_RESOURCE_DESCRIPTION = 'Tool selection guide and data flow map.';
|
|
33
33
|
const WORKFLOW_RESOURCE_NAME = 'filesystem-mcp-workflows';
|
|
34
34
|
const WORKFLOW_RESOURCE_URI = 'internal://workflows';
|
|
35
|
-
const WORKFLOW_RESOURCE_DESCRIPTION = '
|
|
35
|
+
const WORKFLOW_RESOURCE_DESCRIPTION = 'Standard operating procedures for exploration, search, edit, and patch.';
|
|
36
36
|
export function registerInstructionResource(server, instructions, iconInfo) {
|
|
37
37
|
server.registerResource(INSTRUCTIONS_RESOURCE_NAME, INSTRUCTIONS_RESOURCE_URI, withDefaultIcons({
|
|
38
38
|
title: 'Server Instructions',
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -59,9 +59,7 @@ export async function createServer(options = {}) {
|
|
|
59
59
|
}),
|
|
60
60
|
};
|
|
61
61
|
if (taskToolSupport) {
|
|
62
|
-
//
|
|
63
|
-
// lifetime. In HTTP mode this may grow unboundedly for long-lived servers.
|
|
64
|
-
// Use a custom TaskStore with eviction for production HTTP deployments.
|
|
62
|
+
// Enabling task tool support requires configuring a task store and message queue on the server config. We use in-memory implementations which are suitable for short-lived stdio sessions. Long-running HTTP servers should replace these with TTL-evicting implementations to avoid unbounded memory growth.
|
|
65
63
|
serverConfig.taskStore = new InMemoryTaskStore();
|
|
66
64
|
serverConfig.taskMessageQueue = new InMemoryTaskMessageQueue();
|
|
67
65
|
}
|
|
@@ -171,9 +169,6 @@ async function createHttpSession(options, sessions) {
|
|
|
171
169
|
sessions.set(sessionId, { server: mcpServer, transport });
|
|
172
170
|
rootsManager.logMissingDirectoriesIfNeeded(mcpServer);
|
|
173
171
|
},
|
|
174
|
-
onsessionclosed: (sessionId) => {
|
|
175
|
-
sessions.delete(sessionId);
|
|
176
|
-
},
|
|
177
172
|
});
|
|
178
173
|
transport.onclose = () => {
|
|
179
174
|
const { sessionId } = transport;
|
|
@@ -5,13 +5,16 @@ function detectTaskToolSupport() {
|
|
|
5
5
|
return cachedTaskToolSupport;
|
|
6
6
|
}
|
|
7
7
|
try {
|
|
8
|
+
// Instantiate a minimal, unconnected probe server to duck-type check for
|
|
9
|
+
// task tool support. The probe has no transport or active connections, so
|
|
10
|
+
// close() only releases in-memory state; fire-and-forget is safe here.
|
|
8
11
|
const probe = new McpServer({
|
|
9
12
|
name: 'filesystem-mcp-capability-probe',
|
|
10
13
|
version: '0.0.0',
|
|
11
14
|
}, { capabilities: { tools: {} } });
|
|
12
15
|
cachedTaskToolSupport =
|
|
13
16
|
typeof probe.experimental.tasks.registerToolTask === 'function';
|
|
14
|
-
|
|
17
|
+
probe.close().catch(() => { });
|
|
15
18
|
}
|
|
16
19
|
catch {
|
|
17
20
|
cachedTaskToolSupport = false;
|
|
@@ -5,6 +5,8 @@ export declare class RootsManager {
|
|
|
5
5
|
private rootsUpdateTimeout;
|
|
6
6
|
private rootDirectories;
|
|
7
7
|
private clientInitialized;
|
|
8
|
+
private updatingRoots;
|
|
9
|
+
private pendingRootsUpdate;
|
|
8
10
|
private readonly options;
|
|
9
11
|
readonly loggingState: LoggingState;
|
|
10
12
|
constructor(options: ServerOptions, loggingState: LoggingState);
|
|
@@ -84,6 +84,10 @@ export class RootsManager {
|
|
|
84
84
|
rootsUpdateTimeout;
|
|
85
85
|
rootDirectories = [];
|
|
86
86
|
clientInitialized = false;
|
|
87
|
+
// Set to true when an update is in progress, to prevent concurrent executions. If a change arrives while true, we queue a single retry after completion to ensure the last-known state is applied. This
|
|
88
|
+
updatingRoots = false;
|
|
89
|
+
// If an update is in progress and a change arrives, we set this flag to ensure we run another update after completion to apply the latest state
|
|
90
|
+
pendingRootsUpdate = false;
|
|
87
91
|
options;
|
|
88
92
|
loggingState;
|
|
89
93
|
constructor(options, loggingState) {
|
|
@@ -151,23 +155,37 @@ export class RootsManager {
|
|
|
151
155
|
logToMcp(server, 'warning', 'No allowed directories specified. Please provide directories as command-line arguments or enable --allow-cwd to use the current working directory.', this.loggingState.minimumLevel);
|
|
152
156
|
}
|
|
153
157
|
async updateRootsFromClient(server) {
|
|
158
|
+
// Guard against concurrent executions: if one is already running, queue a
|
|
159
|
+
// single retry so the last-known state is always applied after completion.
|
|
160
|
+
if (this.updatingRoots) {
|
|
161
|
+
this.pendingRootsUpdate = true;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.updatingRoots = true;
|
|
154
165
|
try {
|
|
155
166
|
const clientCapabilities = server.server.getClientCapabilities();
|
|
156
167
|
if (!clientCapabilities?.roots) {
|
|
157
168
|
this.rootDirectories = [];
|
|
158
|
-
return;
|
|
159
169
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
else {
|
|
171
|
+
const rootsResult = await server.server.listRoots(undefined, {
|
|
172
|
+
timeout: ROOTS_TIMEOUT_MS,
|
|
173
|
+
});
|
|
174
|
+
const roots = extractRoots(rootsResult);
|
|
175
|
+
this.rootDirectories = await resolveRootDirectories(roots);
|
|
176
|
+
}
|
|
165
177
|
}
|
|
166
178
|
catch (error) {
|
|
167
179
|
logToMcp(server, 'debug', `[DEBUG] MCP Roots protocol unavailable or failed: ${formatUnknownErrorMessage(error)}`, this.loggingState.minimumLevel);
|
|
168
180
|
}
|
|
169
181
|
finally {
|
|
170
182
|
await this.recomputeAllowedDirectories();
|
|
183
|
+
this.updatingRoots = false;
|
|
184
|
+
// If a change arrived while we were running, apply it now.
|
|
185
|
+
if (this.pendingRootsUpdate) {
|
|
186
|
+
this.pendingRootsUpdate = false;
|
|
187
|
+
void this.updateRootsFromClient(server);
|
|
188
|
+
}
|
|
171
189
|
}
|
|
172
190
|
}
|
|
173
191
|
}
|