@j0hanz/filesystem-mcp 1.2.3 → 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/cli.d.ts +1 -0
- package/dist/cli.js +13 -1
- package/dist/completions.d.ts +1 -1
- package/dist/completions.js +36 -1
- package/dist/index.js +26 -8
- 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.d.ts +2 -0
- package/dist/server/bootstrap.js +226 -20
- 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/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -123,6 +123,7 @@ function createCliProgram(output) {
|
|
|
123
123
|
.description('MCP filesystem server. Positional directories define allowed access roots.')
|
|
124
124
|
.argument('[allowedDirs...]', 'Directories the MCP server can access on disk', parseAllowedDirArgument)
|
|
125
125
|
.option('--allow_cwd, --allow-cwd', 'Allow the current working directory as an additional root')
|
|
126
|
+
.option('--port <number>', 'Enable HTTP transport on the given port (MCP Streamable HTTP with SSE)')
|
|
126
127
|
.helpOption('-h, --help', 'Display command help')
|
|
127
128
|
.version(SERVER_VERSION, '-v, --version', 'Display server version')
|
|
128
129
|
.addHelpText('after', `
|
|
@@ -130,6 +131,7 @@ Examples:
|
|
|
130
131
|
$ filesystem-mcp /path/to/allowed/dir
|
|
131
132
|
$ filesystem-mcp --allow-cwd
|
|
132
133
|
$ filesystem-mcp /project/src /project/tests --allow-cwd
|
|
134
|
+
$ filesystem-mcp --port 3000 /path/to/allowed/dir
|
|
133
135
|
`);
|
|
134
136
|
cli.allowUnknownOption(false);
|
|
135
137
|
cli.allowExcessArguments(false);
|
|
@@ -171,6 +173,15 @@ function deduplicateAllowedDirectories(dirs) {
|
|
|
171
173
|
}
|
|
172
174
|
return deduplicated;
|
|
173
175
|
}
|
|
176
|
+
function parsePortOption(raw) {
|
|
177
|
+
if (raw === undefined)
|
|
178
|
+
return undefined;
|
|
179
|
+
const n = Number(raw);
|
|
180
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
181
|
+
throw new CliExitError(`Error: --port must be an integer between 1 and 65535`, 1);
|
|
182
|
+
}
|
|
183
|
+
return n;
|
|
184
|
+
}
|
|
174
185
|
export async function parseArgs() {
|
|
175
186
|
const output = [];
|
|
176
187
|
const cli = createCliProgram(output);
|
|
@@ -185,6 +196,7 @@ export async function parseArgs() {
|
|
|
185
196
|
}
|
|
186
197
|
const options = cli.opts();
|
|
187
198
|
const allowCwd = options.allowCwd === true;
|
|
199
|
+
const port = parsePortOption(options.port);
|
|
188
200
|
const positionals = getParsedAllowedDirs(cli);
|
|
189
201
|
let allowedDirs;
|
|
190
202
|
try {
|
|
@@ -195,5 +207,5 @@ export async function parseArgs() {
|
|
|
195
207
|
throw new CliExitError(normalizeCliExitMessage(error), 1);
|
|
196
208
|
}
|
|
197
209
|
const deduplicatedDirs = deduplicateAllowedDirectories(allowedDirs);
|
|
198
|
-
return { allowedDirs: deduplicatedDirs, allowCwd };
|
|
210
|
+
return { allowedDirs: deduplicatedDirs, allowCwd, port };
|
|
199
211
|
}
|
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, {
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,10 @@ import { DEFAULT_SEARCH_TIMEOUT_MS } from './lib/constants.js';
|
|
|
5
5
|
import { formatUnknownErrorMessage } from './lib/errors.js';
|
|
6
6
|
import { createTimedAbortSignal } from './lib/fs-helpers.js';
|
|
7
7
|
import { setAllowedDirectoriesResolved } from './lib/path-validation.js';
|
|
8
|
-
import { createServer, startServer } from './server.js';
|
|
8
|
+
import { createServer, startHttpServer, startServer } from './server.js';
|
|
9
9
|
const SHUTDOWN_TIMEOUT_MS = 5000;
|
|
10
10
|
let activeServer;
|
|
11
|
+
let activeHttpServer;
|
|
11
12
|
let shutdownStarted = false;
|
|
12
13
|
function isStdinEvent(event) {
|
|
13
14
|
return event === 'end' || event === 'close';
|
|
@@ -31,6 +32,14 @@ async function shutdown(reason, exitCode = 0) {
|
|
|
31
32
|
}, SHUTDOWN_TIMEOUT_MS);
|
|
32
33
|
timer.unref();
|
|
33
34
|
try {
|
|
35
|
+
if (activeHttpServer) {
|
|
36
|
+
const server = activeHttpServer;
|
|
37
|
+
await new Promise((resolve) => {
|
|
38
|
+
server.close(() => {
|
|
39
|
+
resolve();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
34
43
|
if (activeServer) {
|
|
35
44
|
await activeServer.close();
|
|
36
45
|
}
|
|
@@ -48,9 +57,10 @@ async function shutdown(reason, exitCode = 0) {
|
|
|
48
57
|
async function main() {
|
|
49
58
|
let allowedDirs;
|
|
50
59
|
let allowCwd;
|
|
60
|
+
let port;
|
|
51
61
|
try {
|
|
52
62
|
const parsed = await parseArgs();
|
|
53
|
-
({ allowedDirs, allowCwd } = parsed);
|
|
63
|
+
({ allowedDirs, allowCwd, port } = parsed);
|
|
54
64
|
}
|
|
55
65
|
catch (error) {
|
|
56
66
|
if (error instanceof CliExitError) {
|
|
@@ -78,12 +88,20 @@ async function main() {
|
|
|
78
88
|
else {
|
|
79
89
|
console.error(`No directories specified via CLI. Will use MCP Roots${allowCwd ? ' or current working directory' : ''}.`);
|
|
80
90
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
if (port !== undefined) {
|
|
92
|
+
activeHttpServer = await startHttpServer(port, {
|
|
93
|
+
allowCwd,
|
|
94
|
+
cliAllowedDirs: allowedDirs,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const server = await createServer({
|
|
99
|
+
allowCwd,
|
|
100
|
+
cliAllowedDirs: allowedDirs,
|
|
101
|
+
});
|
|
102
|
+
activeServer = server;
|
|
103
|
+
await startServer(server);
|
|
104
|
+
}
|
|
87
105
|
}
|
|
88
106
|
registerShutdownTrigger('SIGTERM');
|
|
89
107
|
registerShutdownTrigger('SIGINT');
|
|
@@ -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(),
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import * as http from 'node:http';
|
|
1
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
3
|
import type { ServerOptions } from './types.js';
|
|
3
4
|
export declare function createServer(options?: ServerOptions): Promise<McpServer>;
|
|
4
5
|
export declare function startServer(server: McpServer): Promise<void>;
|
|
6
|
+
export declare function startHttpServer(port: number, options: ServerOptions): Promise<http.Server>;
|