@j0hanz/filesystem-mcp 1.5.1 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.js +5 -0
- package/dist/lib/fs-helpers.js +1 -1
- package/dist/lib/observability.js +3 -6
- package/dist/prompts.js +8 -1
- package/dist/resources/generated-instructions.js +16 -31
- package/dist/resources/tool-catalog.js +10 -5
- package/dist/resources/tool-info.js +1 -1
- package/dist/resources/workflows.js +6 -8
- package/dist/resources.js +2 -2
- package/dist/schemas.js +6 -2
- package/dist/server/bootstrap.js +49 -8
- package/dist/server/logging.js +1 -1
- package/dist/server/roots-manager.js +1 -1
- package/dist/tools/create-directory.js +2 -1
- package/dist/tools/delete-file.js +2 -1
- package/dist/tools/move-file.js +3 -0
- package/dist/tools/search-content.js +1 -1
- package/dist/tools/shared.js +2 -5
- package/dist/tools/stat-many.js +1 -1
- package/dist/tools/stat.js +1 -1
- package/dist/tools/write-file.js +2 -5
- package/dist/tools.js +31 -41
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -195,7 +195,7 @@ export async function parseArgs() {
|
|
|
195
195
|
throw error;
|
|
196
196
|
}
|
|
197
197
|
const options = cli.opts();
|
|
198
|
-
const allowCwd = options.allowCwd
|
|
198
|
+
const allowCwd = Boolean(options.allowCwd);
|
|
199
199
|
const port = parsePortOption(options.port);
|
|
200
200
|
const positionals = getParsedAllowedDirs(cli);
|
|
201
201
|
let allowedDirs;
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function parseTrueEnvFlag(value: string | undefined): boolean;
|
|
1
2
|
export declare const DEFAULT_LOG_LEVEL: "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency";
|
|
2
3
|
export declare const PARALLEL_CONCURRENCY: number;
|
|
3
4
|
export declare const MAX_SEARCHABLE_FILE_SIZE: number;
|
package/dist/lib/constants.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { availableParallelism } from 'node:os';
|
|
2
2
|
const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes', 'y', 'on']);
|
|
3
3
|
const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'n', 'off']);
|
|
4
|
+
export function parseTrueEnvFlag(value) {
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
return false;
|
|
7
|
+
return TRUE_ENV_VALUES.has(value.trim().toLowerCase());
|
|
8
|
+
}
|
|
4
9
|
const KIB = 1024;
|
|
5
10
|
const MIB = 1024 * KIB;
|
|
6
11
|
function logInvalidEnvValue(envVar, value, expected, defaultValue) {
|
package/dist/lib/fs-helpers.js
CHANGED
|
@@ -419,7 +419,7 @@ async function readRangeContent(handle, startLine, endLine, options) {
|
|
|
419
419
|
if (hasEndLine && lineNumber === stopAt) {
|
|
420
420
|
const peek = await iterator.next();
|
|
421
421
|
hasMoreLines = !peek.done;
|
|
422
|
-
reachedEof = peek.done
|
|
422
|
+
reachedEof = Boolean(peek.done);
|
|
423
423
|
stoppedEarly = true;
|
|
424
424
|
break;
|
|
425
425
|
}
|
|
@@ -2,21 +2,18 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
2
2
|
import { hash } from 'node:crypto';
|
|
3
3
|
import { channel, tracingChannel } from 'node:diagnostics_channel';
|
|
4
4
|
import { monitorEventLoopDelay, performance, PerformanceObserver, } from 'node:perf_hooks';
|
|
5
|
+
import { parseTrueEnvFlag } from './constants.js';
|
|
5
6
|
import { isRecord } from './type-guards.js';
|
|
6
7
|
// --- Configuration ---
|
|
7
8
|
const ENV = process.env;
|
|
8
9
|
let _cachedConfig;
|
|
9
10
|
function readConfig() {
|
|
10
11
|
return (_cachedConfig ??= {
|
|
11
|
-
enabled:
|
|
12
|
+
enabled: parseTrueEnvFlag(ENV['FS_CONTEXT_DIAGNOSTICS']),
|
|
12
13
|
detail: parseDetail(ENV['FS_CONTEXT_DIAGNOSTICS_DETAIL']),
|
|
13
|
-
logToolErrors:
|
|
14
|
+
logToolErrors: parseTrueEnvFlag(ENV['FS_CONTEXT_TOOL_LOG_ERRORS']),
|
|
14
15
|
});
|
|
15
16
|
}
|
|
16
|
-
function isTrue(val) {
|
|
17
|
-
const norm = val?.trim().toLowerCase();
|
|
18
|
-
return norm === '1' || norm === 'true' || norm === 'yes';
|
|
19
|
-
}
|
|
20
17
|
function parseDetail(val) {
|
|
21
18
|
if (val === '2')
|
|
22
19
|
return 2;
|
package/dist/prompts.js
CHANGED
|
@@ -9,7 +9,14 @@ function filterInstructionsByTopic(instructions, topic) {
|
|
|
9
9
|
return instructions;
|
|
10
10
|
const sections = instructions.split(/\n(?=## )/u);
|
|
11
11
|
const match = sections.find((sec) => sec.toLowerCase().startsWith(`## ${normalized}`));
|
|
12
|
-
|
|
12
|
+
if (match !== undefined)
|
|
13
|
+
return match;
|
|
14
|
+
const available = sections
|
|
15
|
+
.filter((sec) => sec.startsWith('## '))
|
|
16
|
+
.map((sec) => sec.split('\n')[0]?.replace(/^##\s*/u, '') ?? '')
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join(', ');
|
|
19
|
+
return `Section '${topic}' not found. Available sections: ${available}\n\n${instructions}`;
|
|
13
20
|
}
|
|
14
21
|
export function registerGetHelpPrompt(server, instructions, iconInfo) {
|
|
15
22
|
const baseConfig = withDefaultIcons({ title: HELP_PROMPT_TITLE, description: HELP_PROMPT_DESCRIPTION }, iconInfo);
|
|
@@ -1,32 +1,28 @@
|
|
|
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 = `# FILESYSTEM-MCP
|
|
4
|
+
const INSTRUCTIONS_HEADER = `# FILESYSTEM-MCP
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Operate ONLY within allowed roots. Always discover before acting — never guess paths.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## TOOLS
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
| Category | Tools |
|
|
11
|
+
|----------|-------|
|
|
12
|
+
| Navigate | \`roots\`, \`ls\`, \`tree\`, \`find\` |
|
|
13
|
+
| Inspect | \`stat\`, \`stat_many\`, \`grep\`, \`calculate_hash\` |
|
|
14
|
+
| Read | \`read\`, \`read_many\`, \`diff_files\` |
|
|
15
|
+
| Write | \`mkdir\`, \`write\`, \`edit\`, \`mv\`, \`rm\`, \`apply_patch\`, \`search_and_replace\` |
|
|
14
16
|
|
|
15
17
|
## RESOURCES
|
|
16
18
|
|
|
17
|
-
- \`filesystem-mcp://result/{id}\`:
|
|
18
|
-
- \`filesystem-mcp://metrics\`: Live tool stats.
|
|
19
|
-
- **Tip:** If response has \`resourceUri\`, call \`resources/read\` to fetch full content.
|
|
19
|
+
- \`filesystem-mcp://result/{id}\`: Large output is cached here. **If a response includes \`resourceUri\`, call \`resources/read\` immediately — results expire on process restart.**
|
|
20
|
+
- \`filesystem-mcp://metrics\`: Live per-tool call/error stats.
|
|
20
21
|
|
|
21
|
-
##
|
|
22
|
+
## TASK PROTOCOL
|
|
22
23
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
- Flow: \`tools/call\` (task) → \`tasks/get\` → \`tasks/result\`.
|
|
26
|
-
|
|
27
|
-
## GOLDEN PATH WORKFLOWS
|
|
28
|
-
|
|
29
|
-
See "Workflow Reference" below for detailed execution sequences.
|
|
24
|
+
Long-running tools support async execution: provide \`_meta.progressToken\` in \`tools/call\`, poll \`tasks/get\`, then call \`tasks/result\`.
|
|
25
|
+
Task-capable: \`find\`, \`tree\`, \`read\`, \`read_many\`, \`stat_many\`, \`grep\`, \`mkdir\`, \`write\`, \`mv\`, \`rm\`, \`calculate_hash\`, \`apply_patch\`, \`search_and_replace\`.
|
|
30
26
|
|
|
31
27
|
`;
|
|
32
28
|
const INSTRUCTIONS_FOOTER = `
|
|
@@ -45,22 +41,11 @@ ${getSharedConstraints()
|
|
|
45
41
|
`;
|
|
46
42
|
function formatToolSection(tool) {
|
|
47
43
|
const parts = [`${tool.name}: ${tool.description}`];
|
|
48
|
-
if (tool.annotations) {
|
|
49
|
-
const attrs = [];
|
|
50
|
-
if (tool.annotations.destructiveHint)
|
|
51
|
-
attrs.push('[Destructive]');
|
|
52
|
-
if (tool.annotations.idempotentHint)
|
|
53
|
-
attrs.push('[Idempotent]');
|
|
54
|
-
if (tool.annotations.readOnlyHint)
|
|
55
|
-
attrs.push('[Read-Only]');
|
|
56
|
-
if (attrs.length > 0)
|
|
57
|
-
parts.push(attrs.join(' '));
|
|
58
|
-
}
|
|
59
44
|
if (tool.nuances && tool.nuances.length > 0) {
|
|
60
|
-
parts.push(...tool.nuances.map((n) =>
|
|
45
|
+
parts.push(...tool.nuances.map((n) => `» ${n}`));
|
|
61
46
|
}
|
|
62
47
|
if (tool.gotchas && tool.gotchas.length > 0) {
|
|
63
|
-
parts.push(...tool.gotchas.map((g) =>
|
|
48
|
+
parts.push(...tool.gotchas.map((g) => `⚠ ${g}`));
|
|
64
49
|
}
|
|
65
50
|
return parts.join('\n');
|
|
66
51
|
}
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { buildCoreContextPack } from './tool-info.js';
|
|
2
|
-
const CATALOG_GUIDE = `## Tool
|
|
2
|
+
const CATALOG_GUIDE = `## Tool Selection Guide
|
|
3
3
|
|
|
4
4
|
## Cross-Tool Data Flow
|
|
5
5
|
|
|
6
6
|
\`\`\`
|
|
7
|
-
find
|
|
8
|
-
diff_files
|
|
7
|
+
find (results[].path) -> grep.paths
|
|
8
|
+
diff_files (patch text) -> apply_patch.patch
|
|
9
9
|
\`\`\`
|
|
10
10
|
|
|
11
|
-
## Search Strategy
|
|
11
|
+
## Search Strategy
|
|
12
12
|
|
|
13
13
|
- Use \`find\` for glob-based file discovery.
|
|
14
14
|
- Use \`grep\` for content-based searches.
|
|
15
15
|
- Use \`search_and_replace\` ONLY for bulk replacements, not for discovery.
|
|
16
16
|
|
|
17
|
+
## Write Strategy
|
|
18
|
+
|
|
19
|
+
- Use \`edit\` for precise, single-occurrence string replacements in existing files.
|
|
20
|
+
- Use \`write\` to create new files or completely overwrite existing content.
|
|
21
|
+
- Use \`search_and_replace\` for bulk regex replacements across multiple files.
|
|
22
|
+
|
|
17
23
|
## Patch Management
|
|
18
24
|
|
|
19
25
|
- Always generate a patch with \`diff_files\` first.
|
|
@@ -21,7 +27,6 @@ diff_files -> output_patch -> apply_patch.patch
|
|
|
21
27
|
- \`apply_patch\` works on unified diff format.
|
|
22
28
|
`;
|
|
23
29
|
export function buildToolCatalog() {
|
|
24
|
-
// Return combined view for standalone resource usage
|
|
25
30
|
return `${buildCoreContextPack()}\n\n${CATALOG_GUIDE}`;
|
|
26
31
|
}
|
|
27
32
|
export function buildToolCatalogDetailsOnly() {
|
|
@@ -40,6 +40,6 @@ export function getSharedConstraints() {
|
|
|
40
40
|
'Allowed roots only (negotiated via CLI).',
|
|
41
41
|
'Sensitive files denylisted by default.',
|
|
42
42
|
`Max file size (${Math.floor(MAX_TEXT_FILE_SIZE / 1024 / 1024)}MB) & search results (${MAX_SEARCH_RESULTS} files, ${DEFAULT_SEARCH_CONTENT_RESULTS} lines) enforced.`,
|
|
43
|
-
'
|
|
43
|
+
'If a response includes `resourceUri`, call `resources/read` immediately — results expire on process restart.',
|
|
44
44
|
];
|
|
45
45
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { getSharedConstraints } from './tool-info.js';
|
|
2
1
|
export function buildWorkflowGuide() {
|
|
3
2
|
return `## Workflow Reference
|
|
4
3
|
|
|
5
4
|
### A: EXPLORE
|
|
5
|
+
Use when: navigating an unfamiliar directory or reading file content.
|
|
6
6
|
1. \`roots\` (List allowed paths).
|
|
7
7
|
2. \`ls\` (files) | \`tree\` (structure).
|
|
8
8
|
3. \`stat\` | \`stat_many\` (size/type check).
|
|
@@ -10,12 +10,14 @@ export function buildWorkflowGuide() {
|
|
|
10
10
|
> **Strict:** Never guess paths. Resolve first.
|
|
11
11
|
|
|
12
12
|
### B: SEARCH
|
|
13
|
+
Use when: locating files by name pattern or by content match.
|
|
13
14
|
1. \`find\` (glob candidates).
|
|
14
15
|
2. \`grep\` (content search).
|
|
15
16
|
3. \`read\` (verify context).
|
|
16
|
-
> **
|
|
17
|
+
> **Strict:** Use \`grep\` for content search, not \`find\`.
|
|
17
18
|
|
|
18
19
|
### C: EDIT
|
|
20
|
+
Use when: modifying existing files or reorganizing the filesystem.
|
|
19
21
|
1. \`edit\` (precise string match).
|
|
20
22
|
2. \`search_and_replace\` (bulk regex/glob).
|
|
21
23
|
3. \`mv\` | \`rm\` (file layout).
|
|
@@ -23,14 +25,10 @@ export function buildWorkflowGuide() {
|
|
|
23
25
|
> **Strict:** Confirm destructive ops (\`write\`, \`mv\`, \`rm\`, bulk replace).
|
|
24
26
|
|
|
25
27
|
### D: PATCH
|
|
28
|
+
Use when: applying structured diffs produced by \`diff_files\`.
|
|
26
29
|
1. \`diff_files\` (generate).
|
|
27
30
|
2. \`apply_patch\` (dryRun: true).
|
|
28
31
|
3. \`apply_patch\` (dryRun: false).
|
|
29
|
-
> **Tip:**
|
|
30
|
-
|
|
31
|
-
## Shared Constraints
|
|
32
|
-
${getSharedConstraints()
|
|
33
|
-
.map((c) => `- ${c}`)
|
|
34
|
-
.join('\n')}
|
|
32
|
+
> **Tip:** Pass \`diff_files\` output directly into \`apply_patch\`.
|
|
35
33
|
`;
|
|
36
34
|
}
|
package/dist/resources.js
CHANGED
|
@@ -47,7 +47,7 @@ export function registerToolCatalogResource(server, iconInfo) {
|
|
|
47
47
|
mimeType: 'text/markdown',
|
|
48
48
|
annotations: {
|
|
49
49
|
audience: ['assistant'],
|
|
50
|
-
priority: 0.
|
|
50
|
+
priority: 0.7,
|
|
51
51
|
},
|
|
52
52
|
}, iconInfo), (uri) => ({
|
|
53
53
|
contents: [
|
|
@@ -66,7 +66,7 @@ export function registerWorkflowGuideResource(server, iconInfo) {
|
|
|
66
66
|
mimeType: 'text/markdown',
|
|
67
67
|
annotations: {
|
|
68
68
|
audience: ['assistant'],
|
|
69
|
-
priority: 0.
|
|
69
|
+
priority: 0.6,
|
|
70
70
|
},
|
|
71
71
|
}, iconInfo), (uri) => ({
|
|
72
72
|
contents: [
|
package/dist/schemas.js
CHANGED
|
@@ -26,8 +26,12 @@ const RequiredPathSchema = PathSchemaBase.min(1, 'Path required');
|
|
|
26
26
|
const FileTypeSchema = z.enum(['file', 'directory', 'symlink', 'other']);
|
|
27
27
|
const ListDirectorySortSchema = z.enum(['name', 'size', 'modified', 'type']);
|
|
28
28
|
const SearchFilesSortSchema = z.enum(['name', 'size', 'modified', 'path']);
|
|
29
|
-
const SearchStopReasonSchema = z
|
|
30
|
-
|
|
29
|
+
const SearchStopReasonSchema = z
|
|
30
|
+
.enum(['maxResults', 'maxFiles', 'timeout'])
|
|
31
|
+
.describe('maxResults: result limit hit; maxFiles: file count limit hit; timeout: time limit exceeded');
|
|
32
|
+
const ListDirectoryStopReasonSchema = z
|
|
33
|
+
.enum(['maxEntries', 'aborted'])
|
|
34
|
+
.describe('maxEntries: entry limit hit; aborted: operation was cancelled');
|
|
31
35
|
const TreeEntrySchema = z.lazy(() => z.strictObject({
|
|
32
36
|
name: z.string().describe('Name'),
|
|
33
37
|
type: FileTypeSchema.describe('Type'),
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -28,9 +28,6 @@ function getRootsManager(server) {
|
|
|
28
28
|
}
|
|
29
29
|
return manager;
|
|
30
30
|
}
|
|
31
|
-
function loadServerInstructions() {
|
|
32
|
-
return buildServerInstructions();
|
|
33
|
-
}
|
|
34
31
|
async function getLocalIconInfo() {
|
|
35
32
|
const name = 'logo.svg';
|
|
36
33
|
const mime = 'image/svg+xml';
|
|
@@ -52,7 +49,7 @@ async function getLocalIconInfo() {
|
|
|
52
49
|
}
|
|
53
50
|
export async function createServer(options = {}) {
|
|
54
51
|
const resourceStore = createInMemoryResourceStore();
|
|
55
|
-
const serverInstructions =
|
|
52
|
+
const serverInstructions = buildServerInstructions();
|
|
56
53
|
const localIcon = await getLocalIconInfo();
|
|
57
54
|
const taskToolSupport = supportsTaskToolRequests();
|
|
58
55
|
const serverConfig = {
|
|
@@ -68,7 +65,7 @@ export async function createServer(options = {}) {
|
|
|
68
65
|
if (serverInstructions) {
|
|
69
66
|
serverConfig.instructions =
|
|
70
67
|
'filesystem-mcp: Secure local filesystem MCP server. ' +
|
|
71
|
-
'
|
|
68
|
+
'Always begin with: roots → ls/find → stat → read. Never guess paths. ' +
|
|
72
69
|
'Full reference: read the internal://instructions resource or invoke the get-help prompt.';
|
|
73
70
|
}
|
|
74
71
|
const server = new McpServer(withDefaultIcons({
|
|
@@ -113,13 +110,37 @@ export async function startServer(server) {
|
|
|
113
110
|
};
|
|
114
111
|
rootsManager.logMissingDirectoriesIfNeeded(server);
|
|
115
112
|
}
|
|
113
|
+
const MAX_REQUEST_BODY_BYTES = parseInt(process.env['FS_CONTEXT_MAX_REQUEST_BYTES'] ?? '', 10) ||
|
|
114
|
+
4 * 1024 * 1024; // 4 MB default
|
|
115
|
+
class RequestBodyError extends Error {
|
|
116
|
+
statusCode;
|
|
117
|
+
constructor(message, statusCode) {
|
|
118
|
+
super(message);
|
|
119
|
+
this.statusCode = statusCode;
|
|
120
|
+
this.name = 'RequestBodyError';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
116
123
|
async function readRequestBody(req) {
|
|
117
124
|
return new Promise((resolve, reject) => {
|
|
118
125
|
const chunks = [];
|
|
126
|
+
let totalBytes = 0;
|
|
127
|
+
let tooBig = false;
|
|
119
128
|
req.on('data', (chunk) => {
|
|
129
|
+
totalBytes += chunk.length;
|
|
130
|
+
if (totalBytes > MAX_REQUEST_BODY_BYTES) {
|
|
131
|
+
if (!tooBig) {
|
|
132
|
+
tooBig = true;
|
|
133
|
+
chunks.length = 0; // free accumulated memory
|
|
134
|
+
req.pause(); // stop emitting data events; TCP window fills naturally
|
|
135
|
+
reject(new RequestBodyError('Request body too large', 413));
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
120
139
|
chunks.push(chunk);
|
|
121
140
|
});
|
|
122
141
|
req.on('end', () => {
|
|
142
|
+
if (tooBig)
|
|
143
|
+
return; // already rejected in 'data' handler
|
|
123
144
|
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
124
145
|
if (!raw) {
|
|
125
146
|
resolve(undefined);
|
|
@@ -129,7 +150,7 @@ async function readRequestBody(req) {
|
|
|
129
150
|
resolve(JSON.parse(raw));
|
|
130
151
|
}
|
|
131
152
|
catch {
|
|
132
|
-
|
|
153
|
+
reject(new RequestBodyError('Invalid JSON in request body', 400));
|
|
133
154
|
}
|
|
134
155
|
});
|
|
135
156
|
req.on('error', reject);
|
|
@@ -171,11 +192,22 @@ function sendJsonRpcError(res, status, code, message) {
|
|
|
171
192
|
id: null,
|
|
172
193
|
}));
|
|
173
194
|
}
|
|
195
|
+
const LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/u;
|
|
196
|
+
function isAllowedOrigin(origin) {
|
|
197
|
+
if (origin === undefined)
|
|
198
|
+
return true; // Non-browser clients omit Origin.
|
|
199
|
+
return LOCALHOST_ORIGIN_RE.test(origin);
|
|
200
|
+
}
|
|
174
201
|
export async function startHttpServer(port, options) {
|
|
175
202
|
const sessions = new Map();
|
|
176
203
|
async function handleMcpRequest(req, res) {
|
|
177
204
|
const { method } = req;
|
|
178
205
|
const sessionId = req.headers['mcp-session-id'];
|
|
206
|
+
const { origin } = req.headers;
|
|
207
|
+
if (!isAllowedOrigin(origin)) {
|
|
208
|
+
sendJsonRpcError(res, 403, -32000, 'Forbidden: disallowed origin');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
179
211
|
const apiKey = process.env['FILESYSTEM_MCP_API_KEY'];
|
|
180
212
|
if (apiKey) {
|
|
181
213
|
const authHeader = req.headers['authorization'];
|
|
@@ -237,6 +269,12 @@ export async function startHttpServer(port, options) {
|
|
|
237
269
|
}
|
|
238
270
|
}
|
|
239
271
|
catch (error) {
|
|
272
|
+
if (error instanceof RequestBodyError && !res.headersSent) {
|
|
273
|
+
const rpcCode = error.statusCode === 413 ? -32600 : -32700;
|
|
274
|
+
res.setHeader('Connection', 'close');
|
|
275
|
+
sendJsonRpcError(res, error.statusCode, rpcCode, error.message);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
240
278
|
console.error('[HTTP] Error handling request:', formatUnknownErrorMessage(error));
|
|
241
279
|
if (!res.headersSent) {
|
|
242
280
|
sendJsonRpcError(res, 500, -32603, 'Internal Server Error');
|
|
@@ -255,10 +293,13 @@ export async function startHttpServer(port, options) {
|
|
|
255
293
|
res.end('Not Found');
|
|
256
294
|
}
|
|
257
295
|
});
|
|
296
|
+
// Default to localhost-only binding to prevent DNS-rebinding and unintended
|
|
297
|
+
// external exposure. Override with FILESYSTEM_MCP_HTTP_HOST for remote setups.
|
|
298
|
+
const httpHost = process.env['FILESYSTEM_MCP_HTTP_HOST'] ?? '127.0.0.1';
|
|
258
299
|
return new Promise((resolve, reject) => {
|
|
259
300
|
httpServer.once('error', reject);
|
|
260
|
-
httpServer.listen(port, () => {
|
|
261
|
-
console.error(`MCP HTTP server listening on
|
|
301
|
+
httpServer.listen(port, httpHost, () => {
|
|
302
|
+
console.error(`MCP HTTP server listening on ${httpHost}:${port}`);
|
|
262
303
|
resolve(httpServer);
|
|
263
304
|
});
|
|
264
305
|
});
|
package/dist/server/logging.js
CHANGED
|
@@ -20,7 +20,7 @@ function canSendMcpLogs(server) {
|
|
|
20
20
|
return false;
|
|
21
21
|
if (!('logging' in capabilities))
|
|
22
22
|
return false;
|
|
23
|
-
return capabilities
|
|
23
|
+
return !!capabilities['logging'];
|
|
24
24
|
}
|
|
25
25
|
export function logToMcp(server, level, data, minLevel = 'debug') {
|
|
26
26
|
if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[minLevel]) {
|
|
@@ -117,7 +117,7 @@ export class RootsManager {
|
|
|
117
117
|
}
|
|
118
118
|
async recomputeAllowedDirectories() {
|
|
119
119
|
const cliAllowedDirs = normalizeCLIDirectories(this.options.cliAllowedDirs ?? []);
|
|
120
|
-
const allowCwd = this.options.allowCwd
|
|
120
|
+
const allowCwd = Boolean(this.options.allowCwd);
|
|
121
121
|
const allowCwdDirs = allowCwd ? [normalizePath(process.cwd())] : [];
|
|
122
122
|
const baseline = [...cliAllowedDirs, ...allowCwdDirs];
|
|
123
123
|
const { signal, cleanup } = createTimedAbortSignal(undefined, ROOTS_TIMEOUT_MS);
|
|
@@ -9,10 +9,11 @@ import { registerToolTaskIfAvailable } from './task-support.js';
|
|
|
9
9
|
export const CREATE_DIRECTORY_TOOL = {
|
|
10
10
|
name: 'mkdir',
|
|
11
11
|
title: 'Create Directory',
|
|
12
|
-
description: 'Create a new directory at the specified path (recursive)',
|
|
12
|
+
description: 'Create a new directory at the specified path (recursive).',
|
|
13
13
|
inputSchema: CreateDirectoryInputSchema,
|
|
14
14
|
outputSchema: CreateDirectoryOutputSchema,
|
|
15
15
|
annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
|
|
16
|
+
nuances: ['Succeeds silently if the directory already exists (idempotent).'],
|
|
16
17
|
};
|
|
17
18
|
async function handleCreateDirectory(args, signal) {
|
|
18
19
|
const validPath = await validatePathForWrite(args.path, signal);
|
|
@@ -9,11 +9,12 @@ import { registerToolTaskIfAvailable } from './task-support.js';
|
|
|
9
9
|
export const DELETE_FILE_TOOL = {
|
|
10
10
|
name: 'rm',
|
|
11
11
|
title: 'Delete File',
|
|
12
|
-
description: '
|
|
12
|
+
description: 'Permanently delete a file or directory. This action is irreversible.',
|
|
13
13
|
inputSchema: DeleteFileInputSchema,
|
|
14
14
|
outputSchema: DeleteFileOutputSchema,
|
|
15
15
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
16
16
|
gotchas: [
|
|
17
|
+
'Deletion is permanent — there is no undo or recycle bin.',
|
|
17
18
|
'Non-empty directory delete requires `recursive=true`; else returns actionable input error.',
|
|
18
19
|
],
|
|
19
20
|
};
|
package/dist/tools/move-file.js
CHANGED
|
@@ -14,6 +14,9 @@ export const MOVE_FILE_TOOL = {
|
|
|
14
14
|
outputSchema: MoveFileOutputSchema,
|
|
15
15
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
16
16
|
nuances: ['Cross-device moves fall back to copy+delete.'],
|
|
17
|
+
gotchas: [
|
|
18
|
+
'On POSIX, an existing destination is silently overwritten; on Windows, rename fails with EEXIST if destination exists.',
|
|
19
|
+
],
|
|
17
20
|
};
|
|
18
21
|
async function handleMoveFile(args, signal) {
|
|
19
22
|
const validSource = await validateExistingPath(args.source, signal);
|
|
@@ -24,7 +24,7 @@ export const SEARCH_CONTENT_TOOL = {
|
|
|
24
24
|
'Skips binary and oversized files.',
|
|
25
25
|
],
|
|
26
26
|
gotchas: [
|
|
27
|
-
'
|
|
27
|
+
'Skips binary and oversized files silently — check file type with `stat` if no matches appear.',
|
|
28
28
|
],
|
|
29
29
|
taskSupport: 'required',
|
|
30
30
|
};
|
package/dist/tools/shared.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { channel } from 'node:diagnostics_channel';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { parseTrueEnvFlag } from '../lib/constants.js';
|
|
3
4
|
import { createDetailedError, ErrorCode, formatDetailedError, getSuggestion, McpError, } from '../lib/errors.js';
|
|
4
5
|
import { createTimedAbortSignal } from '../lib/fs-helpers.js';
|
|
5
6
|
import { withToolDiagnostics } from '../lib/observability.js';
|
|
@@ -8,7 +9,6 @@ export {} from './contract.js';
|
|
|
8
9
|
const MAX_INLINE_CONTENT_CHARS = parseInt(process.env['FS_CONTEXT_MAX_INLINE_CHARS'] ?? '', 10) || 20_000;
|
|
9
10
|
const MAX_INLINE_PREVIEW_CHARS = 4_000;
|
|
10
11
|
const PROGRESS_RATE_LIMIT_MS = 50;
|
|
11
|
-
const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes']);
|
|
12
12
|
const CONTEXT_DIAGNOSTICS_CHANNEL = channel('filesystem-mcp:context');
|
|
13
13
|
function publishContextDiagnostics(event) {
|
|
14
14
|
if (!CONTEXT_DIAGNOSTICS_CHANNEL.hasSubscribers)
|
|
@@ -33,10 +33,7 @@ export const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS = {
|
|
|
33
33
|
openWorldHint: false,
|
|
34
34
|
};
|
|
35
35
|
export function shouldStripStructuredOutput() {
|
|
36
|
-
|
|
37
|
-
if (value === undefined)
|
|
38
|
-
return false;
|
|
39
|
-
return TRUE_ENV_VALUES.has(value.trim().toLowerCase());
|
|
36
|
+
return parseTrueEnvFlag(process.env['FS_CONTEXT_STRIP_STRUCTURED']);
|
|
40
37
|
}
|
|
41
38
|
export function maybeStripStructuredContentFromResult(result) {
|
|
42
39
|
if (!shouldStripStructuredOutput())
|
package/dist/tools/stat-many.js
CHANGED
|
@@ -9,7 +9,7 @@ import { registerToolTaskIfAvailable } from './task-support.js';
|
|
|
9
9
|
export const GET_MULTIPLE_FILE_INFO_TOOL = {
|
|
10
10
|
name: 'stat_many',
|
|
11
11
|
title: 'Get Multiple File Info',
|
|
12
|
-
description: 'Get metadata for multiple files or directories in one request.',
|
|
12
|
+
description: 'Get metadata (including tokenEstimate) for multiple files or directories in one request. Use tokenEstimate (size÷4) to pre-screen token cost before reading.',
|
|
13
13
|
inputSchema: GetMultipleFileInfoInputSchema,
|
|
14
14
|
outputSchema: GetMultipleFileInfoOutputSchema,
|
|
15
15
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
package/dist/tools/stat.js
CHANGED
|
@@ -8,7 +8,7 @@ import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, execut
|
|
|
8
8
|
export const GET_FILE_INFO_TOOL = {
|
|
9
9
|
name: 'stat',
|
|
10
10
|
title: 'Get File Info',
|
|
11
|
-
description: 'Get metadata (size, modified time, permissions, mime type) for a file or directory.',
|
|
11
|
+
description: 'Get metadata (size, modified time, permissions, mime type, tokenEstimate) for a file or directory. Use tokenEstimate (size÷4) to pre-screen token cost before reading.',
|
|
12
12
|
inputSchema: GetFileInfoInputSchema,
|
|
13
13
|
outputSchema: GetFileInfoOutputSchema,
|
|
14
14
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
package/dist/tools/write-file.js
CHANGED
|
@@ -9,15 +9,12 @@ import { registerToolTaskIfAvailable } from './task-support.js';
|
|
|
9
9
|
export const WRITE_FILE_TOOL = {
|
|
10
10
|
name: 'write',
|
|
11
11
|
title: 'Write File',
|
|
12
|
-
description: 'Write content to a file. Creates the file
|
|
12
|
+
description: 'Write content to a file, OVERWRITING ALL existing content. Creates the file and parent directories if needed.',
|
|
13
13
|
inputSchema: WriteFileInputSchema,
|
|
14
14
|
outputSchema: WriteFileOutputSchema,
|
|
15
15
|
annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
|
|
16
|
-
nuances: [
|
|
17
|
-
'Creates parent directories automatically; overwrites existing content.',
|
|
18
|
-
],
|
|
19
16
|
gotchas: [
|
|
20
|
-
'
|
|
17
|
+
'`write` replaces ALL existing content — use `edit` for partial updates.',
|
|
21
18
|
],
|
|
22
19
|
};
|
|
23
20
|
async function handleWriteFile(args, signal) {
|
package/dist/tools.js
CHANGED
|
@@ -18,48 +18,38 @@ import { GET_FILE_INFO_TOOL, registerGetFileInfoTool } from './tools/stat.js';
|
|
|
18
18
|
import { registerTreeTool, TREE_TOOL } from './tools/tree.js';
|
|
19
19
|
import { registerWriteFileTool, WRITE_FILE_TOOL } from './tools/write-file.js';
|
|
20
20
|
export { buildToolErrorResponse, buildToolResponse } from './tools/shared.js';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
registerGetMultipleFileInfoTool,
|
|
50
|
-
registerSearchContentTool,
|
|
51
|
-
registerCreateDirectoryTool,
|
|
52
|
-
registerWriteFileTool,
|
|
53
|
-
registerEditFileTool,
|
|
54
|
-
registerMoveFileTool,
|
|
55
|
-
registerDeleteFileTool,
|
|
56
|
-
registerCalculateHashTool,
|
|
57
|
-
registerDiffFilesTool,
|
|
58
|
-
registerApplyPatchTool,
|
|
59
|
-
registerSearchAndReplaceTool,
|
|
21
|
+
const TOOL_ENTRIES = [
|
|
22
|
+
{
|
|
23
|
+
contract: LIST_ALLOWED_DIRECTORIES_TOOL,
|
|
24
|
+
register: registerListAllowedDirectoriesTool,
|
|
25
|
+
},
|
|
26
|
+
{ contract: LIST_DIRECTORY_TOOL, register: registerListDirectoryTool },
|
|
27
|
+
{ contract: SEARCH_FILES_TOOL, register: registerSearchFilesTool },
|
|
28
|
+
{ contract: TREE_TOOL, register: registerTreeTool },
|
|
29
|
+
{ contract: READ_FILE_TOOL, register: registerReadFileTool },
|
|
30
|
+
{
|
|
31
|
+
contract: READ_MULTIPLE_FILES_TOOL,
|
|
32
|
+
register: registerReadMultipleFilesTool,
|
|
33
|
+
},
|
|
34
|
+
{ contract: GET_FILE_INFO_TOOL, register: registerGetFileInfoTool },
|
|
35
|
+
{
|
|
36
|
+
contract: GET_MULTIPLE_FILE_INFO_TOOL,
|
|
37
|
+
register: registerGetMultipleFileInfoTool,
|
|
38
|
+
},
|
|
39
|
+
{ contract: SEARCH_CONTENT_TOOL, register: registerSearchContentTool },
|
|
40
|
+
{ contract: CREATE_DIRECTORY_TOOL, register: registerCreateDirectoryTool },
|
|
41
|
+
{ contract: WRITE_FILE_TOOL, register: registerWriteFileTool },
|
|
42
|
+
{ contract: EDIT_FILE_TOOL, register: registerEditFileTool },
|
|
43
|
+
{ contract: MOVE_FILE_TOOL, register: registerMoveFileTool },
|
|
44
|
+
{ contract: DELETE_FILE_TOOL, register: registerDeleteFileTool },
|
|
45
|
+
{ contract: CALCULATE_HASH_TOOL, register: registerCalculateHashTool },
|
|
46
|
+
{ contract: DIFF_FILES_TOOL, register: registerDiffFilesTool },
|
|
47
|
+
{ contract: APPLY_PATCH_TOOL, register: registerApplyPatchTool },
|
|
48
|
+
{ contract: SEARCH_AND_REPLACE_TOOL, register: registerSearchAndReplaceTool },
|
|
60
49
|
];
|
|
50
|
+
export const ALL_TOOLS = TOOL_ENTRIES.map((e) => e.contract);
|
|
61
51
|
export function registerAllTools(server, options = {}) {
|
|
62
|
-
for (const
|
|
63
|
-
|
|
52
|
+
for (const { register } of TOOL_ENTRIES) {
|
|
53
|
+
register(server, options);
|
|
64
54
|
}
|
|
65
55
|
}
|