@j0hanz/filesystem-mcp 1.2.4 → 1.3.1

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 (62) hide show
  1. package/README.md +8 -0
  2. package/dist/completions.d.ts +1 -1
  3. package/dist/completions.js +36 -1
  4. package/dist/lib/observability.d.ts +6 -0
  5. package/dist/lib/observability.js +1 -1
  6. package/dist/lib/resource-store.js +53 -0
  7. package/dist/prompts.js +34 -14
  8. package/dist/resources/generated-instructions.d.ts +1 -0
  9. package/dist/resources/generated-instructions.js +100 -0
  10. package/dist/resources.d.ts +1 -0
  11. package/dist/resources.js +36 -1
  12. package/dist/schemas.d.ts +6 -0
  13. package/dist/schemas.js +24 -0
  14. package/dist/server/bootstrap.js +45 -22
  15. package/dist/server.d.ts +1 -1
  16. package/dist/server.js +1 -1
  17. package/dist/tools/apply-patch.d.ts +2 -1
  18. package/dist/tools/apply-patch.js +7 -5
  19. package/dist/tools/calculate-hash.d.ts +2 -1
  20. package/dist/tools/calculate-hash.js +9 -5
  21. package/dist/tools/contract.d.ts +41 -0
  22. package/dist/tools/contract.js +1 -0
  23. package/dist/tools/create-directory.d.ts +2 -1
  24. package/dist/tools/create-directory.js +6 -5
  25. package/dist/tools/delete-file.d.ts +2 -1
  26. package/dist/tools/delete-file.js +9 -5
  27. package/dist/tools/diff-files.d.ts +2 -1
  28. package/dist/tools/diff-files.js +7 -4
  29. package/dist/tools/edit-file.d.ts +2 -1
  30. package/dist/tools/edit-file.js +19 -6
  31. package/dist/tools/list-directory.d.ts +2 -1
  32. package/dist/tools/list-directory.js +36 -7
  33. package/dist/tools/move-file.d.ts +2 -1
  34. package/dist/tools/move-file.js +7 -5
  35. package/dist/tools/read-multiple.d.ts +2 -1
  36. package/dist/tools/read-multiple.js +10 -5
  37. package/dist/tools/read.d.ts +2 -1
  38. package/dist/tools/read.js +9 -5
  39. package/dist/tools/replace-in-files.d.ts +2 -1
  40. package/dist/tools/replace-in-files.js +14 -7
  41. package/dist/tools/roots.d.ts +2 -1
  42. package/dist/tools/roots.js +7 -4
  43. package/dist/tools/search-content.d.ts +2 -1
  44. package/dist/tools/search-content.js +14 -6
  45. package/dist/tools/search-files.d.ts +2 -1
  46. package/dist/tools/search-files.js +39 -7
  47. package/dist/tools/shared.d.ts +2 -2
  48. package/dist/tools/shared.js +16 -1
  49. package/dist/tools/stat-many.d.ts +2 -1
  50. package/dist/tools/stat-many.js +7 -5
  51. package/dist/tools/stat.d.ts +2 -1
  52. package/dist/tools/stat.js +7 -4
  53. package/dist/tools/task-support.d.ts +2 -0
  54. package/dist/tools/task-support.js +61 -9
  55. package/dist/tools/tree.d.ts +2 -1
  56. package/dist/tools/tree.js +7 -5
  57. package/dist/tools/write-file.d.ts +2 -1
  58. package/dist/tools/write-file.js +12 -5
  59. package/dist/tools.d.ts +2 -0
  60. package/dist/tools.js +39 -18
  61. package/package.json +1 -2
  62. 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:
@@ -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, {
@@ -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,8 +1,6 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as http from 'node:http';
3
- import * as path from 'node:path';
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
- async function loadServerInstructions() {
32
- const defaultInstructions = `
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 = await loadServerInstructions();
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 = serverInstructions;
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
- registerCompletions(server);
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: { code: -32000, message: 'Bad Request: Session not found' },
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: { code: -32000, message: 'Bad Request: Session not found' },
279
+ error: {
280
+ code: -32000,
281
+ message: 'Bad Request: Session not found',
282
+ },
260
283
  id: null,
261
284
  }));
262
285
  }