@j0hanz/filesystem-mcp 1.6.1 → 1.6.2

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/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,44 @@
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
+ - \`filesystem-mcp://result/{id}\`: Large output cache. Call \`resources/read\` immediately if \`resourceUri\` is returned.
19
+ - \`filesystem-mcp://metrics\`: Live per-tool stats.
20
+ </resources>
23
21
 
24
- Long-running tools support async execution: provide \`_meta.progressToken\` in \`tools/call\`, poll \`tasks/get\`, then call \`tasks/result\`.
22
+ <task_protocol>
23
+ Async execution: provide \`_meta.progressToken\` in \`tools/call\`, poll \`tasks/get\`, call \`tasks/result\`.
25
24
  Task-capable: \`find\`, \`tree\`, \`read\`, \`read_many\`, \`stat_many\`, \`grep\`, \`mkdir\`, \`write\`, \`mv\`, \`rm\`, \`calculate_hash\`, \`apply_patch\`, \`search_and_replace\`.
26
-
25
+ </task_protocol>
27
26
  `;
28
- const INSTRUCTIONS_FOOTER = `
29
- ## CONSTRAINTS
30
-
27
+ const INSTRUCTIONS_FOOTER = `<constraints>
31
28
  ${getSharedConstraints()
32
29
  .map((c) => `- ${c}`)
33
30
  .join('\n')}
31
+ </constraints>
34
32
 
35
- ## ERROR HANDLING
36
-
33
+ <error_handling>
37
34
  - \`E_ACCESS_DENIED\` → Call \`roots\`; use allowed path.
38
35
  - \`E_NOT_FOUND\` → Call \`ls\`/\`find\`; verify spelling.
39
36
  - \`E_TOO_LARGE\` → Use range/head or \`read_many\`.
40
37
  - \`E_TIMEOUT\` → Reduce scope or result limits.
38
+ </error_handling>
41
39
  `;
42
40
  function formatToolSection(tool) {
43
- const parts = [`${tool.name}: ${tool.description}`];
41
+ const parts = [`### ${tool.name}\n${tool.description}`];
44
42
  if (tool.nuances && tool.nuances.length > 0) {
45
43
  parts.push(...tool.nuances.map((n) => `» ${n}`));
46
44
  }
@@ -57,13 +55,12 @@ export function buildServerInstructions() {
57
55
  '',
58
56
  buildToolCatalogDetailsOnly(),
59
57
  '',
60
- '## TOOL REFERENCE',
61
- '',
58
+ '<tool_reference>',
62
59
  toolSections,
60
+ '</tool_reference>',
63
61
  '',
64
62
  buildWorkflowGuide(),
65
63
  '',
66
- '---',
67
64
  INSTRUCTIONS_FOOTER,
68
65
  ].join('\n');
69
66
  }
@@ -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
  }
@@ -47,6 +47,7 @@ export type ToolResponse<T> = ReturnType<typeof buildToolResponse<T>> & {
47
47
  interface ToolErrorResponse extends Record<string, unknown> {
48
48
  content: ContentBlock[];
49
49
  isError: true;
50
+ errorCode?: string;
50
51
  }
51
52
  export type ToolResult<T> = ToolResponse<T> | ToolErrorResponse;
52
53
  export declare function withValidatedArgs<Args, Result>(schema: z.ZodType<Args>, handler: (args: Args, extra: ToolExtra) => Promise<ToolResult<Result>>): (args: unknown, extra: ToolExtra) => Promise<ToolResult<Result>>;
@@ -116,4 +117,13 @@ export declare function wrapToolHandler<Args, Result>(handler: (args: Args, extr
116
117
  progressMessage?: (args: Args) => string;
117
118
  completionMessage?: (args: Args, result: ToolResult<Result>) => string | undefined;
118
119
  }): (args: Args, extra?: ToolExtra) => Promise<ToolResult<Result>>;
120
+ /**
121
+ * Returns `pathValue` if non-empty; otherwise resolves to the single allowed
122
+ * directory from module-level state managed by `RootsManager`. Throws when the
123
+ * path is ambiguous (multiple roots) or when no roots are configured.
124
+ *
125
+ * NOTE: Depends on `getAllowedDirectories()` which reads module-level state
126
+ * updated by `RootsManager`. Ensure the server is initialized before calling.
127
+ * See `src/server/roots-manager.ts` for the update lifecycle.
128
+ */
119
129
  export declare function resolvePathOrRoot(pathValue: string | undefined): string;
@@ -198,6 +198,7 @@ export function buildToolErrorResponse(error, defaultCode, path) {
198
198
  return {
199
199
  content: [{ type: 'text', text }],
200
200
  isError: true,
201
+ errorCode: detailed.code,
201
202
  };
202
203
  }
203
204
  function buildNotInitializedResult() {
@@ -259,10 +260,11 @@ export function createProgressReporter(extra) {
259
260
  // out-of-order progress is undefined in the MCP spec.
260
261
  if (current <= lastProgress)
261
262
  return;
262
- // Enforce rate-limiting to prevent client flooding. Progress updates faster
263
- // than PROGRESS_RATE_LIMIT_MS are silently dropped.
263
+ // Terminal notifications always bypass the rate limit so clients reliably
264
+ // receive the final state even when updates arrive in quick succession.
265
+ const isTerminal = total !== undefined && current >= total;
264
266
  const now = Date.now();
265
- if (now - lastSentMs < PROGRESS_RATE_LIMIT_MS)
267
+ if (!isTerminal && now - lastSentMs < PROGRESS_RATE_LIMIT_MS)
266
268
  return;
267
269
  lastProgress = current;
268
270
  lastSentMs = now;
@@ -283,11 +285,12 @@ async function withProgress(message, extra, run, getCompletionMessage) {
283
285
  return run();
284
286
  }
285
287
  const total = 1;
286
- await reportProgress(extra, {
287
- current: 0,
288
- total,
289
- message,
290
- });
288
+ // Emit the start notification only when a progressToken is present; for
289
+ // task-only mode the task status is already 'working' — a zero-progress
290
+ // notification would add unnecessary overhead without client value.
291
+ if (canSendProgress(extra)) {
292
+ await reportProgress(extra, { current: 0, total, message });
293
+ }
291
294
  try {
292
295
  const result = await run();
293
296
  const endMessage = getCompletionMessage?.(result) ?? message;
@@ -326,6 +329,15 @@ export function wrapToolHandler(handler, options) {
326
329
  return maybeStripStructuredContentFromResult(result);
327
330
  };
328
331
  }
332
+ /**
333
+ * Returns `pathValue` if non-empty; otherwise resolves to the single allowed
334
+ * directory from module-level state managed by `RootsManager`. Throws when the
335
+ * path is ambiguous (multiple roots) or when no roots are configured.
336
+ *
337
+ * NOTE: Depends on `getAllowedDirectories()` which reads module-level state
338
+ * updated by `RootsManager`. Ensure the server is initialized before calling.
339
+ * See `src/server/roots-manager.ts` for the update lifecycle.
340
+ */
329
341
  export function resolvePathOrRoot(pathValue) {
330
342
  if (pathValue && pathValue.trim().length > 0)
331
343
  return pathValue;
@@ -134,6 +134,10 @@ function normalizeCallToolResult(value) {
134
134
  function getToolResultErrorCode(result) {
135
135
  if (!isRecord(result) || result['isError'] !== true)
136
136
  return undefined;
137
+ // First check for a dedicated errorCode property to avoid regex parsing of the content for structured error results produced by newer code.
138
+ if (typeof result['errorCode'] === 'string')
139
+ return result['errorCode'];
140
+ // Fallback to regex parsing of the human-readable error message for older error results that lack a structured errorCode property.
137
141
  const { content } = result;
138
142
  if (!Array.isArray(content) || content.length === 0)
139
143
  return undefined;
@@ -293,6 +297,21 @@ async function runTaskInBackground(run, args, extra, taskStore, taskId, toolName
293
297
  }
294
298
  catch (innerError) {
295
299
  console.error(`Failed to store task failure result for task ${taskId}:`, innerError);
300
+ // If storing the failure result also fails, there's not much we can do. The task will remain in 'working' status until it expires, which is not ideal but at least prevents clients from receiving incorrect results or hanging indefinitely waiting for a result that will never arrive. We log the error to aid debugging, and we attempt to notify the client of the failure if possible, but we don't want to throw further errors that could crash the server or cause cascading failures.
301
+ const syntheticTask = {
302
+ taskId,
303
+ status: 'failed',
304
+ ttl: null,
305
+ createdAt: new Date().toISOString(),
306
+ lastUpdatedAt: new Date().toISOString(),
307
+ };
308
+ const { sendNotification } = extra;
309
+ if (typeof sendNotification === 'function') {
310
+ void sendNotification({
311
+ method: TASK_STATUS_NOTIFICATION_METHOD,
312
+ params: buildTaskStatusNotificationParams(syntheticTask),
313
+ });
314
+ }
296
315
  }
297
316
  }
298
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "mcpName": "io.github.j0hanz/filesystem-mcp",
5
5
  "description": "MCP Server that enables LLMs to interact with the local filesystem.",
6
6
  "type": "module",