@j0hanz/filesystem-mcp 1.6.1 → 1.7.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 CHANGED
@@ -474,10 +474,14 @@ Replace text in all files matching a glob. Replaces **all** occurrences per file
474
474
 
475
475
  ### Resources
476
476
 
477
- | URI | Description | MIME Type |
478
- | :----------------------------- | :--------------------------------- | :-------------- |
479
- | `internal://instructions` | Usage guidance for models | `text/markdown` |
480
- | `filesystem-mcp://result/{id}` | Ephemeral cached large tool output | varies |
477
+ | URI | Description | MIME Type |
478
+ | :----------------------------- | :---------------------------------- | :----------------- |
479
+ | `internal://instructions` | Usage guidance for models | `text/markdown` |
480
+ | `internal://tool-catalog` | Tool routing and data-flow guide | `text/markdown` |
481
+ | `internal://workflows` | Explore/search/edit/patch workflows | `text/markdown` |
482
+ | `internal://tool-info/{name}` | Per-tool nuances and gotchas | `text/markdown` |
483
+ | `filesystem-mcp://metrics` | Live per-tool metrics snapshot | `application/json` |
484
+ | `filesystem-mcp://result/{id}` | Ephemeral cached large tool output | varies |
481
485
 
482
486
  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
487
 
@@ -565,7 +569,7 @@ Set `FS_CONTEXT_STRIP_STRUCTURED=1` to strip `structuredContent` from tool resul
565
569
  - **Input limits**: Paths are bounded to 4,096 characters; patterns to 1,000 characters.
566
570
  - **Atomic writes**: File writes use an atomic write-then-rename strategy to prevent partial writes.
567
571
  - **Docker**: The container runs as a non-root user (`mcp`).
568
- - **HTTP host binding**: The HTTP transport binds to `127.0.0.1` by default. Setting `FILESYSTEM_MCP_HTTP_HOST=0.0.0.0` binds to all network interfaces and exposes the server externally — only do this behind a trusted reverse proxy with `FILESYSTEM_MCP_AUTH_TOKEN` configured.
572
+ - **HTTP host binding**: The HTTP transport binds to `127.0.0.1` by default. Setting `FILESYSTEM_MCP_HTTP_HOST=0.0.0.0` binds to all network interfaces and exposes the server externally — only do this behind a trusted reverse proxy with `FILESYSTEM_MCP_API_KEY` configured.
569
573
 
570
574
  > [!IMPORTANT]
571
575
  > All diagnostic output goes to `stderr`. Tool handlers must never write to `stdout`, as doing so would corrupt the stdio transport.
@@ -580,18 +584,16 @@ npm ci
580
584
 
581
585
  ### Scripts
582
586
 
583
- | Script | Command | Purpose |
584
- | :-------------- | :-------------------------------------------------------- | :---------------------------------- |
585
- | `dev` | `tsc --watch` | Watch-mode TypeScript compilation |
586
- | `dev:run` | `node --watch dist/index.js` | Run built server with file watching |
587
- | `build` | `node scripts/tasks.mjs build` | Production build |
588
- | `test` | `node scripts/tasks.mjs test` | Run full test suite |
589
- | `test:fast` | `node --test --import tsx/esm src/__tests__/**/*.test.ts` | Fast test runner (no build step) |
590
- | `test:coverage` | `node scripts/tasks.mjs test --coverage` | Test with coverage |
591
- | `lint` | `eslint .` | Lint source files |
592
- | `lint:fix` | `eslint . --fix` | Auto-fix lint issues |
593
- | `format` | `prettier --write .` | Format all files |
594
- | `type-check` | `node scripts/tasks.mjs type-check` | TypeScript type checking |
587
+ | Script | Command | Purpose |
588
+ | :----------- | :---------------------------------- | :---------------------------------- |
589
+ | `dev` | `tsc --watch` | Watch-mode TypeScript compilation |
590
+ | `dev:run` | `node --watch dist/index.js` | Run built server with file watching |
591
+ | `build` | `node scripts/tasks.mjs build` | Production build |
592
+ | `test` | `node scripts/tasks.mjs test` | Run full test suite |
593
+ | `lint` | `eslint .` | Lint source files |
594
+ | `lint:fix` | `eslint . --fix` | Auto-fix lint issues |
595
+ | `format` | `prettier --write .` | Format all files |
596
+ | `type-check` | `node scripts/tasks.mjs type-check` | TypeScript type checking |
595
597
 
596
598
  ### MCP Inspector
597
599
 
@@ -125,15 +125,16 @@ function walkErrorChain(error, visitor) {
125
125
  let current = error;
126
126
  const visited = new Set();
127
127
  while (current !== undefined && current !== null && !visited.has(current)) {
128
- if (visitor(current))
129
- return true;
128
+ const visitedResult = visitor(current);
129
+ if (visitedResult !== undefined)
130
+ return visitedResult;
130
131
  if (!isNativeError(current))
131
132
  break;
132
133
  visited.add(current);
133
134
  const next = current.cause;
134
135
  current = next;
135
136
  }
136
- return false;
137
+ return undefined;
137
138
  }
138
139
  function isAbortErrorSingle(error) {
139
140
  if (!isNativeError(error))
@@ -144,7 +145,7 @@ function isAbortErrorSingle(error) {
144
145
  return code === 'ABORT_ERR';
145
146
  }
146
147
  export function isAbortError(error) {
147
- return walkErrorChain(error, isAbortErrorSingle);
148
+ return (walkErrorChain(error, (candidate) => isAbortErrorSingle(candidate) ? true : undefined) === true);
148
149
  }
149
150
  function isTimeoutErrorSingle(error) {
150
151
  if (!isNativeError(error))
@@ -158,7 +159,7 @@ function isTimeoutErrorSingle(error) {
158
159
  return message.includes('timed out') || message.includes('timeout');
159
160
  }
160
161
  export function isTimeoutLikeError(error) {
161
- return walkErrorChain(error, isTimeoutErrorSingle);
162
+ return (walkErrorChain(error, (candidate) => isTimeoutErrorSingle(candidate) ? true : undefined) === true);
162
163
  }
163
164
  export class McpError extends Error {
164
165
  code;
@@ -224,17 +225,20 @@ function classifyMessageError(error) {
224
225
  return undefined;
225
226
  }
226
227
  function classifyError(error) {
227
- if (isAbortError(error)) {
228
- return ErrorCode.E_CANCELLED;
229
- }
230
- if (isTimeoutLikeError(error)) {
231
- return ErrorCode.E_TIMEOUT;
232
- }
233
- const direct = getDirectErrorCode(error);
234
- if (direct)
235
- return direct;
236
- const messageCode = classifyMessageError(error);
237
- return messageCode ?? ErrorCode.E_UNKNOWN;
228
+ let timeoutCode;
229
+ let fallbackCode;
230
+ const terminalCode = walkErrorChain(error, (candidate) => {
231
+ if (isAbortErrorSingle(candidate)) {
232
+ return ErrorCode.E_CANCELLED;
233
+ }
234
+ if (timeoutCode === undefined && isTimeoutErrorSingle(candidate)) {
235
+ timeoutCode = ErrorCode.E_TIMEOUT;
236
+ }
237
+ fallbackCode ??=
238
+ getDirectErrorCode(candidate) ?? classifyMessageError(candidate);
239
+ return undefined;
240
+ });
241
+ return terminalCode ?? timeoutCode ?? fallbackCode ?? ErrorCode.E_UNKNOWN;
238
242
  }
239
243
  export function createDetailedError(error, path, additionalDetails) {
240
244
  const message = error instanceof Error ? error.message : String(error);
@@ -106,40 +106,57 @@ function buildHiddenPatterns(normalizedPattern, maxDepth) {
106
106
  function shouldUseGlobDirents(options) {
107
107
  return !options.stats && !options.followSymbolicLinks;
108
108
  }
109
- function assertOptionsShape(options) {
110
- const unknownOptions = options;
111
- if (unknownOptions === null || typeof unknownOptions !== 'object') {
112
- throw new TypeError('globEntries: options must be an object');
113
- }
114
- const o = unknownOptions;
115
- if (typeof o.cwd !== 'string') {
116
- throw new TypeError('globEntries: options.cwd must be a string');
109
+ function assertOptionString(options, key) {
110
+ if (typeof options[key] !== 'string') {
111
+ throw new TypeError(`globEntries: options.${key} must be a string`);
117
112
  }
118
- if (typeof o.pattern !== 'string') {
119
- throw new TypeError('globEntries: options.pattern must be a string');
120
- }
121
- if (!Array.isArray(o.excludePatterns)) {
113
+ }
114
+ function assertExcludePatternsOption(options) {
115
+ if (!Array.isArray(options.excludePatterns)) {
122
116
  throw new TypeError('globEntries: options.excludePatterns must be an array');
123
117
  }
124
- for (const p of o.excludePatterns) {
125
- if (typeof p !== 'string') {
118
+ for (const pattern of options.excludePatterns) {
119
+ if (typeof pattern !== 'string') {
126
120
  throw new TypeError('globEntries: options.excludePatterns must contain only strings');
127
121
  }
128
122
  }
123
+ }
124
+ function assertBooleanOptions(options) {
129
125
  for (const key of GLOB_BOOLEAN_OPTION_KEYS) {
130
- if (typeof o[key] !== 'boolean') {
126
+ if (typeof options[key] !== 'boolean') {
131
127
  throw new TypeError(`globEntries: options.${key} must be a boolean`);
132
128
  }
133
129
  }
134
- if (o.maxDepth !== undefined) {
135
- if (typeof o.maxDepth !== 'number' || !Number.isFinite(o.maxDepth)) {
136
- throw new TypeError('globEntries: options.maxDepth must be a finite number');
137
- }
130
+ }
131
+ function assertOptionalMaxDepth(options) {
132
+ const { maxDepth } = options;
133
+ if (maxDepth === undefined)
134
+ return;
135
+ if (typeof maxDepth !== 'number' || !Number.isFinite(maxDepth)) {
136
+ throw new TypeError('globEntries: options.maxDepth must be a finite number');
138
137
  }
139
- if (o.suppressErrors !== undefined && typeof o.suppressErrors !== 'boolean') {
138
+ }
139
+ function assertOptionalSuppressErrors(options) {
140
+ const { suppressErrors } = options;
141
+ if (suppressErrors === undefined)
142
+ return;
143
+ if (typeof suppressErrors !== 'boolean') {
140
144
  throw new TypeError('globEntries: options.suppressErrors must be a boolean');
141
145
  }
142
146
  }
147
+ function assertOptionsShape(options) {
148
+ const unknownOptions = options;
149
+ if (unknownOptions === null || typeof unknownOptions !== 'object') {
150
+ throw new TypeError('globEntries: options must be an object');
151
+ }
152
+ const o = unknownOptions;
153
+ assertOptionString(o, 'cwd');
154
+ assertOptionString(o, 'pattern');
155
+ assertExcludePatternsOption(o);
156
+ assertBooleanOptions(o);
157
+ assertOptionalMaxDepth(o);
158
+ assertOptionalSuppressErrors(o);
159
+ }
143
160
  function normalizeOptions(options) {
144
161
  const cwd = path.resolve(options.cwd);
145
162
  const normalizedPattern = normalizePattern(options.pattern, options.baseNameMatch);
@@ -120,23 +120,22 @@ function createParallelAbortError() {
120
120
  return createAbortError();
121
121
  }
122
122
  export async function processInParallel(items, processor, concurrency = PARALLEL_CONCURRENCY, signal) {
123
- if (items.length === 0)
123
+ const itemCount = items.length;
124
+ if (itemCount === 0)
124
125
  return { results: [], errors: [] };
125
126
  const effectiveConcurrency = normalizeConcurrency(concurrency);
126
127
  // Pre-allocate slots by index to guarantee input-order output.
127
- const resultSlots = new Array(items.length);
128
+ const resultSlots = new Array(itemCount);
128
129
  const errors = [];
129
130
  if (signal?.aborted)
130
131
  throw createParallelAbortError();
131
132
  let nextIndex = 0;
132
133
  const next = async () => {
133
- while (nextIndex < items.length) {
134
+ while (nextIndex < itemCount) {
134
135
  if (signal?.aborted)
135
136
  throw createParallelAbortError();
136
- const index = nextIndex++;
137
- // Check again because another worker might have incremented past length
138
- if (index >= items.length)
139
- break;
137
+ const index = nextIndex;
138
+ nextIndex += 1;
140
139
  const item = items[index];
141
140
  try {
142
141
  const result = await processor(item);
@@ -154,7 +153,7 @@ export async function processInParallel(items, processor, concurrency = PARALLEL
154
153
  }
155
154
  }
156
155
  };
157
- const workerCount = Math.min(items.length, effectiveConcurrency);
156
+ const workerCount = Math.min(itemCount, effectiveConcurrency);
158
157
  const workers = new Array(workerCount);
159
158
  for (let index = 0; index < workerCount; index += 1) {
160
159
  workers[index] = next();
@@ -36,45 +36,61 @@ function compilePatterns(patterns) {
36
36
  const normalized = normalizeForMatch(pattern);
37
37
  const matchesPath = normalized.includes('/');
38
38
  compiled.push({
39
- raw: normalized,
40
39
  globs: matchesPath ? compilePatternGlobs(normalized) : [normalized],
41
40
  matchesPath,
42
41
  });
43
42
  }
44
43
  return compiled;
45
44
  }
46
- const DENY_PATTERNS = compilePatterns(SENSITIVE_FILE_DENYLIST);
47
- const ALLOW_PATTERNS = compilePatterns(SENSITIVE_FILE_ALLOWLIST);
45
+ function toPatternSet(patterns) {
46
+ const pathGlobs = new Set();
47
+ const nameGlobs = new Set();
48
+ for (const pattern of patterns) {
49
+ const target = pattern.matchesPath ? pathGlobs : nameGlobs;
50
+ for (const glob of pattern.globs) {
51
+ target.add(glob);
52
+ }
53
+ }
54
+ return {
55
+ pathGlobs: [...pathGlobs],
56
+ nameGlobs: [...nameGlobs],
57
+ };
58
+ }
59
+ const DENY_PATTERNS = toPatternSet(compilePatterns(SENSITIVE_FILE_DENYLIST));
60
+ const ALLOW_PATTERNS = toPatternSet(compilePatterns(SENSITIVE_FILE_ALLOWLIST));
48
61
  function uniquePair(primary, secondary) {
49
62
  if (!secondary || secondary === primary)
50
63
  return [primary];
51
64
  return [primary, secondary];
52
65
  }
53
- function matchesAny(patterns, pathCandidates, nameCandidates) {
54
- for (const pattern of patterns) {
55
- const candidates = pattern.matchesPath ? pathCandidates : nameCandidates;
56
- for (const candidate of candidates) {
57
- for (const glob of pattern.globs) {
58
- if (path.posix.matchesGlob(candidate, glob))
59
- return true;
60
- }
66
+ function matchesAnyGlobs(globs, candidates) {
67
+ if (globs.length === 0 || candidates.length === 0)
68
+ return false;
69
+ for (const candidate of candidates) {
70
+ for (const glob of globs) {
71
+ if (path.posix.matchesGlob(candidate, glob))
72
+ return true;
61
73
  }
62
74
  }
63
75
  return false;
64
76
  }
65
77
  export function isSensitivePath(requestedPath, resolvedPath) {
66
- if (DENY_PATTERNS.length === 0)
78
+ if (DENY_PATTERNS.pathGlobs.length === 0 &&
79
+ DENY_PATTERNS.nameGlobs.length === 0) {
67
80
  return false;
81
+ }
68
82
  const normalizedRequested = normalizeForMatch(requestedPath);
69
83
  const normalizedResolved = resolvedPath
70
84
  ? normalizeForMatch(resolvedPath)
71
85
  : undefined;
72
86
  const pathCandidates = uniquePair(normalizedRequested, normalizedResolved);
73
87
  const nameCandidates = uniquePair(path.posix.basename(normalizedRequested), normalizedResolved ? path.posix.basename(normalizedResolved) : undefined);
74
- if (matchesAny(ALLOW_PATTERNS, pathCandidates, nameCandidates)) {
88
+ if (matchesAnyGlobs(ALLOW_PATTERNS.pathGlobs, pathCandidates) ||
89
+ matchesAnyGlobs(ALLOW_PATTERNS.nameGlobs, nameCandidates)) {
75
90
  return false;
76
91
  }
77
- return matchesAny(DENY_PATTERNS, pathCandidates, nameCandidates);
92
+ return (matchesAnyGlobs(DENY_PATTERNS.pathGlobs, pathCandidates) ||
93
+ matchesAnyGlobs(DENY_PATTERNS.nameGlobs, nameCandidates));
78
94
  }
79
95
  export function assertAllowedFileAccess(requestedPath, resolvedPath) {
80
96
  if (!isSensitivePath(requestedPath, resolvedPath))
package/dist/prompts.js CHANGED
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
  import { withDefaultIcons } from './tools/shared.js';
3
3
  const HELP_PROMPT_NAME = 'get-help';
4
4
  const HELP_PROMPT_TITLE = 'Get Help';
5
- const HELP_PROMPT_DESCRIPTION = 'Return the filesystem-mcp usage instructions.';
5
+ const HELP_PROMPT_DESCRIPTION = 'Retrieve the full filesystem-mcp XML usage guide.';
6
6
  function filterInstructionsByTopic(instructions, topic) {
7
7
  const normalized = topic.trim().toLowerCase();
8
8
  if (!normalized)
@@ -1,46 +1,48 @@
1
1
  import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
2
2
  import { buildCoreContextPack, getSharedConstraints, getToolContracts, } from './tool-info.js';
3
3
  import { buildWorkflowGuide } from './workflows.js';
4
- const INSTRUCTIONS_HEADER = `# FILESYSTEM-MCP
5
-
6
- Operate ONLY within allowed roots. Always discover before acting — never guess paths.
7
-
8
- ## TOOLS
4
+ const INSTRUCTIONS_HEADER = `<role>
5
+ Expert filesystem agent. Operate ONLY within allowed roots. Always discover before acting — never guess paths.
6
+ </role>
9
7
 
8
+ <tools_overview>
10
9
  | Category | Tools |
11
10
  |----------|-------|
12
11
  | Navigate | \`roots\`, \`ls\`, \`tree\`, \`find\` |
13
12
  | Inspect | \`stat\`, \`stat_many\`, \`grep\`, \`calculate_hash\` |
14
13
  | Read | \`read\`, \`read_many\`, \`diff_files\` |
15
14
  | Write | \`mkdir\`, \`write\`, \`edit\`, \`mv\`, \`rm\`, \`apply_patch\`, \`search_and_replace\` |
15
+ </tools_overview>
16
16
 
17
- ## RESOURCES
18
-
19
- - \`filesystem-mcp://result/{id}\`: Large output is cached here. **If a response includes \`resourceUri\`, call \`resources/read\` immediately — results expire on process restart.**
20
- - \`filesystem-mcp://metrics\`: Live per-tool call/error stats.
21
-
22
- ## TASK PROTOCOL
17
+ <resources>
18
+ - \`internal://instructions\`: Full server usage guide.
19
+ - \`internal://tool-catalog\`: Tool routing and cross-tool data-flow guide.
20
+ - \`internal://workflows\`: Standard operating sequences (explore/search/edit/patch).
21
+ - \`internal://tool-info/{name}\`: Per-tool details (nuances/gotchas), e.g. \`internal://tool-info/read\`.
22
+ - \`filesystem-mcp://result/{id}\`: Large output cache. Call \`resources/read\` immediately if \`resourceUri\` is returned.
23
+ - \`filesystem-mcp://metrics\`: Live per-tool stats.
24
+ </resources>
23
25
 
24
- Long-running tools support async execution: provide \`_meta.progressToken\` in \`tools/call\`, poll \`tasks/get\`, then call \`tasks/result\`.
26
+ <task_protocol>
27
+ Async execution: provide \`_meta.progressToken\` in \`tools/call\`, poll \`tasks/get\`, call \`tasks/result\`.
25
28
  Task-capable: \`find\`, \`tree\`, \`read\`, \`read_many\`, \`stat_many\`, \`grep\`, \`mkdir\`, \`write\`, \`mv\`, \`rm\`, \`calculate_hash\`, \`apply_patch\`, \`search_and_replace\`.
26
-
29
+ </task_protocol>
27
30
  `;
28
- const INSTRUCTIONS_FOOTER = `
29
- ## CONSTRAINTS
30
-
31
+ const INSTRUCTIONS_FOOTER = `<constraints>
31
32
  ${getSharedConstraints()
32
33
  .map((c) => `- ${c}`)
33
34
  .join('\n')}
35
+ </constraints>
34
36
 
35
- ## ERROR HANDLING
36
-
37
+ <error_handling>
37
38
  - \`E_ACCESS_DENIED\` → Call \`roots\`; use allowed path.
38
39
  - \`E_NOT_FOUND\` → Call \`ls\`/\`find\`; verify spelling.
39
40
  - \`E_TOO_LARGE\` → Use range/head or \`read_many\`.
40
41
  - \`E_TIMEOUT\` → Reduce scope or result limits.
42
+ </error_handling>
41
43
  `;
42
44
  function formatToolSection(tool) {
43
- const parts = [`${tool.name}: ${tool.description}`];
45
+ const parts = [`### ${tool.name}\n${tool.description}`];
44
46
  if (tool.nuances && tool.nuances.length > 0) {
45
47
  parts.push(...tool.nuances.map((n) => `» ${n}`));
46
48
  }
@@ -57,13 +59,12 @@ export function buildServerInstructions() {
57
59
  '',
58
60
  buildToolCatalogDetailsOnly(),
59
61
  '',
60
- '## TOOL REFERENCE',
61
- '',
62
+ '<tool_reference>',
62
63
  toolSections,
64
+ '</tool_reference>',
63
65
  '',
64
66
  buildWorkflowGuide(),
65
67
  '',
66
- '---',
67
68
  INSTRUCTIONS_FOOTER,
68
69
  ].join('\n');
69
70
  }
@@ -1,6 +1,5 @@
1
1
  import { buildCoreContextPack } from './tool-info.js';
2
- const CATALOG_GUIDE = `## Tool Selection Guide
3
-
2
+ const CATALOG_GUIDE = `<tool_selection_guide>
4
3
  ## Cross-Tool Data Flow
5
4
 
6
5
  \`\`\`
@@ -25,6 +24,7 @@ diff_files (patch text) -> apply_patch.patch
25
24
  - Always generate a patch with \`diff_files\` first.
26
25
  - Always use \`dryRun: true\` with \`apply_patch\` to verify changes.
27
26
  - \`apply_patch\` works on unified diff format.
27
+ </tool_selection_guide>
28
28
  `;
29
29
  export function buildToolCatalog() {
30
30
  return `${buildCoreContextPack()}\n\n${CATALOG_GUIDE}`;
@@ -33,7 +33,7 @@ export function buildCoreContextPack() {
33
33
  const annotations = e.annotations ? ` ${e.annotations.join(' ')}` : '';
34
34
  return `| \`${e.name}\` | ${e.description}${annotations} |`;
35
35
  });
36
- return `## Core Context Pack\n\n| Tool | Purpose |\n|------|---------|\n${rows.join('\n')}`;
36
+ return `<core_context>\n| Tool | Purpose |\n|------|---------|\n${rows.join('\n')}\n</core_context>`;
37
37
  }
38
38
  export function getSharedConstraints() {
39
39
  return [
@@ -1,6 +1,5 @@
1
1
  export function buildWorkflowGuide() {
2
- return `## Workflow Reference
3
-
2
+ return `<workflows>
4
3
  ### A: EXPLORE
5
4
  Use when: navigating an unfamiliar directory or reading file content.
6
5
  1. \`roots\` (List allowed paths).
@@ -30,5 +29,5 @@ Use when: applying structured diffs produced by \`diff_files\`.
30
29
  2. \`apply_patch\` (dryRun: true).
31
30
  3. \`apply_patch\` (dryRun: false).
32
31
  > **Tip:** Pass \`diff_files\` output directly into \`apply_patch\`.
33
- `;
32
+ </workflows>`;
34
33
  }
package/dist/resources.js CHANGED
@@ -21,7 +21,7 @@ const TOOL_INFO_RESOURCE_NAME = 'filesystem-mcp-tool-info';
21
21
  const TOOL_INFO_RESOURCE_DESCRIPTION = 'Per-tool contract details, nuances, and gotchas. Read internal://tool-info/{name} with a tool name such as "read", "ls", or "grep".';
22
22
  const INSTRUCTIONS_RESOURCE_NAME = 'filesystem-mcp-instructions';
23
23
  const INSTRUCTIONS_RESOURCE_URI = 'internal://instructions';
24
- const INSTRUCTIONS_RESOURCE_DESCRIPTION = 'Guidance for using the filesystem-mcp MCP tools effectively.';
24
+ const INSTRUCTIONS_RESOURCE_DESCRIPTION = 'Comprehensive rules and guidelines for filesystem-mcp usage.';
25
25
  const RESULT_RESOURCE_NAME = 'filesystem-mcp-result';
26
26
  const RESULT_RESOURCE_DESCRIPTION = 'Ephemeral cached tool output exposed as an MCP resource. Not guaranteed to be listed via resources/list.';
27
27
  const METRICS_RESOURCE_NAME = 'filesystem-mcp-metrics';
@@ -29,10 +29,10 @@ const METRICS_RESOURCE_URI = 'filesystem-mcp://metrics';
29
29
  const METRICS_RESOURCE_DESCRIPTION = 'Live per-tool call/error/avgDurationMs metrics snapshot.';
30
30
  const CATALOG_RESOURCE_NAME = 'filesystem-mcp-catalog';
31
31
  const CATALOG_RESOURCE_URI = 'internal://tool-catalog';
32
- const CATALOG_RESOURCE_DESCRIPTION = 'Detailed catalog of tools and their inter-dependencies.';
32
+ const CATALOG_RESOURCE_DESCRIPTION = 'Tool selection guide and data flow map.';
33
33
  const WORKFLOW_RESOURCE_NAME = 'filesystem-mcp-workflows';
34
34
  const WORKFLOW_RESOURCE_URI = 'internal://workflows';
35
- const WORKFLOW_RESOURCE_DESCRIPTION = 'Recommended workflows for common tasks.';
35
+ const WORKFLOW_RESOURCE_DESCRIPTION = 'Standard operating procedures for exploration, search, edit, and patch.';
36
36
  export function registerInstructionResource(server, instructions, iconInfo) {
37
37
  server.registerResource(INSTRUCTIONS_RESOURCE_NAME, INSTRUCTIONS_RESOURCE_URI, withDefaultIcons({
38
38
  title: 'Server Instructions',
@@ -59,9 +59,7 @@ export async function createServer(options = {}) {
59
59
  }),
60
60
  };
61
61
  if (taskToolSupport) {
62
- // Note: InMemoryTaskStore has no TTL tasks accumulate for the process
63
- // lifetime. In HTTP mode this may grow unboundedly for long-lived servers.
64
- // Use a custom TaskStore with eviction for production HTTP deployments.
62
+ // Enabling task tool support requires configuring a task store and message queue on the server config. We use in-memory implementations which are suitable for short-lived stdio sessions. Long-running HTTP servers should replace these with TTL-evicting implementations to avoid unbounded memory growth.
65
63
  serverConfig.taskStore = new InMemoryTaskStore();
66
64
  serverConfig.taskMessageQueue = new InMemoryTaskMessageQueue();
67
65
  }
@@ -171,9 +169,6 @@ async function createHttpSession(options, sessions) {
171
169
  sessions.set(sessionId, { server: mcpServer, transport });
172
170
  rootsManager.logMissingDirectoriesIfNeeded(mcpServer);
173
171
  },
174
- onsessionclosed: (sessionId) => {
175
- sessions.delete(sessionId);
176
- },
177
172
  });
178
173
  transport.onclose = () => {
179
174
  const { sessionId } = transport;
@@ -5,13 +5,16 @@ function detectTaskToolSupport() {
5
5
  return cachedTaskToolSupport;
6
6
  }
7
7
  try {
8
+ // Instantiate a minimal, unconnected probe server to duck-type check for
9
+ // task tool support. The probe has no transport or active connections, so
10
+ // close() only releases in-memory state; fire-and-forget is safe here.
8
11
  const probe = new McpServer({
9
12
  name: 'filesystem-mcp-capability-probe',
10
13
  version: '0.0.0',
11
14
  }, { capabilities: { tools: {} } });
12
15
  cachedTaskToolSupport =
13
16
  typeof probe.experimental.tasks.registerToolTask === 'function';
14
- void probe.close().catch(() => { });
17
+ probe.close().catch(() => { });
15
18
  }
16
19
  catch {
17
20
  cachedTaskToolSupport = false;
@@ -5,6 +5,8 @@ export declare class RootsManager {
5
5
  private rootsUpdateTimeout;
6
6
  private rootDirectories;
7
7
  private clientInitialized;
8
+ private updatingRoots;
9
+ private pendingRootsUpdate;
8
10
  private readonly options;
9
11
  readonly loggingState: LoggingState;
10
12
  constructor(options: ServerOptions, loggingState: LoggingState);
@@ -84,6 +84,10 @@ export class RootsManager {
84
84
  rootsUpdateTimeout;
85
85
  rootDirectories = [];
86
86
  clientInitialized = false;
87
+ // Set to true when an update is in progress, to prevent concurrent executions. If a change arrives while true, we queue a single retry after completion to ensure the last-known state is applied. This
88
+ updatingRoots = false;
89
+ // If an update is in progress and a change arrives, we set this flag to ensure we run another update after completion to apply the latest state
90
+ pendingRootsUpdate = false;
87
91
  options;
88
92
  loggingState;
89
93
  constructor(options, loggingState) {
@@ -151,23 +155,37 @@ export class RootsManager {
151
155
  logToMcp(server, 'warning', 'No allowed directories specified. Please provide directories as command-line arguments or enable --allow-cwd to use the current working directory.', this.loggingState.minimumLevel);
152
156
  }
153
157
  async updateRootsFromClient(server) {
158
+ // Guard against concurrent executions: if one is already running, queue a
159
+ // single retry so the last-known state is always applied after completion.
160
+ if (this.updatingRoots) {
161
+ this.pendingRootsUpdate = true;
162
+ return;
163
+ }
164
+ this.updatingRoots = true;
154
165
  try {
155
166
  const clientCapabilities = server.server.getClientCapabilities();
156
167
  if (!clientCapabilities?.roots) {
157
168
  this.rootDirectories = [];
158
- return;
159
169
  }
160
- const rootsResult = await server.server.listRoots(undefined, {
161
- timeout: ROOTS_TIMEOUT_MS,
162
- });
163
- const roots = extractRoots(rootsResult);
164
- this.rootDirectories = await resolveRootDirectories(roots);
170
+ else {
171
+ const rootsResult = await server.server.listRoots(undefined, {
172
+ timeout: ROOTS_TIMEOUT_MS,
173
+ });
174
+ const roots = extractRoots(rootsResult);
175
+ this.rootDirectories = await resolveRootDirectories(roots);
176
+ }
165
177
  }
166
178
  catch (error) {
167
179
  logToMcp(server, 'debug', `[DEBUG] MCP Roots protocol unavailable or failed: ${formatUnknownErrorMessage(error)}`, this.loggingState.minimumLevel);
168
180
  }
169
181
  finally {
170
182
  await this.recomputeAllowedDirectories();
183
+ this.updatingRoots = false;
184
+ // If a change arrived while we were running, apply it now.
185
+ if (this.pendingRootsUpdate) {
186
+ this.pendingRootsUpdate = false;
187
+ void this.updateRootsFromClient(server);
188
+ }
171
189
  }
172
190
  }
173
191
  }