@j0hanz/filesystem-mcp 1.5.0 → 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 +11 -1
- package/dist/lib/constants.js +16 -1
- 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 +3 -2
- package/dist/resources/workflows.js +6 -8
- package/dist/resources.js +2 -2
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +24 -18
- 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/calculate-hash.js +1 -1
- package/dist/tools/create-directory.js +4 -3
- package/dist/tools/delete-file.js +2 -1
- package/dist/tools/edit-file.js +10 -3
- package/dist/tools/list-directory.js +1 -1
- package/dist/tools/move-file.js +3 -0
- package/dist/tools/replace-in-files.js +1 -1
- package/dist/tools/search-content.js +2 -2
- package/dist/tools/shared.d.ts +2 -0
- package/dist/tools/shared.js +4 -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 +3 -3
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;
|
|
@@ -11,8 +12,17 @@ export declare const DEFAULT_SEARCH_TIMEOUT_MS: number;
|
|
|
11
12
|
*/
|
|
12
13
|
export declare const SEARCH_WORKERS: number;
|
|
13
14
|
export declare const DEFAULT_MAX_DEPTH = 10;
|
|
14
|
-
export declare const DEFAULT_LIST_MAX_ENTRIES =
|
|
15
|
+
export declare const DEFAULT_LIST_MAX_ENTRIES = 20000;
|
|
15
16
|
export declare const DEFAULT_SEARCH_MAX_FILES = 20000;
|
|
17
|
+
export declare const MAX_TREE_DEPTH = 50;
|
|
18
|
+
export declare const DEFAULT_TREE_DEPTH = 5;
|
|
19
|
+
export declare const MAX_TREE_ENTRIES = 20000;
|
|
20
|
+
export declare const DEFAULT_TREE_ENTRIES = 1000;
|
|
21
|
+
export declare const MAX_LIST_ENTRIES = 20000;
|
|
22
|
+
export declare const MAX_SEARCH_RESULTS = 10000;
|
|
23
|
+
export declare const DEFAULT_SEARCH_RESULTS = 100;
|
|
24
|
+
export declare const MAX_SEARCH_DEPTH = 100;
|
|
25
|
+
export declare const DEFAULT_SEARCH_CONTENT_RESULTS = 500;
|
|
16
26
|
export declare const MAX_LINE_CONTENT_LENGTH = 200;
|
|
17
27
|
export declare const BINARY_CHECK_BUFFER_SIZE = 512;
|
|
18
28
|
export declare const SENSITIVE_FILE_DENYLIST: string[];
|
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) {
|
|
@@ -109,8 +114,18 @@ const ENV_ALLOWLIST = parseEnvList('FS_CONTEXT_ALLOWLIST');
|
|
|
109
114
|
export const SEARCH_WORKERS = parseEnvInt('FS_CONTEXT_SEARCH_WORKERS', getDefaultSearchWorkers(), 1, 16);
|
|
110
115
|
// Hardcoded defaults
|
|
111
116
|
export const DEFAULT_MAX_DEPTH = 10;
|
|
112
|
-
export const DEFAULT_LIST_MAX_ENTRIES =
|
|
117
|
+
export const DEFAULT_LIST_MAX_ENTRIES = 20000;
|
|
113
118
|
export const DEFAULT_SEARCH_MAX_FILES = 20000;
|
|
119
|
+
// Schema limits and defaults
|
|
120
|
+
export const MAX_TREE_DEPTH = 50;
|
|
121
|
+
export const DEFAULT_TREE_DEPTH = 5;
|
|
122
|
+
export const MAX_TREE_ENTRIES = 20000;
|
|
123
|
+
export const DEFAULT_TREE_ENTRIES = 1000;
|
|
124
|
+
export const MAX_LIST_ENTRIES = 20000;
|
|
125
|
+
export const MAX_SEARCH_RESULTS = 10000;
|
|
126
|
+
export const DEFAULT_SEARCH_RESULTS = 100;
|
|
127
|
+
export const MAX_SEARCH_DEPTH = 100;
|
|
128
|
+
export const DEFAULT_SEARCH_CONTENT_RESULTS = 500;
|
|
114
129
|
// Non-configurable constants
|
|
115
130
|
export const MAX_LINE_CONTENT_LENGTH = 200;
|
|
116
131
|
export const BINARY_CHECK_BUFFER_SIZE = 512;
|
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() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_SEARCH_CONTENT_RESULTS, MAX_SEARCH_RESULTS, MAX_TEXT_FILE_SIZE, } from '../lib/constants.js';
|
|
1
2
|
import { ALL_TOOLS } from '../tools.js';
|
|
2
3
|
function toEntry(contract) {
|
|
3
4
|
const annotations = [];
|
|
@@ -38,7 +39,7 @@ export function getSharedConstraints() {
|
|
|
38
39
|
return [
|
|
39
40
|
'Allowed roots only (negotiated via CLI).',
|
|
40
41
|
'Sensitive files denylisted by default.',
|
|
41
|
-
|
|
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
|
+
'If a response includes `resourceUri`, call `resources/read` immediately — results expire on process restart.',
|
|
43
44
|
];
|
|
44
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.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export declare const ListDirectoryInputSchema: z.ZodObject<{
|
|
|
38
38
|
includeHidden: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
39
39
|
includeIgnored: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
40
40
|
maxDepth: z.ZodOptional<z.ZodNumber>;
|
|
41
|
-
maxEntries: z.ZodOptional<z.ZodNumber
|
|
41
|
+
maxEntries: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
42
42
|
sortBy: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
43
43
|
name: "name";
|
|
44
44
|
size: "size";
|
package/dist/schemas.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { DEFAULT_LIST_MAX_ENTRIES, DEFAULT_SEARCH_CONTENT_RESULTS, DEFAULT_SEARCH_RESULTS, DEFAULT_TREE_DEPTH, DEFAULT_TREE_ENTRIES, MAX_LIST_ENTRIES, MAX_SEARCH_DEPTH, MAX_SEARCH_RESULTS, MAX_TREE_DEPTH, MAX_TREE_ENTRIES, } from './lib/constants.js';
|
|
2
3
|
import { ErrorCode } from './lib/errors.js';
|
|
3
4
|
function isSafeGlobPattern(value) {
|
|
4
5
|
if (value.length === 0)
|
|
@@ -25,8 +26,12 @@ const RequiredPathSchema = PathSchemaBase.min(1, 'Path required');
|
|
|
25
26
|
const FileTypeSchema = z.enum(['file', 'directory', 'symlink', 'other']);
|
|
26
27
|
const ListDirectorySortSchema = z.enum(['name', 'size', 'modified', 'type']);
|
|
27
28
|
const SearchFilesSortSchema = z.enum(['name', 'size', 'modified', 'path']);
|
|
28
|
-
const SearchStopReasonSchema = z
|
|
29
|
-
|
|
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');
|
|
30
35
|
const TreeEntrySchema = z.lazy(() => z.strictObject({
|
|
31
36
|
name: z.string().describe('Name'),
|
|
32
37
|
type: FileTypeSchema.describe('Type'),
|
|
@@ -111,16 +116,17 @@ export const ListDirectoryInputSchema = z.strictObject({
|
|
|
111
116
|
.number()
|
|
112
117
|
.int({ error: 'Must be integer' })
|
|
113
118
|
.min(1, 'Min: 1')
|
|
114
|
-
.max(
|
|
119
|
+
.max(MAX_TREE_DEPTH, `Max: ${MAX_TREE_DEPTH}`)
|
|
115
120
|
.optional()
|
|
116
121
|
.describe('Max recursion depth when pattern is provided'),
|
|
117
122
|
maxEntries: z
|
|
118
123
|
.number()
|
|
119
124
|
.int({ error: 'Must be integer' })
|
|
120
125
|
.min(1, 'Min: 1')
|
|
121
|
-
.max(
|
|
126
|
+
.max(MAX_LIST_ENTRIES, `Max: ${MAX_LIST_ENTRIES}`)
|
|
122
127
|
.optional()
|
|
123
|
-
.
|
|
128
|
+
.default(DEFAULT_LIST_MAX_ENTRIES)
|
|
129
|
+
.describe(`Maximum entries to return before truncation. Default: ${DEFAULT_LIST_MAX_ENTRIES}`),
|
|
124
130
|
sortBy: ListDirectorySortSchema.optional()
|
|
125
131
|
.default('name')
|
|
126
132
|
.describe('Sort field (name, size, modified, type)'),
|
|
@@ -157,10 +163,10 @@ export const SearchFilesInputSchema = z.strictObject({
|
|
|
157
163
|
.number()
|
|
158
164
|
.int({ error: 'Must be integer' })
|
|
159
165
|
.min(1, 'Min: 1')
|
|
160
|
-
.max(
|
|
166
|
+
.max(MAX_SEARCH_RESULTS, `Max: ${MAX_SEARCH_RESULTS}`)
|
|
161
167
|
.optional()
|
|
162
|
-
.default(
|
|
163
|
-
.describe(
|
|
168
|
+
.default(DEFAULT_SEARCH_RESULTS)
|
|
169
|
+
.describe(`Max results (1-${MAX_SEARCH_RESULTS}). Default: ${DEFAULT_SEARCH_RESULTS}`),
|
|
164
170
|
includeIgnored: z
|
|
165
171
|
.boolean()
|
|
166
172
|
.optional()
|
|
@@ -178,7 +184,7 @@ export const SearchFilesInputSchema = z.strictObject({
|
|
|
178
184
|
.number()
|
|
179
185
|
.int({ error: 'Must be integer' })
|
|
180
186
|
.min(0, 'Min: 0')
|
|
181
|
-
.max(
|
|
187
|
+
.max(MAX_SEARCH_DEPTH, `Max: ${MAX_SEARCH_DEPTH}`)
|
|
182
188
|
.optional()
|
|
183
189
|
.describe('Maximum directory depth to scan'),
|
|
184
190
|
cursor: z
|
|
@@ -192,18 +198,18 @@ export const TreeInputSchema = z.strictObject({
|
|
|
192
198
|
.number()
|
|
193
199
|
.int({ error: 'Must be integer' })
|
|
194
200
|
.min(0, 'Min: 0')
|
|
195
|
-
.max(
|
|
201
|
+
.max(MAX_TREE_DEPTH, `Max: ${MAX_TREE_DEPTH}`)
|
|
196
202
|
.optional()
|
|
197
|
-
.default(
|
|
198
|
-
.describe(
|
|
203
|
+
.default(DEFAULT_TREE_DEPTH)
|
|
204
|
+
.describe(`Depth (0=root node only, no children). Default: ${DEFAULT_TREE_DEPTH}`),
|
|
199
205
|
maxEntries: z
|
|
200
206
|
.number()
|
|
201
207
|
.int({ error: 'Must be integer' })
|
|
202
208
|
.min(1, 'Min: 1')
|
|
203
|
-
.max(
|
|
209
|
+
.max(MAX_TREE_ENTRIES, `Max: ${MAX_TREE_ENTRIES}`)
|
|
204
210
|
.optional()
|
|
205
|
-
.default(
|
|
206
|
-
.describe(
|
|
211
|
+
.default(DEFAULT_TREE_ENTRIES)
|
|
212
|
+
.describe(`Max entries. Default: ${DEFAULT_TREE_ENTRIES}`),
|
|
207
213
|
includeHidden: z
|
|
208
214
|
.boolean()
|
|
209
215
|
.optional()
|
|
@@ -249,10 +255,10 @@ export const SearchContentInputSchema = z.strictObject({
|
|
|
249
255
|
.number()
|
|
250
256
|
.int({ error: 'Must be integer' })
|
|
251
257
|
.min(0, 'Min: 0')
|
|
252
|
-
.max(
|
|
258
|
+
.max(MAX_SEARCH_RESULTS, `Max: ${MAX_SEARCH_RESULTS}`)
|
|
253
259
|
.optional()
|
|
254
|
-
.default(
|
|
255
|
-
.describe(
|
|
260
|
+
.default(DEFAULT_SEARCH_CONTENT_RESULTS)
|
|
261
|
+
.describe(`Maximum match rows to return. Default: ${DEFAULT_SEARCH_CONTENT_RESULTS}`),
|
|
256
262
|
filePattern: z
|
|
257
263
|
.string()
|
|
258
264
|
.min(1, 'Pattern required')
|
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);
|
|
@@ -171,7 +171,7 @@ export function registerCalculateHashTool(server, options = {}) {
|
|
|
171
171
|
baseReporter({
|
|
172
172
|
current,
|
|
173
173
|
...(total !== undefined ? { total } : {}),
|
|
174
|
-
message: `🕮 calculate_hash: ${baseName}
|
|
174
|
+
message: `🕮 calculate_hash: ${baseName} [${current} ${fileWord} hashed]`,
|
|
175
175
|
});
|
|
176
176
|
};
|
|
177
177
|
try {
|
|
@@ -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);
|
|
@@ -34,11 +35,11 @@ export function registerCreateDirectoryTool(server, options = {}) {
|
|
|
34
35
|
const wrappedHandler = wrapToolHandler(handler, {
|
|
35
36
|
guard: options.isInitialized,
|
|
36
37
|
progressMessage: (args) => {
|
|
37
|
-
const name = path.basename(args.path) ||
|
|
38
|
+
const name = path.basename(args.path) || '.';
|
|
38
39
|
return `🛠 mkdir: ${name}`;
|
|
39
40
|
},
|
|
40
41
|
completionMessage: (args, result) => {
|
|
41
|
-
const name = path.basename(args.path) ||
|
|
42
|
+
const name = path.basename(args.path) || '.';
|
|
42
43
|
if (result.isError)
|
|
43
44
|
return `🛠 mkdir: ${name} • failed`;
|
|
44
45
|
return `🛠 mkdir: ${name} • created`;
|
|
@@ -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/edit-file.js
CHANGED
|
@@ -96,7 +96,8 @@ export function registerEditFileTool(server, options = {}) {
|
|
|
96
96
|
guard: options.isInitialized,
|
|
97
97
|
progressMessage: (args) => {
|
|
98
98
|
const name = path.basename(args.path);
|
|
99
|
-
|
|
99
|
+
const dryTag = args.dryRun ? ' [dry run]' : '';
|
|
100
|
+
return `🛠 edit: ${name} [${args.edits.length} edits]${dryTag}`;
|
|
100
101
|
},
|
|
101
102
|
completionMessage: (args, result) => {
|
|
102
103
|
const name = path.basename(args.path);
|
|
@@ -105,10 +106,16 @@ export function registerEditFileTool(server, options = {}) {
|
|
|
105
106
|
const sc = result.structuredContent;
|
|
106
107
|
if (!sc.ok)
|
|
107
108
|
return `🛠 edit: ${name} • failed`;
|
|
109
|
+
const applied = sc.appliedEdits ?? 0;
|
|
110
|
+
const unmatched = sc.unmatchedEdits?.length ?? 0;
|
|
111
|
+
const dryPrefix = args.dryRun ? 'dry run — ' : '';
|
|
112
|
+
if (unmatched > 0) {
|
|
113
|
+
return `🛠 edit: ${name} • ${dryPrefix}${applied} applied, ${unmatched} unmatched`;
|
|
114
|
+
}
|
|
108
115
|
if (sc.lineRange) {
|
|
109
|
-
return `🛠 edit: ${name} •
|
|
116
|
+
return `🛠 edit: ${name} • ${dryPrefix}lines ${sc.lineRange[0]}–${sc.lineRange[1]}`;
|
|
110
117
|
}
|
|
111
|
-
return `🛠 edit: ${name} •
|
|
118
|
+
return `🛠 edit: ${name} • ${dryPrefix}${applied} applied`;
|
|
112
119
|
},
|
|
113
120
|
});
|
|
114
121
|
const validatedHandler = withValidatedArgs(EditFileInputSchema, wrappedHandler);
|
|
@@ -98,7 +98,7 @@ function decodeCursor(cursor) {
|
|
|
98
98
|
async function handleListDirectory(args, signal) {
|
|
99
99
|
const dirPath = resolvePathOrRoot(args.path);
|
|
100
100
|
const cursorOffset = args.cursor !== undefined ? decodeCursor(args.cursor) : 0;
|
|
101
|
-
const pageSize = args.maxEntries
|
|
101
|
+
const pageSize = args.maxEntries;
|
|
102
102
|
const options = {
|
|
103
103
|
includeHidden: args.includeHidden,
|
|
104
104
|
excludePatterns: args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS,
|
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);
|
|
@@ -265,7 +265,7 @@ export function registerSearchAndReplaceTool(server, options = {}) {
|
|
|
265
265
|
baseReporter({
|
|
266
266
|
current,
|
|
267
267
|
...(total !== undefined ? { total } : {}),
|
|
268
|
-
message: `🛠 search_and_replace:
|
|
268
|
+
message: `🛠 search_and_replace: ${args.searchPattern} [${current} files processed]`,
|
|
269
269
|
});
|
|
270
270
|
};
|
|
271
271
|
try {
|
|
@@ -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
|
};
|
|
@@ -220,7 +220,7 @@ export function registerSearchContentTool(server, options = {}) {
|
|
|
220
220
|
baseReporter({
|
|
221
221
|
current,
|
|
222
222
|
...(total !== undefined ? { total } : {}),
|
|
223
|
-
message: `🔎︎ grep: ${pattern}
|
|
223
|
+
message: `🔎︎ grep: ${pattern} [${current} ${fileWord} scanned]`,
|
|
224
224
|
});
|
|
225
225
|
};
|
|
226
226
|
try {
|
package/dist/tools/shared.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { type ToolContract } from './contract.js';
|
|
|
8
8
|
export declare const READ_ONLY_TOOL_ANNOTATIONS: {
|
|
9
9
|
readonly readOnlyHint: true;
|
|
10
10
|
readonly idempotentHint: true;
|
|
11
|
+
readonly destructiveHint: false;
|
|
11
12
|
readonly openWorldHint: false;
|
|
12
13
|
};
|
|
13
14
|
export declare const DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS: {
|
|
@@ -18,6 +19,7 @@ export declare const DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS: {
|
|
|
18
19
|
export declare const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS: {
|
|
19
20
|
readonly readOnlyHint: false;
|
|
20
21
|
readonly idempotentHint: true;
|
|
22
|
+
readonly destructiveHint: false;
|
|
21
23
|
readonly openWorldHint: false;
|
|
22
24
|
};
|
|
23
25
|
export declare function shouldStripStructuredOutput(): boolean;
|
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)
|
|
@@ -18,6 +18,7 @@ function publishContextDiagnostics(event) {
|
|
|
18
18
|
export const READ_ONLY_TOOL_ANNOTATIONS = {
|
|
19
19
|
readOnlyHint: true,
|
|
20
20
|
idempotentHint: true,
|
|
21
|
+
destructiveHint: false,
|
|
21
22
|
openWorldHint: false,
|
|
22
23
|
};
|
|
23
24
|
export const DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS = {
|
|
@@ -28,13 +29,11 @@ export const DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS = {
|
|
|
28
29
|
export const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS = {
|
|
29
30
|
readOnlyHint: false,
|
|
30
31
|
idempotentHint: true,
|
|
32
|
+
destructiveHint: false,
|
|
31
33
|
openWorldHint: false,
|
|
32
34
|
};
|
|
33
35
|
export function shouldStripStructuredOutput() {
|
|
34
|
-
|
|
35
|
-
if (value === undefined)
|
|
36
|
-
return false;
|
|
37
|
-
return TRUE_ENV_VALUES.has(value.trim().toLowerCase());
|
|
36
|
+
return parseTrueEnvFlag(process.env['FS_CONTEXT_STRIP_STRUCTURED']);
|
|
38
37
|
}
|
|
39
38
|
export function maybeStripStructuredContentFromResult(result) {
|
|
40
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@j0hanz/filesystem-mcp",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"mcpName": "io.github.j0hanz/filesystem-mcp",
|
|
5
5
|
"description": "MCP Server that enables LLMs to interact with the local filesystem.",
|
|
6
6
|
"type": "module",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@eslint/js": "^10.0.1",
|
|
76
|
-
"eslint": "^10.0.
|
|
76
|
+
"eslint": "^10.0.1",
|
|
77
77
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
|
78
78
|
"@types/node": "^24",
|
|
79
79
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"eslint-plugin-depend": "^1.4.0",
|
|
82
82
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
83
83
|
"jscpd": "^4.0.8",
|
|
84
|
-
"knip": "^5.
|
|
84
|
+
"knip": "^5.85.0",
|
|
85
85
|
"prettier": "^3.8.1",
|
|
86
86
|
"tsx": "^4.21.0",
|
|
87
87
|
"typescript": "^5.9.3",
|