@j0hanz/filesystem-mcp 1.2.4 → 1.3.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 +8 -0
- package/dist/completions.d.ts +1 -1
- package/dist/completions.js +36 -1
- package/dist/lib/observability.d.ts +6 -0
- package/dist/lib/observability.js +1 -1
- package/dist/lib/resource-store.js +53 -0
- package/dist/prompts.js +34 -14
- package/dist/resources/generated-instructions.d.ts +1 -0
- package/dist/resources/generated-instructions.js +100 -0
- package/dist/resources.d.ts +1 -0
- package/dist/resources.js +36 -1
- package/dist/schemas.d.ts +6 -0
- package/dist/schemas.js +24 -0
- package/dist/server/bootstrap.js +45 -22
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/tools/apply-patch.d.ts +2 -1
- package/dist/tools/apply-patch.js +7 -5
- package/dist/tools/calculate-hash.d.ts +2 -1
- package/dist/tools/calculate-hash.js +9 -5
- package/dist/tools/contract.d.ts +41 -0
- package/dist/tools/contract.js +1 -0
- package/dist/tools/create-directory.d.ts +2 -1
- package/dist/tools/create-directory.js +6 -5
- package/dist/tools/delete-file.d.ts +2 -1
- package/dist/tools/delete-file.js +9 -5
- package/dist/tools/diff-files.d.ts +2 -1
- package/dist/tools/diff-files.js +7 -4
- package/dist/tools/edit-file.d.ts +2 -1
- package/dist/tools/edit-file.js +19 -6
- package/dist/tools/list-directory.d.ts +2 -1
- package/dist/tools/list-directory.js +36 -7
- package/dist/tools/move-file.d.ts +2 -1
- package/dist/tools/move-file.js +7 -5
- package/dist/tools/read-multiple.d.ts +2 -1
- package/dist/tools/read-multiple.js +10 -5
- package/dist/tools/read.d.ts +2 -1
- package/dist/tools/read.js +9 -5
- package/dist/tools/replace-in-files.d.ts +2 -1
- package/dist/tools/replace-in-files.js +14 -7
- package/dist/tools/roots.d.ts +2 -1
- package/dist/tools/roots.js +7 -4
- package/dist/tools/search-content.d.ts +2 -1
- package/dist/tools/search-content.js +14 -6
- package/dist/tools/search-files.d.ts +2 -1
- package/dist/tools/search-files.js +39 -7
- package/dist/tools/shared.d.ts +2 -2
- package/dist/tools/shared.js +16 -1
- package/dist/tools/stat-many.d.ts +2 -1
- package/dist/tools/stat-many.js +7 -5
- package/dist/tools/stat.d.ts +2 -1
- package/dist/tools/stat.js +7 -4
- package/dist/tools/task-support.d.ts +2 -0
- package/dist/tools/task-support.js +48 -7
- package/dist/tools/tree.d.ts +2 -1
- package/dist/tools/tree.js +7 -5
- package/dist/tools/write-file.d.ts +2 -1
- package/dist/tools/write-file.js +12 -5
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +39 -18
- package/package.json +1 -2
- package/dist/instructions.md +0 -200
package/README.md
CHANGED
|
@@ -479,6 +479,8 @@ Replace text in all files matching a glob. Replaces **all** occurrences per file
|
|
|
479
479
|
| `internal://instructions` | Usage guidance for models | `text/markdown` |
|
|
480
480
|
| `filesystem-mcp://result/{id}` | Ephemeral cached large tool output | varies |
|
|
481
481
|
|
|
482
|
+
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
|
+
|
|
482
484
|
### Prompts
|
|
483
485
|
|
|
484
486
|
| Prompt | Description |
|
|
@@ -493,6 +495,12 @@ The server declares full task capabilities (`tasks/list`, `tasks/cancel`). The f
|
|
|
493
495
|
|
|
494
496
|
Include `_meta.progressToken` in a `tools/call` request to receive `notifications/progress` updates. Use `tools/call` with a `task` field to invoke as a background task, then poll `tasks/get` and retrieve output via `tasks/result`.
|
|
495
497
|
|
|
498
|
+
Recommended task follow-up loop:
|
|
499
|
+
|
|
500
|
+
1. Start with `tools/call` + `task` (optional `_meta.progressToken`).
|
|
501
|
+
2. Poll `tasks/get` until terminal status (`completed`, `failed`, `cancelled`).
|
|
502
|
+
3. Fetch final payload with `tasks/result`.
|
|
503
|
+
|
|
496
504
|
Task status notifications (`notifications/tasks/status`) are best-effort and emitted only when the transport/runtime provides a notification sender.
|
|
497
505
|
|
|
498
506
|
Cancellation semantics:
|
package/dist/completions.d.ts
CHANGED
|
@@ -9,5 +9,5 @@ interface CompletionOptions {
|
|
|
9
9
|
contextArguments?: Record<string, string>;
|
|
10
10
|
}
|
|
11
11
|
export declare function getPathCompletions(currentValue: string, options?: CompletionOptions): Promise<CompletionResult>;
|
|
12
|
-
export declare function registerCompletions(server: McpServer): void;
|
|
12
|
+
export declare function registerCompletions(server: McpServer, instructions?: string): void;
|
|
13
13
|
export {};
|
package/dist/completions.js
CHANGED
|
@@ -5,6 +5,19 @@ import { toPosixPath } from './lib/path-format.js';
|
|
|
5
5
|
import { getAllowedDirectories, isPathWithinDirectories, normalizePath, } from './lib/path-validation.js';
|
|
6
6
|
import { isRecord } from './lib/type-guards.js';
|
|
7
7
|
const MAX_COMPLETION_ITEMS = 100;
|
|
8
|
+
const COMPLETION_RATE_LIMIT_MS = 100;
|
|
9
|
+
const completionLastCallMs = new Map();
|
|
10
|
+
function extractTopicCompletions(instructions) {
|
|
11
|
+
const headers = [];
|
|
12
|
+
for (const line of instructions.split('\n')) {
|
|
13
|
+
if (line.startsWith('## ')) {
|
|
14
|
+
const header = line.slice(3).trim().toLowerCase();
|
|
15
|
+
if (header)
|
|
16
|
+
headers.push(header);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return headers;
|
|
20
|
+
}
|
|
8
21
|
const PATH_ARGUMENTS = new Set([
|
|
9
22
|
'path',
|
|
10
23
|
'source',
|
|
@@ -352,16 +365,38 @@ export async function getPathCompletions(currentValue, options = {}) {
|
|
|
352
365
|
return { values: [] };
|
|
353
366
|
}
|
|
354
367
|
}
|
|
355
|
-
export function registerCompletions(server) {
|
|
368
|
+
export function registerCompletions(server, instructions = '') {
|
|
369
|
+
const topicValues = extractTopicCompletions(instructions);
|
|
356
370
|
server.server.setRequestHandler(CompleteRequestSchema, async (request) => {
|
|
357
371
|
const { params } = request;
|
|
358
372
|
const { argument, ref } = params;
|
|
359
373
|
const argName = argument.name.toLowerCase();
|
|
374
|
+
// Handle prompt topic completions
|
|
375
|
+
if (isRecord(ref) && ref['type'] === 'ref/prompt' && argName === 'topic') {
|
|
376
|
+
const currentValue = argument.value.toLowerCase();
|
|
377
|
+
const filtered = currentValue
|
|
378
|
+
? topicValues.filter((v) => v.startsWith(currentValue))
|
|
379
|
+
: topicValues;
|
|
380
|
+
const sliced = filtered.slice(0, MAX_COMPLETION_ITEMS);
|
|
381
|
+
return {
|
|
382
|
+
completion: {
|
|
383
|
+
values: sliced,
|
|
384
|
+
total: filtered.length,
|
|
385
|
+
hasMore: filtered.length > MAX_COMPLETION_ITEMS,
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
360
389
|
const isPathArg = isPathLikeArgumentName(argName) ||
|
|
361
390
|
isPathArgumentFromReference(argName, ref);
|
|
362
391
|
if (!isPathArg) {
|
|
363
392
|
return { completion: { values: [], total: 0, hasMore: false } };
|
|
364
393
|
}
|
|
394
|
+
const now = Date.now();
|
|
395
|
+
const lastCallMs = completionLastCallMs.get(argName) ?? 0;
|
|
396
|
+
if (now - lastCallMs < COMPLETION_RATE_LIMIT_MS) {
|
|
397
|
+
return { completion: { values: [], total: 0, hasMore: false } };
|
|
398
|
+
}
|
|
399
|
+
completionLastCallMs.set(argName, now);
|
|
365
400
|
const contextArguments = extractContextArguments(params.context);
|
|
366
401
|
const { value } = argument;
|
|
367
402
|
const completions = await getPathCompletions(value, {
|
|
@@ -5,6 +5,12 @@ interface OpsTraceContext {
|
|
|
5
5
|
path?: string | undefined;
|
|
6
6
|
[key: string]: unknown;
|
|
7
7
|
}
|
|
8
|
+
export interface ToolMetrics {
|
|
9
|
+
calls: number;
|
|
10
|
+
errors: number;
|
|
11
|
+
totalDurationMs: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const globalMetrics: Map<string, ToolMetrics>;
|
|
8
14
|
export declare function shouldPublishOpsTrace(): boolean;
|
|
9
15
|
export declare function publishOpsTraceStart(context: OpsTraceContext): void;
|
|
10
16
|
export declare function publishOpsTraceEnd(context: OpsTraceContext): void;
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { hash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { channel } from 'node:diagnostics_channel';
|
|
2
3
|
import { ErrorCode, McpError } from './errors.js';
|
|
3
4
|
const DEFAULT_RESOURCE_STORE_OPTIONS = {
|
|
4
5
|
maxEntries: 64,
|
|
5
6
|
maxTotalBytes: 25 * 1024 * 1024,
|
|
6
7
|
maxEntryBytes: 10 * 1024 * 1024,
|
|
7
8
|
};
|
|
9
|
+
const RESOURCE_STORE_DIAGNOSTICS_CHANNEL = channel('filesystem-mcp:resource-store');
|
|
10
|
+
function publishResourceStoreDiagnostics(event) {
|
|
11
|
+
if (!RESOURCE_STORE_DIAGNOSTICS_CHANNEL.hasSubscribers)
|
|
12
|
+
return;
|
|
13
|
+
RESOURCE_STORE_DIAGNOSTICS_CHANNEL.publish(event);
|
|
14
|
+
}
|
|
8
15
|
function estimateBytes(text) {
|
|
9
16
|
return Buffer.byteLength(text, 'utf8');
|
|
10
17
|
}
|
|
@@ -41,6 +48,12 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
41
48
|
totalBytes -= existing.size;
|
|
42
49
|
byUri.delete(uri);
|
|
43
50
|
byHashIndex.delete(existing.hash);
|
|
51
|
+
publishResourceStoreDiagnostics({
|
|
52
|
+
phase: 'cache_evict',
|
|
53
|
+
uri,
|
|
54
|
+
name: existing.name,
|
|
55
|
+
bytes: existing.size,
|
|
56
|
+
});
|
|
44
57
|
}
|
|
45
58
|
function enforceLimits() {
|
|
46
59
|
while (byUri.size > resolved.maxEntries)
|
|
@@ -55,6 +68,11 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
55
68
|
const mimeType = params.mimeType ?? 'text/plain';
|
|
56
69
|
const entryBytes = estimateBytes(params.text);
|
|
57
70
|
if (entryBytes > resolved.maxEntryBytes) {
|
|
71
|
+
publishResourceStoreDiagnostics({
|
|
72
|
+
phase: 'cache_reject',
|
|
73
|
+
bytes: entryBytes,
|
|
74
|
+
reason: 'entry_too_large',
|
|
75
|
+
});
|
|
58
76
|
throw new McpError(ErrorCode.E_TOO_LARGE, `Resource too large to cache (${entryBytes} bytes)`);
|
|
59
77
|
}
|
|
60
78
|
const contentHash = computeSha256(params.text);
|
|
@@ -62,6 +80,12 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
62
80
|
if (existingUri !== undefined) {
|
|
63
81
|
const cached = byUri.get(existingUri);
|
|
64
82
|
if (cached !== undefined) {
|
|
83
|
+
publishResourceStoreDiagnostics({
|
|
84
|
+
phase: 'cache_hit',
|
|
85
|
+
uri: cached.uri,
|
|
86
|
+
name: cached.name,
|
|
87
|
+
bytes: cached.size,
|
|
88
|
+
});
|
|
65
89
|
return cached;
|
|
66
90
|
}
|
|
67
91
|
}
|
|
@@ -76,8 +100,21 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
76
100
|
byUri.set(uri, entry);
|
|
77
101
|
byHashIndex.set(contentHash, uri);
|
|
78
102
|
totalBytes += entryBytes;
|
|
103
|
+
publishResourceStoreDiagnostics({
|
|
104
|
+
phase: 'cache_store',
|
|
105
|
+
uri: entry.uri,
|
|
106
|
+
name: entry.name,
|
|
107
|
+
bytes: entry.size,
|
|
108
|
+
});
|
|
79
109
|
enforceLimits();
|
|
80
110
|
if (!byUri.has(uri)) {
|
|
111
|
+
publishResourceStoreDiagnostics({
|
|
112
|
+
phase: 'cache_reject',
|
|
113
|
+
uri,
|
|
114
|
+
name: entry.name,
|
|
115
|
+
bytes: entry.size,
|
|
116
|
+
reason: 'evicted_immediately',
|
|
117
|
+
});
|
|
81
118
|
throw new McpError(ErrorCode.E_TOO_LARGE, 'Resource cache full: entry evicted immediately');
|
|
82
119
|
}
|
|
83
120
|
return entry;
|
|
@@ -85,14 +122,30 @@ export function createInMemoryResourceStore(options = {}) {
|
|
|
85
122
|
function getText(uri) {
|
|
86
123
|
const existing = byUri.get(uri);
|
|
87
124
|
if (!existing) {
|
|
125
|
+
publishResourceStoreDiagnostics({
|
|
126
|
+
phase: 'cache_miss',
|
|
127
|
+
uri,
|
|
128
|
+
reason: 'not_found',
|
|
129
|
+
});
|
|
88
130
|
throw new McpError(ErrorCode.E_NOT_FOUND, `Resource not found: ${uri}. The cached result may have been evicted. Re-run the originating tool to regenerate.`);
|
|
89
131
|
}
|
|
132
|
+
publishResourceStoreDiagnostics({
|
|
133
|
+
phase: 'cache_hit',
|
|
134
|
+
uri: existing.uri,
|
|
135
|
+
name: existing.name,
|
|
136
|
+
bytes: existing.size,
|
|
137
|
+
});
|
|
90
138
|
return existing;
|
|
91
139
|
}
|
|
92
140
|
function clear() {
|
|
141
|
+
const bytesBeforeClear = totalBytes;
|
|
93
142
|
byUri.clear();
|
|
94
143
|
byHashIndex.clear();
|
|
95
144
|
totalBytes = 0;
|
|
145
|
+
publishResourceStoreDiagnostics({
|
|
146
|
+
phase: 'cache_clear',
|
|
147
|
+
bytes: bytesBeforeClear,
|
|
148
|
+
});
|
|
96
149
|
}
|
|
97
150
|
return { putText, getText, clear };
|
|
98
151
|
}
|
package/dist/prompts.js
CHANGED
|
@@ -1,21 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { withDefaultIcons } from './tools/shared.js';
|
|
2
3
|
const HELP_PROMPT_NAME = 'get-help';
|
|
3
4
|
const HELP_PROMPT_TITLE = 'Get Help';
|
|
4
5
|
const HELP_PROMPT_DESCRIPTION = 'Return the filesystem-mcp usage instructions.';
|
|
6
|
+
function filterInstructionsByTopic(instructions, topic) {
|
|
7
|
+
const normalized = topic.trim().toLowerCase();
|
|
8
|
+
if (!normalized)
|
|
9
|
+
return instructions;
|
|
10
|
+
const sections = instructions.split(/\n(?=## )/u);
|
|
11
|
+
const match = sections.find((sec) => sec.toLowerCase().startsWith(`## ${normalized}`));
|
|
12
|
+
return match ?? instructions;
|
|
13
|
+
}
|
|
5
14
|
export function registerGetHelpPrompt(server, instructions, iconInfo) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const baseConfig = withDefaultIcons({ title: HELP_PROMPT_TITLE, description: HELP_PROMPT_DESCRIPTION }, iconInfo);
|
|
16
|
+
server.registerPrompt(HELP_PROMPT_NAME, {
|
|
17
|
+
...baseConfig,
|
|
18
|
+
argsSchema: {
|
|
19
|
+
topic: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('Section heading prefix to filter (e.g. "error handling strategy"). Omit for full instructions.'),
|
|
23
|
+
},
|
|
24
|
+
}, ({ topic }) => {
|
|
25
|
+
const text = topic
|
|
26
|
+
? filterInstructionsByTopic(instructions, topic)
|
|
27
|
+
: instructions;
|
|
28
|
+
return {
|
|
29
|
+
description: HELP_PROMPT_DESCRIPTION,
|
|
30
|
+
messages: [
|
|
31
|
+
{
|
|
32
|
+
role: 'user',
|
|
33
|
+
content: {
|
|
34
|
+
type: 'text',
|
|
35
|
+
text,
|
|
36
|
+
},
|
|
17
37
|
},
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
})
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
});
|
|
21
41
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildServerInstructions(): string;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ALL_TOOLS } from '../tools.js';
|
|
2
|
+
const INSTRUCTIONS_HEADER = `# FILESYSTEM-MCP INSTRUCTIONS
|
|
3
|
+
|
|
4
|
+
> Resource: \`internal://instructions\` | Prompt: \`get-help\`
|
|
5
|
+
|
|
6
|
+
## CORE CAPABILITY
|
|
7
|
+
|
|
8
|
+
- **Domain:** Safe local filesystem operations (read/write/diff/patch) within allowed roots.
|
|
9
|
+
- **Tools:**
|
|
10
|
+
- READ: \`roots\`, \`ls\`, \`find\`, \`tree\`, \`read\`, \`read_many\`, \`stat\`, \`stat_many\`, \`grep\`, \`calculate_hash\`, \`diff_files\`.
|
|
11
|
+
- WRITE: \`mkdir\`, \`write\`, \`edit\`, \`mv\`, \`rm\`, \`apply_patch\`, \`search_and_replace\`.
|
|
12
|
+
|
|
13
|
+
## RESOURCES
|
|
14
|
+
|
|
15
|
+
- \`filesystem-mcp://result/{id}\`: Ephemeral cached output.
|
|
16
|
+
- \`filesystem-mcp://metrics\`: Live tool stats.
|
|
17
|
+
- **Tip:** If response has \`resourceUri\`, call \`resources/read\` to fetch full content.
|
|
18
|
+
|
|
19
|
+
## PROGRESS & TASKS
|
|
20
|
+
|
|
21
|
+
- Support \`_meta.progressToken\` for updates.
|
|
22
|
+
- Task tools: \`find\`, \`tree\`, \`read\`, \`read_many\`, \`stat_many\`, \`grep\`, \`mkdir\`, \`write\`, \`mv\`, \`rm\`, \`calculate_hash\`, \`apply_patch\`, \`search_and_replace\`.
|
|
23
|
+
- Flow: \`tools/call\` (task) → \`tasks/get\` → \`tasks/result\`.
|
|
24
|
+
|
|
25
|
+
## GOLDEN PATH WORKFLOWS
|
|
26
|
+
|
|
27
|
+
### A: EXPLORE
|
|
28
|
+
1. \`roots\` (List allowed paths).
|
|
29
|
+
2. \`ls\` (files) | \`tree\` (structure).
|
|
30
|
+
3. \`stat\` | \`stat_many\` (size/type check).
|
|
31
|
+
4. \`read\` | \`read_many\` (content).
|
|
32
|
+
> **Strict:** Never guess paths. Resolve first.
|
|
33
|
+
|
|
34
|
+
### B: SEARCH
|
|
35
|
+
1. \`find\` (glob candidates).
|
|
36
|
+
2. \`grep\` (content search).
|
|
37
|
+
3. \`read\` (verify context).
|
|
38
|
+
> **Tip:** Content search requires \`grep\`, not \`find\`.
|
|
39
|
+
|
|
40
|
+
### C: EDIT
|
|
41
|
+
1. \`edit\` (precise string match).
|
|
42
|
+
2. \`search_and_replace\` (bulk regex/glob).
|
|
43
|
+
3. \`mv\` | \`rm\` (file layout).
|
|
44
|
+
4. \`mkdir\` (create dirs).
|
|
45
|
+
> **Strict:** Confirm destructive ops (\`write\`, \`mv\`, \`rm\`, bulk replace).
|
|
46
|
+
|
|
47
|
+
### D: PATCH
|
|
48
|
+
1. \`diff_files\` (generate).
|
|
49
|
+
2. \`apply_patch\` (dryRun: true).
|
|
50
|
+
3. \`apply_patch\` (dryRun: false).
|
|
51
|
+
> **Tip:** Use \`diff_files\` output directly.
|
|
52
|
+
`;
|
|
53
|
+
const INSTRUCTIONS_FOOTER = `
|
|
54
|
+
## CONSTRAINTS
|
|
55
|
+
|
|
56
|
+
- **Scope:** Allowed roots only (negotiated via CLI).
|
|
57
|
+
- **Security:** Sensitive files denylisted by default.
|
|
58
|
+
- **Limits:** Max file size & search results enforced.
|
|
59
|
+
- **Cache:** Externalized results are ephemeral (in-memory).
|
|
60
|
+
|
|
61
|
+
## ERROR HANDLING
|
|
62
|
+
|
|
63
|
+
- \`E_ACCESS_DENIED\` → Call \`roots\`; use allowed path.
|
|
64
|
+
- \`E_NOT_FOUND\` → Call \`ls\`/\`find\`; verify spelling.
|
|
65
|
+
- \`E_TOO_LARGE\` → Use range/head or \`read_many\`.
|
|
66
|
+
- \`E_TIMEOUT\` → Reduce scope or result limits.
|
|
67
|
+
`;
|
|
68
|
+
function formatToolSection(tool) {
|
|
69
|
+
const parts = [`${tool.name}: ${tool.description}`];
|
|
70
|
+
if (tool.annotations) {
|
|
71
|
+
const attrs = [];
|
|
72
|
+
if (tool.annotations.destructiveHint)
|
|
73
|
+
attrs.push('[Destructive]');
|
|
74
|
+
if (tool.annotations.idempotentHint)
|
|
75
|
+
attrs.push('[Idempotent]');
|
|
76
|
+
if (tool.annotations.readOnlyHint)
|
|
77
|
+
attrs.push('[Read-Only]');
|
|
78
|
+
if (attrs.length > 0)
|
|
79
|
+
parts.push(attrs.join(' '));
|
|
80
|
+
}
|
|
81
|
+
if (tool.nuances && tool.nuances.length > 0) {
|
|
82
|
+
parts.push(...tool.nuances.map((n) => `! ${n}`));
|
|
83
|
+
}
|
|
84
|
+
if (tool.gotchas && tool.gotchas.length > 0) {
|
|
85
|
+
parts.push(...tool.gotchas.map((g) => `! ${g}`));
|
|
86
|
+
}
|
|
87
|
+
return parts.join('\n');
|
|
88
|
+
}
|
|
89
|
+
export function buildServerInstructions() {
|
|
90
|
+
const toolSections = ALL_TOOLS.map(formatToolSection).join('\n\n');
|
|
91
|
+
return [
|
|
92
|
+
INSTRUCTIONS_HEADER,
|
|
93
|
+
'## TOOL REFERENCE',
|
|
94
|
+
'',
|
|
95
|
+
toolSections,
|
|
96
|
+
'',
|
|
97
|
+
'---',
|
|
98
|
+
INSTRUCTIONS_FOOTER,
|
|
99
|
+
].join('\n');
|
|
100
|
+
}
|
package/dist/resources.d.ts
CHANGED
|
@@ -3,3 +3,4 @@ import type { ResourceStore } from './lib/resource-store.js';
|
|
|
3
3
|
import { type IconInfo } from './tools/shared.js';
|
|
4
4
|
export declare function registerInstructionResource(server: McpServer, instructions: string, iconInfo?: IconInfo): void;
|
|
5
5
|
export declare function registerResultResources(server: McpServer, store: ResourceStore, iconInfo?: IconInfo): void;
|
|
6
|
+
export declare function registerMetricsResource(server: McpServer, iconInfo?: IconInfo): void;
|
package/dist/resources.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { ErrorCode, McpError } from './lib/errors.js';
|
|
3
|
+
import { globalMetrics } from './lib/observability.js';
|
|
3
4
|
import { withDefaultIcons } from './tools/shared.js';
|
|
4
5
|
const RESULT_TEMPLATE = new ResourceTemplate('filesystem-mcp://result/{id}', {
|
|
5
6
|
list: undefined,
|
|
@@ -9,6 +10,9 @@ const INSTRUCTIONS_RESOURCE_URI = 'internal://instructions';
|
|
|
9
10
|
const INSTRUCTIONS_RESOURCE_DESCRIPTION = 'Guidance for using the filesystem-mcp MCP tools effectively.';
|
|
10
11
|
const RESULT_RESOURCE_NAME = 'filesystem-mcp-result';
|
|
11
12
|
const RESULT_RESOURCE_DESCRIPTION = 'Ephemeral cached tool output exposed as an MCP resource. Not guaranteed to be listed via resources/list.';
|
|
13
|
+
const METRICS_RESOURCE_NAME = 'filesystem-mcp-metrics';
|
|
14
|
+
const METRICS_RESOURCE_URI = 'filesystem-mcp://metrics';
|
|
15
|
+
const METRICS_RESOURCE_DESCRIPTION = 'Live per-tool call/error/avgDurationMs metrics snapshot.';
|
|
12
16
|
export function registerInstructionResource(server, instructions, iconInfo) {
|
|
13
17
|
server.registerResource(INSTRUCTIONS_RESOURCE_NAME, INSTRUCTIONS_RESOURCE_URI, withDefaultIcons({
|
|
14
18
|
title: 'Server Instructions',
|
|
@@ -40,7 +44,7 @@ export function registerResultResources(server, store, iconInfo) {
|
|
|
40
44
|
}, iconInfo), (uri, variables) => {
|
|
41
45
|
const { id } = variables;
|
|
42
46
|
if (typeof id !== 'string' || id.length === 0) {
|
|
43
|
-
throw new McpError(ErrorCode.
|
|
47
|
+
throw new McpError(ErrorCode.E_NOT_FOUND, 'Cached result has expired — re-run the tool to regenerate.');
|
|
44
48
|
}
|
|
45
49
|
const entry = store.getText(uri.toString());
|
|
46
50
|
return {
|
|
@@ -54,3 +58,34 @@ export function registerResultResources(server, store, iconInfo) {
|
|
|
54
58
|
};
|
|
55
59
|
});
|
|
56
60
|
}
|
|
61
|
+
export function registerMetricsResource(server, iconInfo) {
|
|
62
|
+
server.registerResource(METRICS_RESOURCE_NAME, METRICS_RESOURCE_URI, withDefaultIcons({
|
|
63
|
+
title: 'Tool Metrics',
|
|
64
|
+
description: METRICS_RESOURCE_DESCRIPTION,
|
|
65
|
+
mimeType: 'application/json',
|
|
66
|
+
annotations: {
|
|
67
|
+
audience: ['assistant'],
|
|
68
|
+
priority: 0.5,
|
|
69
|
+
},
|
|
70
|
+
}, iconInfo), (uri) => {
|
|
71
|
+
const snapshot = {};
|
|
72
|
+
for (const [tool, m] of globalMetrics) {
|
|
73
|
+
snapshot[tool] = {
|
|
74
|
+
calls: m.calls,
|
|
75
|
+
errors: m.errors,
|
|
76
|
+
avgDurationMs: m.calls > 0
|
|
77
|
+
? parseFloat((m.totalDurationMs / m.calls).toFixed(2))
|
|
78
|
+
: 0,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
contents: [
|
|
83
|
+
{
|
|
84
|
+
uri: uri.href,
|
|
85
|
+
mimeType: 'application/json',
|
|
86
|
+
text: JSON.stringify({ ok: true, metrics: snapshot }, null, 2),
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
package/dist/schemas.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export declare const ListDirectoryInputSchema: z.ZodObject<{
|
|
|
47
47
|
}>>>;
|
|
48
48
|
pattern: z.ZodOptional<z.ZodString>;
|
|
49
49
|
includeSymlinkTargets: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
50
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
50
51
|
}, z.core.$strict>;
|
|
51
52
|
export declare const ListAllowedDirectoriesInputSchema: z.ZodObject<{}, z.core.$strict>;
|
|
52
53
|
export declare const SearchFilesInputSchema: z.ZodObject<{
|
|
@@ -62,6 +63,7 @@ export declare const SearchFilesInputSchema: z.ZodObject<{
|
|
|
62
63
|
modified: "modified";
|
|
63
64
|
}>>>;
|
|
64
65
|
maxDepth: z.ZodOptional<z.ZodNumber>;
|
|
66
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
65
67
|
}, z.core.$strict>;
|
|
66
68
|
export declare const TreeInputSchema: z.ZodObject<{
|
|
67
69
|
path: z.ZodOptional<z.ZodString>;
|
|
@@ -153,6 +155,7 @@ export declare const ListDirectoryOutputSchema: z.ZodObject<{
|
|
|
153
155
|
}>>;
|
|
154
156
|
skippedInaccessible: z.ZodOptional<z.ZodNumber>;
|
|
155
157
|
symlinksNotFollowed: z.ZodOptional<z.ZodNumber>;
|
|
158
|
+
nextCursor: z.ZodOptional<z.ZodString>;
|
|
156
159
|
error: z.ZodOptional<z.ZodObject<{
|
|
157
160
|
code: z.ZodEnum<{
|
|
158
161
|
readonly E_ACCESS_DENIED: "E_ACCESS_DENIED";
|
|
@@ -211,6 +214,7 @@ export declare const SearchFilesOutputSchema: z.ZodObject<{
|
|
|
211
214
|
maxFiles: "maxFiles";
|
|
212
215
|
timeout: "timeout";
|
|
213
216
|
}>>;
|
|
217
|
+
nextCursor: z.ZodOptional<z.ZodString>;
|
|
214
218
|
}, z.core.$strict>;
|
|
215
219
|
export declare const SearchContentOutputSchema: z.ZodObject<{
|
|
216
220
|
totalMatches: z.ZodOptional<z.ZodNumber>;
|
|
@@ -711,6 +715,8 @@ export declare const SearchAndReplaceInputSchema: z.ZodObject<{
|
|
|
711
715
|
replacement: z.ZodString;
|
|
712
716
|
isRegex: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
713
717
|
dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
718
|
+
includeHidden: z.ZodOptional<z.ZodBoolean>;
|
|
719
|
+
includeIgnored: z.ZodOptional<z.ZodBoolean>;
|
|
714
720
|
}, z.core.$strict>;
|
|
715
721
|
export declare const SearchAndReplaceOutputSchema: z.ZodObject<{
|
|
716
722
|
ok: z.ZodBoolean;
|
package/dist/schemas.js
CHANGED
|
@@ -135,6 +135,10 @@ export const ListDirectoryInputSchema = z.strictObject({
|
|
|
135
135
|
.optional()
|
|
136
136
|
.default(false)
|
|
137
137
|
.describe('Resolve and include symlink targets in results'),
|
|
138
|
+
cursor: z
|
|
139
|
+
.string()
|
|
140
|
+
.optional()
|
|
141
|
+
.describe('Pagination cursor from a previous response'),
|
|
138
142
|
});
|
|
139
143
|
export const ListAllowedDirectoriesInputSchema = z
|
|
140
144
|
.strictObject({})
|
|
@@ -177,6 +181,10 @@ export const SearchFilesInputSchema = z.strictObject({
|
|
|
177
181
|
.max(100, 'Max: 100')
|
|
178
182
|
.optional()
|
|
179
183
|
.describe('Maximum directory depth to scan'),
|
|
184
|
+
cursor: z
|
|
185
|
+
.string()
|
|
186
|
+
.optional()
|
|
187
|
+
.describe('Pagination cursor from a previous response'),
|
|
180
188
|
});
|
|
181
189
|
export const TreeInputSchema = z.strictObject({
|
|
182
190
|
path: OptionalPathSchema.describe(DESC_PATH_ROOT),
|
|
@@ -325,6 +333,10 @@ export const ListDirectoryOutputSchema = z.strictObject({
|
|
|
325
333
|
stoppedReason: ListDirectoryStopReasonSchema.optional(),
|
|
326
334
|
skippedInaccessible: z.number().optional(),
|
|
327
335
|
symlinksNotFollowed: z.number().optional(),
|
|
336
|
+
nextCursor: z
|
|
337
|
+
.string()
|
|
338
|
+
.optional()
|
|
339
|
+
.describe('Cursor for the next page; absent on the final page'),
|
|
328
340
|
error: ErrorSchema.optional(),
|
|
329
341
|
});
|
|
330
342
|
const SearchSummarySchema = z.strictObject({
|
|
@@ -347,6 +359,10 @@ export const SearchFilesOutputSchema = SearchSummarySchema.extend({
|
|
|
347
359
|
filesScanned: z.number().optional().describe('Files scanned'),
|
|
348
360
|
skippedInaccessible: z.number().optional().describe('Inaccessible files'),
|
|
349
361
|
stoppedReason: SearchStopReasonSchema.optional().describe('Why search stopped'),
|
|
362
|
+
nextCursor: z
|
|
363
|
+
.string()
|
|
364
|
+
.optional()
|
|
365
|
+
.describe('Cursor for the next page; absent on the final page'),
|
|
350
366
|
});
|
|
351
367
|
export const SearchContentOutputSchema = SearchSummarySchema.extend({
|
|
352
368
|
ok: z.boolean(),
|
|
@@ -613,6 +629,14 @@ export const SearchAndReplaceInputSchema = z.strictObject({
|
|
|
613
629
|
.optional()
|
|
614
630
|
.default(false)
|
|
615
631
|
.describe('Preview matches without writing. Check changedFiles and matches in the response before committing.'),
|
|
632
|
+
includeHidden: z
|
|
633
|
+
.boolean()
|
|
634
|
+
.optional()
|
|
635
|
+
.describe('Include hidden files and directories (starting with .) in the search scope. Default: false.'),
|
|
636
|
+
includeIgnored: z
|
|
637
|
+
.boolean()
|
|
638
|
+
.optional()
|
|
639
|
+
.describe('Include files and directories ignored by .gitignore rules (e.g. node_modules, dist). Default: false.'),
|
|
616
640
|
});
|
|
617
641
|
export const SearchAndReplaceOutputSchema = z.strictObject({
|
|
618
642
|
ok: z.boolean(),
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises';
|
|
2
2
|
import * as http from 'node:http';
|
|
3
|
-
import
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
6
4
|
import { InMemoryTaskMessageQueue, InMemoryTaskStore, } from '@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js';
|
|
7
5
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -13,7 +11,8 @@ import { formatUnknownErrorMessage } from '../lib/errors.js';
|
|
|
13
11
|
import { createInMemoryResourceStore } from '../lib/resource-store.js';
|
|
14
12
|
import { pkgInfo } from '../pkg-info.js';
|
|
15
13
|
import { registerGetHelpPrompt } from '../prompts.js';
|
|
16
|
-
import { registerInstructionResource, registerResultResources, } from '../resources.js';
|
|
14
|
+
import { registerInstructionResource, registerMetricsResource, registerResultResources, } from '../resources.js';
|
|
15
|
+
import { buildServerInstructions } from '../resources/generated-instructions.js';
|
|
17
16
|
import { registerAllTools } from '../tools.js';
|
|
18
17
|
import { withDefaultIcons } from '../tools/shared.js';
|
|
19
18
|
import { buildServerCapabilities, supportsTaskToolRequests, } from './capabilities.js';
|
|
@@ -28,19 +27,8 @@ function getRootsManager(server) {
|
|
|
28
27
|
}
|
|
29
28
|
return manager;
|
|
30
29
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Filesystem MCP Instructions
|
|
34
|
-
(Detailed instructions failed to load - check logs)
|
|
35
|
-
`;
|
|
36
|
-
try {
|
|
37
|
-
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
38
|
-
return await fs.readFile(path.join(currentDir, '../instructions.md'), 'utf-8');
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
console.error('[WARNING] Failed to load instructions.md:', formatUnknownErrorMessage(error));
|
|
42
|
-
return defaultInstructions;
|
|
43
|
-
}
|
|
30
|
+
function loadServerInstructions() {
|
|
31
|
+
return buildServerInstructions();
|
|
44
32
|
}
|
|
45
33
|
async function getLocalIconInfo() {
|
|
46
34
|
const name = 'logo.svg';
|
|
@@ -63,7 +51,7 @@ async function getLocalIconInfo() {
|
|
|
63
51
|
}
|
|
64
52
|
export async function createServer(options = {}) {
|
|
65
53
|
const resourceStore = createInMemoryResourceStore();
|
|
66
|
-
const serverInstructions =
|
|
54
|
+
const serverInstructions = loadServerInstructions();
|
|
67
55
|
const localIcon = await getLocalIconInfo();
|
|
68
56
|
const taskToolSupport = supportsTaskToolRequests();
|
|
69
57
|
const serverConfig = {
|
|
@@ -77,7 +65,10 @@ export async function createServer(options = {}) {
|
|
|
77
65
|
serverConfig.taskMessageQueue = new InMemoryTaskMessageQueue();
|
|
78
66
|
}
|
|
79
67
|
if (serverInstructions) {
|
|
80
|
-
serverConfig.instructions =
|
|
68
|
+
serverConfig.instructions =
|
|
69
|
+
'filesystem-mcp: Secure local filesystem MCP server. ' +
|
|
70
|
+
'Essential sequence: roots → ls/tree/find → read/grep. ' +
|
|
71
|
+
'Full reference: read the internal://instructions resource or invoke the get-help prompt.';
|
|
81
72
|
}
|
|
82
73
|
const server = new McpServer(withDefaultIcons({
|
|
83
74
|
name: 'filesystem-mcp',
|
|
@@ -96,7 +87,8 @@ export async function createServer(options = {}) {
|
|
|
96
87
|
registerInstructionResource(server, serverInstructions, localIcon);
|
|
97
88
|
registerGetHelpPrompt(server, serverInstructions, localIcon);
|
|
98
89
|
registerResultResources(server, resourceStore, localIcon);
|
|
99
|
-
|
|
90
|
+
registerMetricsResource(server, localIcon);
|
|
91
|
+
registerCompletions(server, serverInstructions);
|
|
100
92
|
registerAllTools(server, {
|
|
101
93
|
resourceStore,
|
|
102
94
|
isInitialized: () => rootsManager.isInitialized(),
|
|
@@ -173,6 +165,31 @@ export async function startHttpServer(port, options) {
|
|
|
173
165
|
async function handleMcpRequest(req, res) {
|
|
174
166
|
const { method } = req;
|
|
175
167
|
const sessionId = req.headers['mcp-session-id'];
|
|
168
|
+
const apiKey = process.env['FILESYSTEM_MCP_API_KEY'];
|
|
169
|
+
if (apiKey) {
|
|
170
|
+
const authHeader = req.headers['authorization'];
|
|
171
|
+
const bearerPrefix = 'Bearer ';
|
|
172
|
+
let authorized = false;
|
|
173
|
+
if (typeof authHeader === 'string' &&
|
|
174
|
+
authHeader.startsWith(bearerPrefix)) {
|
|
175
|
+
const userKey = authHeader.slice(bearerPrefix.length);
|
|
176
|
+
const expectedHash = createHash('sha256').update(apiKey).digest();
|
|
177
|
+
const actualHash = createHash('sha256').update(userKey).digest();
|
|
178
|
+
authorized = timingSafeEqual(expectedHash, actualHash);
|
|
179
|
+
}
|
|
180
|
+
if (!authorized) {
|
|
181
|
+
res.writeHead(401, {
|
|
182
|
+
'Content-Type': 'application/json',
|
|
183
|
+
'WWW-Authenticate': 'Bearer',
|
|
184
|
+
});
|
|
185
|
+
res.end(JSON.stringify({
|
|
186
|
+
jsonrpc: '2.0',
|
|
187
|
+
error: { code: -32000, message: 'Unauthorized' },
|
|
188
|
+
id: null,
|
|
189
|
+
}));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
176
193
|
try {
|
|
177
194
|
if (method === 'POST') {
|
|
178
195
|
const body = await readRequestBody(req);
|
|
@@ -230,7 +247,10 @@ export async function startHttpServer(port, options) {
|
|
|
230
247
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
231
248
|
res.end(JSON.stringify({
|
|
232
249
|
jsonrpc: '2.0',
|
|
233
|
-
error: {
|
|
250
|
+
error: {
|
|
251
|
+
code: -32000,
|
|
252
|
+
message: 'Bad Request: Session not found',
|
|
253
|
+
},
|
|
234
254
|
id: null,
|
|
235
255
|
}));
|
|
236
256
|
}
|
|
@@ -256,7 +276,10 @@ export async function startHttpServer(port, options) {
|
|
|
256
276
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
257
277
|
res.end(JSON.stringify({
|
|
258
278
|
jsonrpc: '2.0',
|
|
259
|
-
error: {
|
|
279
|
+
error: {
|
|
280
|
+
code: -32000,
|
|
281
|
+
message: 'Bad Request: Session not found',
|
|
282
|
+
},
|
|
260
283
|
id: null,
|
|
261
284
|
}));
|
|
262
285
|
}
|