@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.
Files changed (66) hide show
  1. package/README.md +8 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +13 -1
  4. package/dist/completions.d.ts +1 -1
  5. package/dist/completions.js +36 -1
  6. package/dist/index.js +26 -8
  7. package/dist/lib/observability.d.ts +6 -0
  8. package/dist/lib/observability.js +1 -1
  9. package/dist/lib/resource-store.js +53 -0
  10. package/dist/prompts.js +34 -14
  11. package/dist/resources/generated-instructions.d.ts +1 -0
  12. package/dist/resources/generated-instructions.js +100 -0
  13. package/dist/resources.d.ts +1 -0
  14. package/dist/resources.js +36 -1
  15. package/dist/schemas.d.ts +6 -0
  16. package/dist/schemas.js +24 -0
  17. package/dist/server/bootstrap.d.ts +2 -0
  18. package/dist/server/bootstrap.js +226 -20
  19. package/dist/server.d.ts +1 -1
  20. package/dist/server.js +1 -1
  21. package/dist/tools/apply-patch.d.ts +2 -1
  22. package/dist/tools/apply-patch.js +7 -5
  23. package/dist/tools/calculate-hash.d.ts +2 -1
  24. package/dist/tools/calculate-hash.js +9 -5
  25. package/dist/tools/contract.d.ts +41 -0
  26. package/dist/tools/contract.js +1 -0
  27. package/dist/tools/create-directory.d.ts +2 -1
  28. package/dist/tools/create-directory.js +6 -5
  29. package/dist/tools/delete-file.d.ts +2 -1
  30. package/dist/tools/delete-file.js +9 -5
  31. package/dist/tools/diff-files.d.ts +2 -1
  32. package/dist/tools/diff-files.js +7 -4
  33. package/dist/tools/edit-file.d.ts +2 -1
  34. package/dist/tools/edit-file.js +19 -6
  35. package/dist/tools/list-directory.d.ts +2 -1
  36. package/dist/tools/list-directory.js +36 -7
  37. package/dist/tools/move-file.d.ts +2 -1
  38. package/dist/tools/move-file.js +7 -5
  39. package/dist/tools/read-multiple.d.ts +2 -1
  40. package/dist/tools/read-multiple.js +10 -5
  41. package/dist/tools/read.d.ts +2 -1
  42. package/dist/tools/read.js +9 -5
  43. package/dist/tools/replace-in-files.d.ts +2 -1
  44. package/dist/tools/replace-in-files.js +14 -7
  45. package/dist/tools/roots.d.ts +2 -1
  46. package/dist/tools/roots.js +7 -4
  47. package/dist/tools/search-content.d.ts +2 -1
  48. package/dist/tools/search-content.js +14 -6
  49. package/dist/tools/search-files.d.ts +2 -1
  50. package/dist/tools/search-files.js +39 -7
  51. package/dist/tools/shared.d.ts +2 -2
  52. package/dist/tools/shared.js +16 -1
  53. package/dist/tools/stat-many.d.ts +2 -1
  54. package/dist/tools/stat-many.js +7 -5
  55. package/dist/tools/stat.d.ts +2 -1
  56. package/dist/tools/stat.js +7 -4
  57. package/dist/tools/task-support.d.ts +2 -0
  58. package/dist/tools/task-support.js +48 -7
  59. package/dist/tools/tree.d.ts +2 -1
  60. package/dist/tools/tree.js +7 -5
  61. package/dist/tools/write-file.d.ts +2 -1
  62. package/dist/tools/write-file.js +12 -5
  63. package/dist/tools.d.ts +2 -0
  64. package/dist/tools.js +39 -18
  65. package/package.json +1 -2
  66. 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
@@ -5,4 +5,5 @@ export declare class CliExitError extends Error {
5
5
  export declare function parseArgs(): Promise<{
6
6
  allowedDirs: string[];
7
7
  allowCwd: boolean;
8
+ port: number | undefined;
8
9
  }>;
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
  }
@@ -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 {};
@@ -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
- const server = await createServer({
82
- allowCwd,
83
- cliAllowedDirs: allowedDirs,
84
- });
85
- activeServer = server;
86
- await startServer(server);
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;
@@ -24,7 +24,7 @@ function parseDetail(val) {
24
24
  return 1;
25
25
  return 0;
26
26
  }
27
- const globalMetrics = new Map();
27
+ export const globalMetrics = new Map();
28
28
  function updateMetrics(tool, ok, durationMs) {
29
29
  const current = globalMetrics.get(tool) ?? {
30
30
  calls: 0,
@@ -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
- server.registerPrompt(HELP_PROMPT_NAME, withDefaultIcons({
7
- title: HELP_PROMPT_TITLE,
8
- description: HELP_PROMPT_DESCRIPTION,
9
- }, iconInfo), () => ({
10
- description: HELP_PROMPT_DESCRIPTION,
11
- messages: [
12
- {
13
- role: 'user',
14
- content: {
15
- type: 'text',
16
- text: instructions,
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
+ }
@@ -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.E_INVALID_INPUT, 'Missing resource id');
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>;