@j0hanz/filesystem-mcp 1.5.0 → 1.5.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/cli.js CHANGED
@@ -195,7 +195,7 @@ export async function parseArgs() {
195
195
  throw error;
196
196
  }
197
197
  const options = cli.opts();
198
- const allowCwd = options.allowCwd === true;
198
+ const allowCwd = Boolean(options.allowCwd);
199
199
  const port = parsePortOption(options.port);
200
200
  const positionals = getParsedAllowedDirs(cli);
201
201
  let allowedDirs;
@@ -1,3 +1,4 @@
1
+ export declare function parseTrueEnvFlag(value: string | undefined): boolean;
1
2
  export declare const DEFAULT_LOG_LEVEL: "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency";
2
3
  export declare const PARALLEL_CONCURRENCY: number;
3
4
  export declare const MAX_SEARCHABLE_FILE_SIZE: number;
@@ -11,8 +12,17 @@ export declare const DEFAULT_SEARCH_TIMEOUT_MS: number;
11
12
  */
12
13
  export declare const SEARCH_WORKERS: number;
13
14
  export declare const DEFAULT_MAX_DEPTH = 10;
14
- export declare const DEFAULT_LIST_MAX_ENTRIES = 10000;
15
+ export declare const DEFAULT_LIST_MAX_ENTRIES = 20000;
15
16
  export declare const DEFAULT_SEARCH_MAX_FILES = 20000;
17
+ export declare const MAX_TREE_DEPTH = 50;
18
+ export declare const DEFAULT_TREE_DEPTH = 5;
19
+ export declare const MAX_TREE_ENTRIES = 20000;
20
+ export declare const DEFAULT_TREE_ENTRIES = 1000;
21
+ export declare const MAX_LIST_ENTRIES = 20000;
22
+ export declare const MAX_SEARCH_RESULTS = 10000;
23
+ export declare const DEFAULT_SEARCH_RESULTS = 100;
24
+ export declare const MAX_SEARCH_DEPTH = 100;
25
+ export declare const DEFAULT_SEARCH_CONTENT_RESULTS = 500;
16
26
  export declare const MAX_LINE_CONTENT_LENGTH = 200;
17
27
  export declare const BINARY_CHECK_BUFFER_SIZE = 512;
18
28
  export declare const SENSITIVE_FILE_DENYLIST: string[];
@@ -1,6 +1,11 @@
1
1
  import { availableParallelism } from 'node:os';
2
2
  const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes', 'y', 'on']);
3
3
  const FALSE_ENV_VALUES = new Set(['0', 'false', 'no', 'n', 'off']);
4
+ export function parseTrueEnvFlag(value) {
5
+ if (value === undefined)
6
+ return false;
7
+ return TRUE_ENV_VALUES.has(value.trim().toLowerCase());
8
+ }
4
9
  const KIB = 1024;
5
10
  const MIB = 1024 * KIB;
6
11
  function logInvalidEnvValue(envVar, value, expected, defaultValue) {
@@ -109,8 +114,18 @@ const ENV_ALLOWLIST = parseEnvList('FS_CONTEXT_ALLOWLIST');
109
114
  export const SEARCH_WORKERS = parseEnvInt('FS_CONTEXT_SEARCH_WORKERS', getDefaultSearchWorkers(), 1, 16);
110
115
  // Hardcoded defaults
111
116
  export const DEFAULT_MAX_DEPTH = 10;
112
- export const DEFAULT_LIST_MAX_ENTRIES = 10000;
117
+ export const DEFAULT_LIST_MAX_ENTRIES = 20000;
113
118
  export const DEFAULT_SEARCH_MAX_FILES = 20000;
119
+ // Schema limits and defaults
120
+ export const MAX_TREE_DEPTH = 50;
121
+ export const DEFAULT_TREE_DEPTH = 5;
122
+ export const MAX_TREE_ENTRIES = 20000;
123
+ export const DEFAULT_TREE_ENTRIES = 1000;
124
+ export const MAX_LIST_ENTRIES = 20000;
125
+ export const MAX_SEARCH_RESULTS = 10000;
126
+ export const DEFAULT_SEARCH_RESULTS = 100;
127
+ export const MAX_SEARCH_DEPTH = 100;
128
+ export const DEFAULT_SEARCH_CONTENT_RESULTS = 500;
114
129
  // Non-configurable constants
115
130
  export const MAX_LINE_CONTENT_LENGTH = 200;
116
131
  export const BINARY_CHECK_BUFFER_SIZE = 512;
@@ -419,7 +419,7 @@ async function readRangeContent(handle, startLine, endLine, options) {
419
419
  if (hasEndLine && lineNumber === stopAt) {
420
420
  const peek = await iterator.next();
421
421
  hasMoreLines = !peek.done;
422
- reachedEof = peek.done === true;
422
+ reachedEof = Boolean(peek.done);
423
423
  stoppedEarly = true;
424
424
  break;
425
425
  }
@@ -2,21 +2,18 @@ import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import { hash } from 'node:crypto';
3
3
  import { channel, tracingChannel } from 'node:diagnostics_channel';
4
4
  import { monitorEventLoopDelay, performance, PerformanceObserver, } from 'node:perf_hooks';
5
+ import { parseTrueEnvFlag } from './constants.js';
5
6
  import { isRecord } from './type-guards.js';
6
7
  // --- Configuration ---
7
8
  const ENV = process.env;
8
9
  let _cachedConfig;
9
10
  function readConfig() {
10
11
  return (_cachedConfig ??= {
11
- enabled: isTrue(ENV['FS_CONTEXT_DIAGNOSTICS']),
12
+ enabled: parseTrueEnvFlag(ENV['FS_CONTEXT_DIAGNOSTICS']),
12
13
  detail: parseDetail(ENV['FS_CONTEXT_DIAGNOSTICS_DETAIL']),
13
- logToolErrors: isTrue(ENV['FS_CONTEXT_TOOL_LOG_ERRORS']),
14
+ logToolErrors: parseTrueEnvFlag(ENV['FS_CONTEXT_TOOL_LOG_ERRORS']),
14
15
  });
15
16
  }
16
- function isTrue(val) {
17
- const norm = val?.trim().toLowerCase();
18
- return norm === '1' || norm === 'true' || norm === 'yes';
19
- }
20
17
  function parseDetail(val) {
21
18
  if (val === '2')
22
19
  return 2;
package/dist/prompts.js CHANGED
@@ -9,7 +9,14 @@ function filterInstructionsByTopic(instructions, topic) {
9
9
  return instructions;
10
10
  const sections = instructions.split(/\n(?=## )/u);
11
11
  const match = sections.find((sec) => sec.toLowerCase().startsWith(`## ${normalized}`));
12
- return match ?? instructions;
12
+ if (match !== undefined)
13
+ return match;
14
+ const available = sections
15
+ .filter((sec) => sec.startsWith('## '))
16
+ .map((sec) => sec.split('\n')[0]?.replace(/^##\s*/u, '') ?? '')
17
+ .filter(Boolean)
18
+ .join(', ');
19
+ return `Section '${topic}' not found. Available sections: ${available}\n\n${instructions}`;
13
20
  }
14
21
  export function registerGetHelpPrompt(server, instructions, iconInfo) {
15
22
  const baseConfig = withDefaultIcons({ title: HELP_PROMPT_TITLE, description: HELP_PROMPT_DESCRIPTION }, iconInfo);
@@ -1,32 +1,28 @@
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 INSTRUCTIONS
4
+ const INSTRUCTIONS_HEADER = `# FILESYSTEM-MCP
5
5
 
6
- > Resource: \`internal://instructions\` | Prompt: \`get-help\`
6
+ Operate ONLY within allowed roots. Always discover before acting — never guess paths.
7
7
 
8
- ## CORE CAPABILITY
8
+ ## TOOLS
9
9
 
10
- - **Domain:** Safe local filesystem operations (read/write/diff/patch) within allowed roots.
11
- - **Tools:**
12
- - READ: \`roots\`, \`ls\`, \`find\`, \`tree\`, \`read\`, \`read_many\`, \`stat\`, \`stat_many\`, \`grep\`, \`calculate_hash\`, \`diff_files\`.
13
- - WRITE: \`mkdir\`, \`write\`, \`edit\`, \`mv\`, \`rm\`, \`apply_patch\`, \`search_and_replace\`.
10
+ | Category | Tools |
11
+ |----------|-------|
12
+ | Navigate | \`roots\`, \`ls\`, \`tree\`, \`find\` |
13
+ | Inspect | \`stat\`, \`stat_many\`, \`grep\`, \`calculate_hash\` |
14
+ | Read | \`read\`, \`read_many\`, \`diff_files\` |
15
+ | Write | \`mkdir\`, \`write\`, \`edit\`, \`mv\`, \`rm\`, \`apply_patch\`, \`search_and_replace\` |
14
16
 
15
17
  ## RESOURCES
16
18
 
17
- - \`filesystem-mcp://result/{id}\`: Ephemeral cached output.
18
- - \`filesystem-mcp://metrics\`: Live tool stats.
19
- - **Tip:** If response has \`resourceUri\`, call \`resources/read\` to fetch full content.
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.
20
21
 
21
- ## PROGRESS & TASKS
22
+ ## TASK PROTOCOL
22
23
 
23
- - Support \`_meta.progressToken\` for updates.
24
- - Task tools: \`find\`, \`tree\`, \`read\`, \`read_many\`, \`stat_many\`, \`grep\`, \`mkdir\`, \`write\`, \`mv\`, \`rm\`, \`calculate_hash\`, \`apply_patch\`, \`search_and_replace\`.
25
- - Flow: \`tools/call\` (task) → \`tasks/get\` → \`tasks/result\`.
26
-
27
- ## GOLDEN PATH WORKFLOWS
28
-
29
- See "Workflow Reference" below for detailed execution sequences.
24
+ Long-running tools support async execution: provide \`_meta.progressToken\` in \`tools/call\`, poll \`tasks/get\`, then call \`tasks/result\`.
25
+ Task-capable: \`find\`, \`tree\`, \`read\`, \`read_many\`, \`stat_many\`, \`grep\`, \`mkdir\`, \`write\`, \`mv\`, \`rm\`, \`calculate_hash\`, \`apply_patch\`, \`search_and_replace\`.
30
26
 
31
27
  `;
32
28
  const INSTRUCTIONS_FOOTER = `
@@ -45,22 +41,11 @@ ${getSharedConstraints()
45
41
  `;
46
42
  function formatToolSection(tool) {
47
43
  const parts = [`${tool.name}: ${tool.description}`];
48
- if (tool.annotations) {
49
- const attrs = [];
50
- if (tool.annotations.destructiveHint)
51
- attrs.push('[Destructive]');
52
- if (tool.annotations.idempotentHint)
53
- attrs.push('[Idempotent]');
54
- if (tool.annotations.readOnlyHint)
55
- attrs.push('[Read-Only]');
56
- if (attrs.length > 0)
57
- parts.push(attrs.join(' '));
58
- }
59
44
  if (tool.nuances && tool.nuances.length > 0) {
60
- parts.push(...tool.nuances.map((n) => `! ${n}`));
45
+ parts.push(...tool.nuances.map((n) => ${n}`));
61
46
  }
62
47
  if (tool.gotchas && tool.gotchas.length > 0) {
63
- parts.push(...tool.gotchas.map((g) => `! ${g}`));
48
+ parts.push(...tool.gotchas.map((g) => `⚠ ${g}`));
64
49
  }
65
50
  return parts.join('\n');
66
51
  }
@@ -1,19 +1,25 @@
1
1
  import { buildCoreContextPack } from './tool-info.js';
2
- const CATALOG_GUIDE = `## Tool Catalog Details
2
+ const CATALOG_GUIDE = `## Tool Selection Guide
3
3
 
4
4
  ## Cross-Tool Data Flow
5
5
 
6
6
  \`\`\`
7
- find -> output_paths -> grep.paths
8
- diff_files -> output_patch -> apply_patch.patch
7
+ find (results[].path) -> grep.paths
8
+ diff_files (patch text) -> apply_patch.patch
9
9
  \`\`\`
10
10
 
11
- ## Search Strategy Strategy
11
+ ## Search Strategy
12
12
 
13
13
  - Use \`find\` for glob-based file discovery.
14
14
  - Use \`grep\` for content-based searches.
15
15
  - Use \`search_and_replace\` ONLY for bulk replacements, not for discovery.
16
16
 
17
+ ## Write Strategy
18
+
19
+ - Use \`edit\` for precise, single-occurrence string replacements in existing files.
20
+ - Use \`write\` to create new files or completely overwrite existing content.
21
+ - Use \`search_and_replace\` for bulk regex replacements across multiple files.
22
+
17
23
  ## Patch Management
18
24
 
19
25
  - Always generate a patch with \`diff_files\` first.
@@ -21,7 +27,6 @@ diff_files -> output_patch -> apply_patch.patch
21
27
  - \`apply_patch\` works on unified diff format.
22
28
  `;
23
29
  export function buildToolCatalog() {
24
- // Return combined view for standalone resource usage
25
30
  return `${buildCoreContextPack()}\n\n${CATALOG_GUIDE}`;
26
31
  }
27
32
  export function buildToolCatalogDetailsOnly() {
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_SEARCH_CONTENT_RESULTS, MAX_SEARCH_RESULTS, MAX_TEXT_FILE_SIZE, } from '../lib/constants.js';
1
2
  import { ALL_TOOLS } from '../tools.js';
2
3
  function toEntry(contract) {
3
4
  const annotations = [];
@@ -38,7 +39,7 @@ export function getSharedConstraints() {
38
39
  return [
39
40
  'Allowed roots only (negotiated via CLI).',
40
41
  'Sensitive files denylisted by default.',
41
- 'Max file size & search results enforced.',
42
- 'Externalized results are ephemeral (in-memory).',
42
+ `Max file size (${Math.floor(MAX_TEXT_FILE_SIZE / 1024 / 1024)}MB) & search results (${MAX_SEARCH_RESULTS} files, ${DEFAULT_SEARCH_CONTENT_RESULTS} lines) enforced.`,
43
+ 'If a response includes `resourceUri`, call `resources/read` immediately — results expire on process restart.',
43
44
  ];
44
45
  }
@@ -1,8 +1,8 @@
1
- import { getSharedConstraints } from './tool-info.js';
2
1
  export function buildWorkflowGuide() {
3
2
  return `## Workflow Reference
4
3
 
5
4
  ### A: EXPLORE
5
+ Use when: navigating an unfamiliar directory or reading file content.
6
6
  1. \`roots\` (List allowed paths).
7
7
  2. \`ls\` (files) | \`tree\` (structure).
8
8
  3. \`stat\` | \`stat_many\` (size/type check).
@@ -10,12 +10,14 @@ export function buildWorkflowGuide() {
10
10
  > **Strict:** Never guess paths. Resolve first.
11
11
 
12
12
  ### B: SEARCH
13
+ Use when: locating files by name pattern or by content match.
13
14
  1. \`find\` (glob candidates).
14
15
  2. \`grep\` (content search).
15
16
  3. \`read\` (verify context).
16
- > **Tip:** Content search requires \`grep\`, not \`find\`.
17
+ > **Strict:** Use \`grep\` for content search, not \`find\`.
17
18
 
18
19
  ### C: EDIT
20
+ Use when: modifying existing files or reorganizing the filesystem.
19
21
  1. \`edit\` (precise string match).
20
22
  2. \`search_and_replace\` (bulk regex/glob).
21
23
  3. \`mv\` | \`rm\` (file layout).
@@ -23,14 +25,10 @@ export function buildWorkflowGuide() {
23
25
  > **Strict:** Confirm destructive ops (\`write\`, \`mv\`, \`rm\`, bulk replace).
24
26
 
25
27
  ### D: PATCH
28
+ Use when: applying structured diffs produced by \`diff_files\`.
26
29
  1. \`diff_files\` (generate).
27
30
  2. \`apply_patch\` (dryRun: true).
28
31
  3. \`apply_patch\` (dryRun: false).
29
- > **Tip:** Use \`diff_files\` output directly.
30
-
31
- ## Shared Constraints
32
- ${getSharedConstraints()
33
- .map((c) => `- ${c}`)
34
- .join('\n')}
32
+ > **Tip:** Pass \`diff_files\` output directly into \`apply_patch\`.
35
33
  `;
36
34
  }
package/dist/resources.js CHANGED
@@ -47,7 +47,7 @@ export function registerToolCatalogResource(server, iconInfo) {
47
47
  mimeType: 'text/markdown',
48
48
  annotations: {
49
49
  audience: ['assistant'],
50
- priority: 0.6,
50
+ priority: 0.7,
51
51
  },
52
52
  }, iconInfo), (uri) => ({
53
53
  contents: [
@@ -66,7 +66,7 @@ export function registerWorkflowGuideResource(server, iconInfo) {
66
66
  mimeType: 'text/markdown',
67
67
  annotations: {
68
68
  audience: ['assistant'],
69
- priority: 0.7,
69
+ priority: 0.6,
70
70
  },
71
71
  }, iconInfo), (uri) => ({
72
72
  contents: [
package/dist/schemas.d.ts CHANGED
@@ -38,7 +38,7 @@ export declare const ListDirectoryInputSchema: z.ZodObject<{
38
38
  includeHidden: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
39
39
  includeIgnored: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
40
40
  maxDepth: z.ZodOptional<z.ZodNumber>;
41
- maxEntries: z.ZodOptional<z.ZodNumber>;
41
+ maxEntries: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
42
42
  sortBy: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
43
43
  name: "name";
44
44
  size: "size";
package/dist/schemas.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { DEFAULT_LIST_MAX_ENTRIES, DEFAULT_SEARCH_CONTENT_RESULTS, DEFAULT_SEARCH_RESULTS, DEFAULT_TREE_DEPTH, DEFAULT_TREE_ENTRIES, MAX_LIST_ENTRIES, MAX_SEARCH_DEPTH, MAX_SEARCH_RESULTS, MAX_TREE_DEPTH, MAX_TREE_ENTRIES, } from './lib/constants.js';
2
3
  import { ErrorCode } from './lib/errors.js';
3
4
  function isSafeGlobPattern(value) {
4
5
  if (value.length === 0)
@@ -25,8 +26,12 @@ const RequiredPathSchema = PathSchemaBase.min(1, 'Path required');
25
26
  const FileTypeSchema = z.enum(['file', 'directory', 'symlink', 'other']);
26
27
  const ListDirectorySortSchema = z.enum(['name', 'size', 'modified', 'type']);
27
28
  const SearchFilesSortSchema = z.enum(['name', 'size', 'modified', 'path']);
28
- const SearchStopReasonSchema = z.enum(['maxResults', 'maxFiles', 'timeout']);
29
- const ListDirectoryStopReasonSchema = z.enum(['maxEntries', 'aborted']);
29
+ const SearchStopReasonSchema = z
30
+ .enum(['maxResults', 'maxFiles', 'timeout'])
31
+ .describe('maxResults: result limit hit; maxFiles: file count limit hit; timeout: time limit exceeded');
32
+ const ListDirectoryStopReasonSchema = z
33
+ .enum(['maxEntries', 'aborted'])
34
+ .describe('maxEntries: entry limit hit; aborted: operation was cancelled');
30
35
  const TreeEntrySchema = z.lazy(() => z.strictObject({
31
36
  name: z.string().describe('Name'),
32
37
  type: FileTypeSchema.describe('Type'),
@@ -111,16 +116,17 @@ export const ListDirectoryInputSchema = z.strictObject({
111
116
  .number()
112
117
  .int({ error: 'Must be integer' })
113
118
  .min(1, 'Min: 1')
114
- .max(50, 'Max: 50')
119
+ .max(MAX_TREE_DEPTH, `Max: ${MAX_TREE_DEPTH}`)
115
120
  .optional()
116
121
  .describe('Max recursion depth when pattern is provided'),
117
122
  maxEntries: z
118
123
  .number()
119
124
  .int({ error: 'Must be integer' })
120
125
  .min(1, 'Min: 1')
121
- .max(20000, 'Max: 20,000')
126
+ .max(MAX_LIST_ENTRIES, `Max: ${MAX_LIST_ENTRIES}`)
122
127
  .optional()
123
- .describe('Maximum entries to return before truncation'),
128
+ .default(DEFAULT_LIST_MAX_ENTRIES)
129
+ .describe(`Maximum entries to return before truncation. Default: ${DEFAULT_LIST_MAX_ENTRIES}`),
124
130
  sortBy: ListDirectorySortSchema.optional()
125
131
  .default('name')
126
132
  .describe('Sort field (name, size, modified, type)'),
@@ -157,10 +163,10 @@ export const SearchFilesInputSchema = z.strictObject({
157
163
  .number()
158
164
  .int({ error: 'Must be integer' })
159
165
  .min(1, 'Min: 1')
160
- .max(10000, 'Max: 10,000')
166
+ .max(MAX_SEARCH_RESULTS, `Max: ${MAX_SEARCH_RESULTS}`)
161
167
  .optional()
162
- .default(100)
163
- .describe('Max results (1-10000)'),
168
+ .default(DEFAULT_SEARCH_RESULTS)
169
+ .describe(`Max results (1-${MAX_SEARCH_RESULTS}). Default: ${DEFAULT_SEARCH_RESULTS}`),
164
170
  includeIgnored: z
165
171
  .boolean()
166
172
  .optional()
@@ -178,7 +184,7 @@ export const SearchFilesInputSchema = z.strictObject({
178
184
  .number()
179
185
  .int({ error: 'Must be integer' })
180
186
  .min(0, 'Min: 0')
181
- .max(100, 'Max: 100')
187
+ .max(MAX_SEARCH_DEPTH, `Max: ${MAX_SEARCH_DEPTH}`)
182
188
  .optional()
183
189
  .describe('Maximum directory depth to scan'),
184
190
  cursor: z
@@ -192,18 +198,18 @@ export const TreeInputSchema = z.strictObject({
192
198
  .number()
193
199
  .int({ error: 'Must be integer' })
194
200
  .min(0, 'Min: 0')
195
- .max(50, 'Max: 50')
201
+ .max(MAX_TREE_DEPTH, `Max: ${MAX_TREE_DEPTH}`)
196
202
  .optional()
197
- .default(5)
198
- .describe('Depth (0=root node only, no children). Default: 5'),
203
+ .default(DEFAULT_TREE_DEPTH)
204
+ .describe(`Depth (0=root node only, no children). Default: ${DEFAULT_TREE_DEPTH}`),
199
205
  maxEntries: z
200
206
  .number()
201
207
  .int({ error: 'Must be integer' })
202
208
  .min(1, 'Min: 1')
203
- .max(20000, 'Max: 20,000')
209
+ .max(MAX_TREE_ENTRIES, `Max: ${MAX_TREE_ENTRIES}`)
204
210
  .optional()
205
- .default(1000)
206
- .describe('Max entries (Default: 1000)'),
211
+ .default(DEFAULT_TREE_ENTRIES)
212
+ .describe(`Max entries. Default: ${DEFAULT_TREE_ENTRIES}`),
207
213
  includeHidden: z
208
214
  .boolean()
209
215
  .optional()
@@ -249,10 +255,10 @@ export const SearchContentInputSchema = z.strictObject({
249
255
  .number()
250
256
  .int({ error: 'Must be integer' })
251
257
  .min(0, 'Min: 0')
252
- .max(10000, 'Max: 10,000')
258
+ .max(MAX_SEARCH_RESULTS, `Max: ${MAX_SEARCH_RESULTS}`)
253
259
  .optional()
254
- .default(500)
255
- .describe('Maximum match rows to return'),
260
+ .default(DEFAULT_SEARCH_CONTENT_RESULTS)
261
+ .describe(`Maximum match rows to return. Default: ${DEFAULT_SEARCH_CONTENT_RESULTS}`),
256
262
  filePattern: z
257
263
  .string()
258
264
  .min(1, 'Pattern required')
@@ -28,9 +28,6 @@ function getRootsManager(server) {
28
28
  }
29
29
  return manager;
30
30
  }
31
- function loadServerInstructions() {
32
- return buildServerInstructions();
33
- }
34
31
  async function getLocalIconInfo() {
35
32
  const name = 'logo.svg';
36
33
  const mime = 'image/svg+xml';
@@ -52,7 +49,7 @@ async function getLocalIconInfo() {
52
49
  }
53
50
  export async function createServer(options = {}) {
54
51
  const resourceStore = createInMemoryResourceStore();
55
- const serverInstructions = loadServerInstructions();
52
+ const serverInstructions = buildServerInstructions();
56
53
  const localIcon = await getLocalIconInfo();
57
54
  const taskToolSupport = supportsTaskToolRequests();
58
55
  const serverConfig = {
@@ -68,7 +65,7 @@ export async function createServer(options = {}) {
68
65
  if (serverInstructions) {
69
66
  serverConfig.instructions =
70
67
  'filesystem-mcp: Secure local filesystem MCP server. ' +
71
- 'Essential sequence: roots → ls/tree/find → read/grep. ' +
68
+ 'Always begin with: roots → ls/find → stat → read. Never guess paths. ' +
72
69
  'Full reference: read the internal://instructions resource or invoke the get-help prompt.';
73
70
  }
74
71
  const server = new McpServer(withDefaultIcons({
@@ -113,13 +110,37 @@ export async function startServer(server) {
113
110
  };
114
111
  rootsManager.logMissingDirectoriesIfNeeded(server);
115
112
  }
113
+ const MAX_REQUEST_BODY_BYTES = parseInt(process.env['FS_CONTEXT_MAX_REQUEST_BYTES'] ?? '', 10) ||
114
+ 4 * 1024 * 1024; // 4 MB default
115
+ class RequestBodyError extends Error {
116
+ statusCode;
117
+ constructor(message, statusCode) {
118
+ super(message);
119
+ this.statusCode = statusCode;
120
+ this.name = 'RequestBodyError';
121
+ }
122
+ }
116
123
  async function readRequestBody(req) {
117
124
  return new Promise((resolve, reject) => {
118
125
  const chunks = [];
126
+ let totalBytes = 0;
127
+ let tooBig = false;
119
128
  req.on('data', (chunk) => {
129
+ totalBytes += chunk.length;
130
+ if (totalBytes > MAX_REQUEST_BODY_BYTES) {
131
+ if (!tooBig) {
132
+ tooBig = true;
133
+ chunks.length = 0; // free accumulated memory
134
+ req.pause(); // stop emitting data events; TCP window fills naturally
135
+ reject(new RequestBodyError('Request body too large', 413));
136
+ }
137
+ return;
138
+ }
120
139
  chunks.push(chunk);
121
140
  });
122
141
  req.on('end', () => {
142
+ if (tooBig)
143
+ return; // already rejected in 'data' handler
123
144
  const raw = Buffer.concat(chunks).toString('utf-8');
124
145
  if (!raw) {
125
146
  resolve(undefined);
@@ -129,7 +150,7 @@ async function readRequestBody(req) {
129
150
  resolve(JSON.parse(raw));
130
151
  }
131
152
  catch {
132
- resolve(undefined);
153
+ reject(new RequestBodyError('Invalid JSON in request body', 400));
133
154
  }
134
155
  });
135
156
  req.on('error', reject);
@@ -171,11 +192,22 @@ function sendJsonRpcError(res, status, code, message) {
171
192
  id: null,
172
193
  }));
173
194
  }
195
+ const LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/u;
196
+ function isAllowedOrigin(origin) {
197
+ if (origin === undefined)
198
+ return true; // Non-browser clients omit Origin.
199
+ return LOCALHOST_ORIGIN_RE.test(origin);
200
+ }
174
201
  export async function startHttpServer(port, options) {
175
202
  const sessions = new Map();
176
203
  async function handleMcpRequest(req, res) {
177
204
  const { method } = req;
178
205
  const sessionId = req.headers['mcp-session-id'];
206
+ const { origin } = req.headers;
207
+ if (!isAllowedOrigin(origin)) {
208
+ sendJsonRpcError(res, 403, -32000, 'Forbidden: disallowed origin');
209
+ return;
210
+ }
179
211
  const apiKey = process.env['FILESYSTEM_MCP_API_KEY'];
180
212
  if (apiKey) {
181
213
  const authHeader = req.headers['authorization'];
@@ -237,6 +269,12 @@ export async function startHttpServer(port, options) {
237
269
  }
238
270
  }
239
271
  catch (error) {
272
+ if (error instanceof RequestBodyError && !res.headersSent) {
273
+ const rpcCode = error.statusCode === 413 ? -32600 : -32700;
274
+ res.setHeader('Connection', 'close');
275
+ sendJsonRpcError(res, error.statusCode, rpcCode, error.message);
276
+ return;
277
+ }
240
278
  console.error('[HTTP] Error handling request:', formatUnknownErrorMessage(error));
241
279
  if (!res.headersSent) {
242
280
  sendJsonRpcError(res, 500, -32603, 'Internal Server Error');
@@ -255,10 +293,13 @@ export async function startHttpServer(port, options) {
255
293
  res.end('Not Found');
256
294
  }
257
295
  });
296
+ // Default to localhost-only binding to prevent DNS-rebinding and unintended
297
+ // external exposure. Override with FILESYSTEM_MCP_HTTP_HOST for remote setups.
298
+ const httpHost = process.env['FILESYSTEM_MCP_HTTP_HOST'] ?? '127.0.0.1';
258
299
  return new Promise((resolve, reject) => {
259
300
  httpServer.once('error', reject);
260
- httpServer.listen(port, () => {
261
- console.error(`MCP HTTP server listening on port ${port}`);
301
+ httpServer.listen(port, httpHost, () => {
302
+ console.error(`MCP HTTP server listening on ${httpHost}:${port}`);
262
303
  resolve(httpServer);
263
304
  });
264
305
  });
@@ -20,7 +20,7 @@ function canSendMcpLogs(server) {
20
20
  return false;
21
21
  if (!('logging' in capabilities))
22
22
  return false;
23
- return capabilities.logging !== null;
23
+ return !!capabilities['logging'];
24
24
  }
25
25
  export function logToMcp(server, level, data, minLevel = 'debug') {
26
26
  if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[minLevel]) {
@@ -117,7 +117,7 @@ export class RootsManager {
117
117
  }
118
118
  async recomputeAllowedDirectories() {
119
119
  const cliAllowedDirs = normalizeCLIDirectories(this.options.cliAllowedDirs ?? []);
120
- const allowCwd = this.options.allowCwd === true;
120
+ const allowCwd = Boolean(this.options.allowCwd);
121
121
  const allowCwdDirs = allowCwd ? [normalizePath(process.cwd())] : [];
122
122
  const baseline = [...cliAllowedDirs, ...allowCwdDirs];
123
123
  const { signal, cleanup } = createTimedAbortSignal(undefined, ROOTS_TIMEOUT_MS);
@@ -171,7 +171,7 @@ export function registerCalculateHashTool(server, options = {}) {
171
171
  baseReporter({
172
172
  current,
173
173
  ...(total !== undefined ? { total } : {}),
174
- message: `🕮 calculate_hash: ${baseName} ${current} ${fileWord} hashed`,
174
+ message: `🕮 calculate_hash: ${baseName} [${current} ${fileWord} hashed]`,
175
175
  });
176
176
  };
177
177
  try {
@@ -9,10 +9,11 @@ import { registerToolTaskIfAvailable } from './task-support.js';
9
9
  export const CREATE_DIRECTORY_TOOL = {
10
10
  name: 'mkdir',
11
11
  title: 'Create Directory',
12
- description: 'Create a new directory at the specified path (recursive)',
12
+ description: 'Create a new directory at the specified path (recursive).',
13
13
  inputSchema: CreateDirectoryInputSchema,
14
14
  outputSchema: CreateDirectoryOutputSchema,
15
15
  annotations: IDEMPOTENT_WRITE_TOOL_ANNOTATIONS,
16
+ nuances: ['Succeeds silently if the directory already exists (idempotent).'],
16
17
  };
17
18
  async function handleCreateDirectory(args, signal) {
18
19
  const validPath = await validatePathForWrite(args.path, signal);
@@ -34,11 +35,11 @@ export function registerCreateDirectoryTool(server, options = {}) {
34
35
  const wrappedHandler = wrapToolHandler(handler, {
35
36
  guard: options.isInitialized,
36
37
  progressMessage: (args) => {
37
- const name = path.basename(args.path) || args.path;
38
+ const name = path.basename(args.path) || '.';
38
39
  return `🛠 mkdir: ${name}`;
39
40
  },
40
41
  completionMessage: (args, result) => {
41
- const name = path.basename(args.path) || args.path;
42
+ const name = path.basename(args.path) || '.';
42
43
  if (result.isError)
43
44
  return `🛠 mkdir: ${name} • failed`;
44
45
  return `🛠 mkdir: ${name} • created`;
@@ -9,11 +9,12 @@ import { registerToolTaskIfAvailable } from './task-support.js';
9
9
  export const DELETE_FILE_TOOL = {
10
10
  name: 'rm',
11
11
  title: 'Delete File',
12
- description: 'Delete a file or directory.',
12
+ description: 'Permanently delete a file or directory. This action is irreversible.',
13
13
  inputSchema: DeleteFileInputSchema,
14
14
  outputSchema: DeleteFileOutputSchema,
15
15
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
16
16
  gotchas: [
17
+ 'Deletion is permanent — there is no undo or recycle bin.',
17
18
  'Non-empty directory delete requires `recursive=true`; else returns actionable input error.',
18
19
  ],
19
20
  };
@@ -96,7 +96,8 @@ export function registerEditFileTool(server, options = {}) {
96
96
  guard: options.isInitialized,
97
97
  progressMessage: (args) => {
98
98
  const name = path.basename(args.path);
99
- return `🛠 edit: ${name} [${args.edits.length} edits]`;
99
+ const dryTag = args.dryRun ? ' [dry run]' : '';
100
+ return `🛠 edit: ${name} [${args.edits.length} edits]${dryTag}`;
100
101
  },
101
102
  completionMessage: (args, result) => {
102
103
  const name = path.basename(args.path);
@@ -105,10 +106,16 @@ export function registerEditFileTool(server, options = {}) {
105
106
  const sc = result.structuredContent;
106
107
  if (!sc.ok)
107
108
  return `🛠 edit: ${name} • failed`;
109
+ const applied = sc.appliedEdits ?? 0;
110
+ const unmatched = sc.unmatchedEdits?.length ?? 0;
111
+ const dryPrefix = args.dryRun ? 'dry run — ' : '';
112
+ if (unmatched > 0) {
113
+ return `🛠 edit: ${name} • ${dryPrefix}${applied} applied, ${unmatched} unmatched`;
114
+ }
108
115
  if (sc.lineRange) {
109
- return `🛠 edit: ${name} • [${sc.lineRange[0]}-${sc.lineRange[1]}]`;
116
+ return `🛠 edit: ${name} • ${dryPrefix}lines ${sc.lineRange[0]}–${sc.lineRange[1]}`;
110
117
  }
111
- return `🛠 edit: ${name} • [${sc.appliedEdits ?? 0} edits]`;
118
+ return `🛠 edit: ${name} • ${dryPrefix}${applied} applied`;
112
119
  },
113
120
  });
114
121
  const validatedHandler = withValidatedArgs(EditFileInputSchema, wrappedHandler);
@@ -98,7 +98,7 @@ function decodeCursor(cursor) {
98
98
  async function handleListDirectory(args, signal) {
99
99
  const dirPath = resolvePathOrRoot(args.path);
100
100
  const cursorOffset = args.cursor !== undefined ? decodeCursor(args.cursor) : 0;
101
- const pageSize = args.maxEntries ?? 20_000;
101
+ const pageSize = args.maxEntries;
102
102
  const options = {
103
103
  includeHidden: args.includeHidden,
104
104
  excludePatterns: args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS,
@@ -14,6 +14,9 @@ export const MOVE_FILE_TOOL = {
14
14
  outputSchema: MoveFileOutputSchema,
15
15
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
16
16
  nuances: ['Cross-device moves fall back to copy+delete.'],
17
+ gotchas: [
18
+ 'On POSIX, an existing destination is silently overwritten; on Windows, rename fails with EEXIST if destination exists.',
19
+ ],
17
20
  };
18
21
  async function handleMoveFile(args, signal) {
19
22
  const validSource = await validateExistingPath(args.source, signal);
@@ -265,7 +265,7 @@ export function registerSearchAndReplaceTool(server, options = {}) {
265
265
  baseReporter({
266
266
  current,
267
267
  ...(total !== undefined ? { total } : {}),
268
- message: `🛠 search_and_replace: "${args.searchPattern}" ${current} files processed`,
268
+ message: `🛠 search_and_replace: ${args.searchPattern} [${current} files processed]`,
269
269
  });
270
270
  };
271
271
  try {
@@ -24,7 +24,7 @@ export const SEARCH_CONTENT_TOOL = {
24
24
  'Skips binary and oversized files.',
25
25
  ],
26
26
  gotchas: [
27
- 'Inline match rows are capped (first 50); full structured results are externalized via `resourceUri`.',
27
+ 'Skips binary and oversized files silently check file type with `stat` if no matches appear.',
28
28
  ],
29
29
  taskSupport: 'required',
30
30
  };
@@ -220,7 +220,7 @@ export function registerSearchContentTool(server, options = {}) {
220
220
  baseReporter({
221
221
  current,
222
222
  ...(total !== undefined ? { total } : {}),
223
- message: `🔎︎ grep: ${pattern} ${current} ${fileWord} scanned`,
223
+ message: `🔎︎ grep: ${pattern} [${current} ${fileWord} scanned]`,
224
224
  });
225
225
  };
226
226
  try {
@@ -8,6 +8,7 @@ export { type ToolContract } from './contract.js';
8
8
  export declare const READ_ONLY_TOOL_ANNOTATIONS: {
9
9
  readonly readOnlyHint: true;
10
10
  readonly idempotentHint: true;
11
+ readonly destructiveHint: false;
11
12
  readonly openWorldHint: false;
12
13
  };
13
14
  export declare const DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS: {
@@ -18,6 +19,7 @@ export declare const DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS: {
18
19
  export declare const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS: {
19
20
  readonly readOnlyHint: false;
20
21
  readonly idempotentHint: true;
22
+ readonly destructiveHint: false;
21
23
  readonly openWorldHint: false;
22
24
  };
23
25
  export declare function shouldStripStructuredOutput(): boolean;
@@ -1,5 +1,6 @@
1
1
  import { channel } from 'node:diagnostics_channel';
2
2
  import { z } from 'zod';
3
+ import { parseTrueEnvFlag } from '../lib/constants.js';
3
4
  import { createDetailedError, ErrorCode, formatDetailedError, getSuggestion, McpError, } from '../lib/errors.js';
4
5
  import { createTimedAbortSignal } from '../lib/fs-helpers.js';
5
6
  import { withToolDiagnostics } from '../lib/observability.js';
@@ -8,7 +9,6 @@ export {} from './contract.js';
8
9
  const MAX_INLINE_CONTENT_CHARS = parseInt(process.env['FS_CONTEXT_MAX_INLINE_CHARS'] ?? '', 10) || 20_000;
9
10
  const MAX_INLINE_PREVIEW_CHARS = 4_000;
10
11
  const PROGRESS_RATE_LIMIT_MS = 50;
11
- const TRUE_ENV_VALUES = new Set(['1', 'true', 'yes']);
12
12
  const CONTEXT_DIAGNOSTICS_CHANNEL = channel('filesystem-mcp:context');
13
13
  function publishContextDiagnostics(event) {
14
14
  if (!CONTEXT_DIAGNOSTICS_CHANNEL.hasSubscribers)
@@ -18,6 +18,7 @@ function publishContextDiagnostics(event) {
18
18
  export const READ_ONLY_TOOL_ANNOTATIONS = {
19
19
  readOnlyHint: true,
20
20
  idempotentHint: true,
21
+ destructiveHint: false,
21
22
  openWorldHint: false,
22
23
  };
23
24
  export const DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS = {
@@ -28,13 +29,11 @@ export const DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS = {
28
29
  export const IDEMPOTENT_WRITE_TOOL_ANNOTATIONS = {
29
30
  readOnlyHint: false,
30
31
  idempotentHint: true,
32
+ destructiveHint: false,
31
33
  openWorldHint: false,
32
34
  };
33
35
  export function shouldStripStructuredOutput() {
34
- const value = process.env['FS_CONTEXT_STRIP_STRUCTURED'];
35
- if (value === undefined)
36
- return false;
37
- return TRUE_ENV_VALUES.has(value.trim().toLowerCase());
36
+ return parseTrueEnvFlag(process.env['FS_CONTEXT_STRIP_STRUCTURED']);
38
37
  }
39
38
  export function maybeStripStructuredContentFromResult(result) {
40
39
  if (!shouldStripStructuredOutput())
@@ -9,7 +9,7 @@ import { registerToolTaskIfAvailable } from './task-support.js';
9
9
  export const GET_MULTIPLE_FILE_INFO_TOOL = {
10
10
  name: 'stat_many',
11
11
  title: 'Get Multiple File Info',
12
- description: 'Get metadata for multiple files or directories in one request.',
12
+ description: 'Get metadata (including tokenEstimate) for multiple files or directories in one request. Use tokenEstimate (size÷4) to pre-screen token cost before reading.',
13
13
  inputSchema: GetMultipleFileInfoInputSchema,
14
14
  outputSchema: GetMultipleFileInfoOutputSchema,
15
15
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
@@ -8,7 +8,7 @@ import { buildFileInfoPayload, buildToolErrorResponse, buildToolResponse, execut
8
8
  export const GET_FILE_INFO_TOOL = {
9
9
  name: 'stat',
10
10
  title: 'Get File Info',
11
- description: 'Get metadata (size, modified time, permissions, mime type) for a file or directory.',
11
+ description: 'Get metadata (size, modified time, permissions, mime type, tokenEstimate) for a file or directory. Use tokenEstimate (size÷4) to pre-screen token cost before reading.',
12
12
  inputSchema: GetFileInfoInputSchema,
13
13
  outputSchema: GetFileInfoOutputSchema,
14
14
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
@@ -9,15 +9,12 @@ import { registerToolTaskIfAvailable } from './task-support.js';
9
9
  export const WRITE_FILE_TOOL = {
10
10
  name: 'write',
11
11
  title: 'Write File',
12
- description: 'Write content to a file. Creates the file if it does not exist.',
12
+ description: 'Write content to a file, OVERWRITING ALL existing content. Creates the file and parent directories if needed.',
13
13
  inputSchema: WriteFileInputSchema,
14
14
  outputSchema: WriteFileOutputSchema,
15
15
  annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS,
16
- nuances: [
17
- 'Creates parent directories automatically; overwrites existing content.',
18
- ],
19
16
  gotchas: [
20
- 'Creates parent directories automatically; overwrites existing content.',
17
+ '`write` replaces ALL existing content use `edit` for partial updates.',
21
18
  ],
22
19
  };
23
20
  async function handleWriteFile(args, signal) {
package/dist/tools.js CHANGED
@@ -18,48 +18,38 @@ import { GET_FILE_INFO_TOOL, registerGetFileInfoTool } from './tools/stat.js';
18
18
  import { registerTreeTool, TREE_TOOL } from './tools/tree.js';
19
19
  import { registerWriteFileTool, WRITE_FILE_TOOL } from './tools/write-file.js';
20
20
  export { buildToolErrorResponse, buildToolResponse } from './tools/shared.js';
21
- export const ALL_TOOLS = [
22
- LIST_ALLOWED_DIRECTORIES_TOOL,
23
- LIST_DIRECTORY_TOOL,
24
- SEARCH_FILES_TOOL,
25
- TREE_TOOL,
26
- READ_FILE_TOOL,
27
- READ_MULTIPLE_FILES_TOOL,
28
- GET_FILE_INFO_TOOL,
29
- GET_MULTIPLE_FILE_INFO_TOOL,
30
- SEARCH_CONTENT_TOOL,
31
- CREATE_DIRECTORY_TOOL,
32
- WRITE_FILE_TOOL,
33
- EDIT_FILE_TOOL,
34
- MOVE_FILE_TOOL,
35
- DELETE_FILE_TOOL,
36
- CALCULATE_HASH_TOOL,
37
- DIFF_FILES_TOOL,
38
- APPLY_PATCH_TOOL,
39
- SEARCH_AND_REPLACE_TOOL,
40
- ];
41
- const TOOL_REGISTRARS = [
42
- registerListAllowedDirectoriesTool,
43
- registerListDirectoryTool,
44
- registerSearchFilesTool,
45
- registerTreeTool,
46
- registerReadFileTool,
47
- registerReadMultipleFilesTool,
48
- registerGetFileInfoTool,
49
- registerGetMultipleFileInfoTool,
50
- registerSearchContentTool,
51
- registerCreateDirectoryTool,
52
- registerWriteFileTool,
53
- registerEditFileTool,
54
- registerMoveFileTool,
55
- registerDeleteFileTool,
56
- registerCalculateHashTool,
57
- registerDiffFilesTool,
58
- registerApplyPatchTool,
59
- registerSearchAndReplaceTool,
21
+ const TOOL_ENTRIES = [
22
+ {
23
+ contract: LIST_ALLOWED_DIRECTORIES_TOOL,
24
+ register: registerListAllowedDirectoriesTool,
25
+ },
26
+ { contract: LIST_DIRECTORY_TOOL, register: registerListDirectoryTool },
27
+ { contract: SEARCH_FILES_TOOL, register: registerSearchFilesTool },
28
+ { contract: TREE_TOOL, register: registerTreeTool },
29
+ { contract: READ_FILE_TOOL, register: registerReadFileTool },
30
+ {
31
+ contract: READ_MULTIPLE_FILES_TOOL,
32
+ register: registerReadMultipleFilesTool,
33
+ },
34
+ { contract: GET_FILE_INFO_TOOL, register: registerGetFileInfoTool },
35
+ {
36
+ contract: GET_MULTIPLE_FILE_INFO_TOOL,
37
+ register: registerGetMultipleFileInfoTool,
38
+ },
39
+ { contract: SEARCH_CONTENT_TOOL, register: registerSearchContentTool },
40
+ { contract: CREATE_DIRECTORY_TOOL, register: registerCreateDirectoryTool },
41
+ { contract: WRITE_FILE_TOOL, register: registerWriteFileTool },
42
+ { contract: EDIT_FILE_TOOL, register: registerEditFileTool },
43
+ { contract: MOVE_FILE_TOOL, register: registerMoveFileTool },
44
+ { contract: DELETE_FILE_TOOL, register: registerDeleteFileTool },
45
+ { contract: CALCULATE_HASH_TOOL, register: registerCalculateHashTool },
46
+ { contract: DIFF_FILES_TOOL, register: registerDiffFilesTool },
47
+ { contract: APPLY_PATCH_TOOL, register: registerApplyPatchTool },
48
+ { contract: SEARCH_AND_REPLACE_TOOL, register: registerSearchAndReplaceTool },
60
49
  ];
50
+ export const ALL_TOOLS = TOOL_ENTRIES.map((e) => e.contract);
61
51
  export function registerAllTools(server, options = {}) {
62
- for (const registerTool of TOOL_REGISTRARS) {
63
- registerTool(server, options);
52
+ for (const { register } of TOOL_ENTRIES) {
53
+ register(server, options);
64
54
  }
65
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/filesystem-mcp",
3
- "version": "1.5.0",
3
+ "version": "1.5.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",
@@ -73,7 +73,7 @@
73
73
  },
74
74
  "devDependencies": {
75
75
  "@eslint/js": "^10.0.1",
76
- "eslint": "^10.0.0",
76
+ "eslint": "^10.0.1",
77
77
  "@trivago/prettier-plugin-sort-imports": "^6.0.2",
78
78
  "@types/node": "^24",
79
79
  "eslint-config-prettier": "^10.1.8",
@@ -81,7 +81,7 @@
81
81
  "eslint-plugin-depend": "^1.4.0",
82
82
  "eslint-plugin-unused-imports": "^4.4.1",
83
83
  "jscpd": "^4.0.8",
84
- "knip": "^5.84.1",
84
+ "knip": "^5.85.0",
85
85
  "prettier": "^3.8.1",
86
86
  "tsx": "^4.21.0",
87
87
  "typescript": "^5.9.3",