@j0hanz/filesystem-mcp 1.7.2 → 1.7.3

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.
@@ -3,9 +3,9 @@ import { DEFAULT_SEARCH_MAX_FILES, DEFAULT_SEARCH_TIMEOUT_MS, } from '../constan
3
3
  import { createTimedAbortSignal } from '../fs-helpers.js';
4
4
  import { isSensitivePath } from '../path-policy.js';
5
5
  import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
6
- import { needsStatsForSort, withOptionalStoppedReason } from './common.js';
6
+ import { compareOptionalNumberDesc, compareStringValues, isEntryAccessibleByType, needsStatsForSort, resolveEntryType, resolveStopReason, stableSortByDerivedString, withOptionalStoppedReason, } from './common.js';
7
7
  import { isIgnoredByGitignore, loadRootGitignore } from './gitignore.js';
8
- import { globEntries, resolveEntryType } from './glob-engine.js';
8
+ import { globEntries } from './glob-engine.js';
9
9
  // Internal default for find tool - not exposed to MCP users
10
10
  const INTERNAL_MAX_RESULTS = 1000;
11
11
  function normalizeOptions(options) {
@@ -41,17 +41,17 @@ function buildSearchResult(entry, entryType, needsStats) {
41
41
  ...(modified !== undefined ? { modified } : {}),
42
42
  };
43
43
  }
44
- function markStopped(state, reason) {
45
- state.truncated = true;
46
- state.stoppedReason = reason;
47
- }
48
44
  function shouldStopCollecting(state, normalized, signal) {
49
- if (signal.aborted) {
50
- markStopped(state, 'timeout');
51
- return true;
52
- }
53
- if (state.filesScanned >= normalized.maxFilesScanned) {
54
- markStopped(state, 'maxFiles');
45
+ const stopReason = resolveStopReason({
46
+ signal,
47
+ current: state.filesScanned,
48
+ max: normalized.maxFilesScanned,
49
+ abortedReason: 'timeout',
50
+ maxReason: 'maxFiles',
51
+ });
52
+ if (stopReason !== undefined) {
53
+ state.truncated = true;
54
+ state.stoppedReason = stopReason;
55
55
  return true;
56
56
  }
57
57
  return false;
@@ -99,7 +99,8 @@ function buildCollectResult(state) {
99
99
  function handleEntry(entry, entryType, needsStats, normalized, state) {
100
100
  state.results.push(buildSearchResult(entry, entryType, needsStats));
101
101
  if (state.results.length >= normalized.maxResults) {
102
- markStopped(state, 'maxResults');
102
+ state.truncated = true;
103
+ state.stoppedReason = 'maxResults';
103
104
  }
104
105
  }
105
106
  function reportSearchFilesProgress(onProgress, current, total, force = false) {
@@ -109,7 +110,7 @@ function reportSearchFilesProgress(onProgress, current, total, force = false) {
109
110
  return;
110
111
  onProgress({ current, total });
111
112
  }
112
- async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, onProgress) {
113
+ async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress) {
113
114
  for await (const entry of stream) {
114
115
  if (shouldStopCollecting(state, normalized, signal))
115
116
  break;
@@ -122,7 +123,7 @@ async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher
122
123
  if (!shouldIncludeEntry(entryType, normalized)) {
123
124
  continue;
124
125
  }
125
- const isAccessible = await isEntryAccessible(entry, entryType, rootDirectories, signal);
126
+ const isAccessible = await isEntryAccessibleByType(entry.path, entryType, rootDirectories, signal, accessDeps);
126
127
  if (!isAccessible) {
127
128
  state.skippedInaccessible++;
128
129
  continue;
@@ -138,31 +139,21 @@ function isEntryIgnoredByGitignore(matcher, root, entryPath, relativePath) {
138
139
  return false;
139
140
  return isIgnoredByGitignore(matcher, root, entryPath, relativePath ? { relativePath } : {});
140
141
  }
141
- async function isEntryAccessible(entry, entryType, rootDirectories, signal) {
142
- if (entryType === 'symlink') {
143
- try {
144
- const validated = await validateExistingPathDetailed(entry.path, signal);
145
- return !isSensitivePath(validated.requestedPath, validated.resolvedPath);
146
- }
147
- catch {
148
- return false;
149
- }
150
- }
151
- const resolvedPath = normalizePath(entry.path);
152
- if (!isPathWithinDirectories(resolvedPath, rootDirectories)) {
153
- return false;
154
- }
155
- return !isSensitivePath(entry.path, resolvedPath);
156
- }
157
142
  async function collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress) {
158
143
  const needsStats = needsStatsForSort(normalized.sortBy);
159
144
  const stream = buildSearchStream(root, pattern, excludePatterns, normalized, needsStats);
160
145
  const state = createCollectState();
161
146
  const rootDirectories = [root];
147
+ const accessDeps = {
148
+ normalizePath,
149
+ isPathWithinDirectories,
150
+ isSensitivePath,
151
+ validateSymlinkPath: validateExistingPathDetailed,
152
+ };
162
153
  const gitignoreMatcher = normalized.respectGitignore
163
154
  ? await loadRootGitignore(root, signal)
164
155
  : null;
165
- await collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, onProgress);
156
+ await collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress);
166
157
  return buildCollectResult(state);
167
158
  }
168
159
  function buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible) {
@@ -174,27 +165,17 @@ function buildSearchSummary(results, filesScanned, truncated, stoppedReason, ski
174
165
  };
175
166
  return withOptionalStoppedReason(summary, stoppedReason);
176
167
  }
177
- const collator = new Intl.Collator(undefined, { numeric: true });
178
- function compareString(a, b) {
179
- return collator.compare(a ?? '', b ?? '');
180
- }
181
168
  function compareNameThenPath(a, b) {
182
- const nameCompare = compareString(a.name, b.name);
169
+ const nameCompare = compareStringValues(a.name, b.name);
183
170
  if (nameCompare !== 0)
184
171
  return nameCompare;
185
- return compareString(a.path, b.path);
172
+ return compareStringValues(a.path, b.path);
186
173
  }
187
174
  function comparePathThenName(a, b) {
188
- const pathCompare = compareString(a.path, b.path);
175
+ const pathCompare = compareStringValues(a.path, b.path);
189
176
  if (pathCompare !== 0)
190
177
  return pathCompare;
191
- return compareString(a.name, b.name);
192
- }
193
- function compareOptionalNumberDesc(left, right, tieBreak) {
194
- const diff = (right ?? 0) - (left ?? 0);
195
- if (diff !== 0)
196
- return diff;
197
- return tieBreak();
178
+ return compareStringValues(a.name, b.name);
198
179
  }
199
180
  const SORT_COMPARATORS = {
200
181
  size: (a, b) => compareOptionalNumberDesc(a.size, b.size, () => compareNameThenPath(a, b)),
@@ -204,32 +185,7 @@ const SORT_COMPARATORS = {
204
185
  };
205
186
  export function sortSearchResults(results, sortBy) {
206
187
  if (sortBy === 'name') {
207
- const decorated = [];
208
- for (let index = 0; index < results.length; index += 1) {
209
- const item = results[index];
210
- if (!item)
211
- continue;
212
- decorated.push({
213
- item,
214
- baseName: path.basename(item.path ?? ''),
215
- index,
216
- });
217
- }
218
- decorated.sort((a, b) => {
219
- const baseCompare = compareString(a.baseName, b.baseName);
220
- if (baseCompare !== 0)
221
- return baseCompare;
222
- const pathCompare = compareString(a.item.path, b.item.path);
223
- if (pathCompare !== 0)
224
- return pathCompare;
225
- return a.index - b.index;
226
- });
227
- for (let index = 0; index < decorated.length; index += 1) {
228
- const entry = decorated[index];
229
- if (entry) {
230
- results[index] = entry.item;
231
- }
232
- }
188
+ stableSortByDerivedString(results, (item) => path.basename(item.path ?? ''), (left, right) => comparePathThenName(left, right));
233
189
  return;
234
190
  }
235
191
  const comparator = SORT_COMPARATORS[sortBy];
@@ -1,7 +1,7 @@
1
- type TreeEntryType = 'file' | 'directory' | 'symlink' | 'other';
1
+ import type { EntryType } from './common.js';
2
2
  interface TreeEntry {
3
3
  name: string;
4
- type: TreeEntryType;
4
+ type: EntryType;
5
5
  relativePath: string;
6
6
  children?: TreeEntry[];
7
7
  }
@@ -4,8 +4,9 @@ import { createTimedAbortSignal } from '../fs-helpers.js';
4
4
  import { toPosixPath } from '../path-format.js';
5
5
  import { isSensitivePath } from '../path-policy.js';
6
6
  import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
7
+ import { isEntryAccessibleByType, resolveEntryType, resolveStopReason, } from './common.js';
7
8
  import { isIgnoredByGitignore, loadRootGitignore } from './gitignore.js';
8
- import { globEntries, resolveEntryType } from './glob-engine.js';
9
+ import { globEntries } from './glob-engine.js';
9
10
  function toSafeNonNegativeInt(value, fallback) {
10
11
  if (typeof value !== 'number' || !Number.isFinite(value))
11
12
  return fallback;
@@ -90,36 +91,11 @@ function getTreeTypeRank(type) {
90
91
  return 1;
91
92
  return 2;
92
93
  }
93
- function getStopReason(signal, totalEntries, maxEntries) {
94
- if (signal.aborted) {
95
- return 'aborted';
96
- }
97
- if (totalEntries >= maxEntries) {
98
- return 'maxEntries';
99
- }
100
- return undefined;
101
- }
102
- async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal) {
94
+ async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal, accessDeps) {
103
95
  const type = resolveEntryType(entry.dirent);
104
- if (type !== 'symlink') {
105
- const normalized = normalizePath(entry.path);
106
- if (!isPathWithinDirectories(normalized, rootDirectories)) {
107
- return null;
108
- }
109
- if (isSensitivePath(entry.path, normalized)) {
110
- return null;
111
- }
112
- }
113
- else {
114
- try {
115
- const validated = await validateExistingPathDetailed(entry.path, signal);
116
- if (isSensitivePath(validated.requestedPath, validated.resolvedPath)) {
117
- return null;
118
- }
119
- }
120
- catch {
121
- return null;
122
- }
96
+ const isAccessible = await isEntryAccessibleByType(entry.path, type, rootDirectories, signal, accessDeps);
97
+ if (!isAccessible) {
98
+ return null;
123
99
  }
124
100
  if (gitignoreMatcher &&
125
101
  isIgnoredByGitignore(gitignoreMatcher, root, entry.path, {
@@ -222,6 +198,12 @@ export async function treeDirectory(dirPath, options = {}) {
222
198
  const root = await validateExistingDirectory(dirPath, signal);
223
199
  const rootNormalized = normalizePath(root);
224
200
  const rootDirectories = [rootNormalized];
201
+ const accessDeps = {
202
+ normalizePath,
203
+ isPathWithinDirectories,
204
+ isSensitivePath,
205
+ validateSymlinkPath: validateExistingPathDetailed,
206
+ };
225
207
  try {
226
208
  const excludePatterns = normalized.includeIgnored
227
209
  ? []
@@ -253,12 +235,18 @@ export async function treeDirectory(dirPath, options = {}) {
253
235
  suppressErrors: true,
254
236
  });
255
237
  for await (const entry of stream) {
256
- const stopReason = getStopReason(signal, totalEntries, normalized.maxEntries);
238
+ const stopReason = resolveStopReason({
239
+ signal,
240
+ current: totalEntries,
241
+ max: normalized.maxEntries,
242
+ abortedReason: 'aborted',
243
+ maxReason: 'maxEntries',
244
+ });
257
245
  if (stopReason) {
258
246
  truncated = true;
259
247
  break;
260
248
  }
261
- const resolved = await resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal);
249
+ const resolved = await resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal, accessDeps);
262
250
  if (!resolved) {
263
251
  continue;
264
252
  }
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 = 'Retrieve the full filesystem-mcp XML usage guide.';
5
+ const HELP_PROMPT_DESCRIPTION = 'Return filesystem-mcp usage instructions.';
6
6
  function filterInstructionsByTopic(instructions, topic) {
7
7
  const normalized = topic.trim().toLowerCase();
8
8
  if (!normalized)
@@ -16,7 +16,7 @@ function filterInstructionsByTopic(instructions, topic) {
16
16
  .map((sec) => sec.split('\n')[0]?.replace(/^##\s*/u, '') ?? '')
17
17
  .filter(Boolean)
18
18
  .join(', ');
19
- return `Section '${topic}' not found. Available sections: ${available}\n\n${instructions}`;
19
+ return `Section '${topic}' not found. Available: ${available}\n\n${instructions}`;
20
20
  }
21
21
  export function registerGetHelpPrompt(server, instructions, iconInfo) {
22
22
  const baseConfig = withDefaultIcons({ title: HELP_PROMPT_TITLE, description: HELP_PROMPT_DESCRIPTION }, iconInfo);
@@ -26,7 +26,7 @@ export function registerGetHelpPrompt(server, instructions, iconInfo) {
26
26
  topic: z
27
27
  .string()
28
28
  .optional()
29
- .describe('Section heading prefix to filter (e.g. "error handling strategy"). Omit for full instructions.'),
29
+ .describe('Optional section heading prefix (example: "error handling"). Omit to return full instructions.'),
30
30
  },
31
31
  }, ({ topic }) => {
32
32
  const text = topic
@@ -2,7 +2,7 @@ import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
2
2
  import { buildCoreContextPack, getSharedConstraints, getToolContracts, } from './tool-info.js';
3
3
  import { buildWorkflowGuide } from './workflows.js';
4
4
  const INSTRUCTIONS_HEADER = `<role>
5
- Expert filesystem agent. Operate ONLY within allowed roots. Always discover before acting never guess paths.
5
+ Filesystem agent for local paths only. Operate inside allowed roots. Discover before action. Never guess paths.
6
6
  </role>
7
7
 
8
8
  <tools_overview>
@@ -15,16 +15,16 @@ Expert filesystem agent. Operate ONLY within allowed roots. Always discover befo
15
15
  </tools_overview>
16
16
 
17
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.
18
+ - \`internal://instructions\`: Full usage reference.
19
+ - \`internal://tool-catalog\`: Tool routing and data-flow rules.
20
+ - \`internal://workflows\`: Standard execution sequences.
21
+ - \`internal://tool-info/{name}\`: Per-tool nuances and gotchas (example: \`internal://tool-info/read\`).
22
+ - \`filesystem-mcp://result/{id}\`: Cached large output. If \`resourceUri\` is returned, call \`resources/read\` immediately.
23
+ - \`filesystem-mcp://metrics\`: Per-tool runtime metrics.
24
24
  </resources>
25
25
 
26
26
  <task_protocol>
27
- Async execution: provide \`_meta.progressToken\` in \`tools/call\`, poll \`tasks/get\`, call \`tasks/result\`.
27
+ Async execution: pass \`_meta.progressToken\` in \`tools/call\`, poll \`tasks/get\`, then call \`tasks/result\`.
28
28
  Task-capable: \`find\`, \`tree\`, \`read\`, \`read_many\`, \`stat_many\`, \`grep\`, \`mkdir\`, \`write\`, \`mv\`, \`rm\`, \`calculate_hash\`, \`apply_patch\`, \`search_and_replace\`.
29
29
  </task_protocol>
30
30
  `;
@@ -35,19 +35,19 @@ ${getSharedConstraints()
35
35
  </constraints>
36
36
 
37
37
  <error_handling>
38
- - \`E_ACCESS_DENIED\` Call \`roots\`; use allowed path.
39
- - \`E_NOT_FOUND\` Call \`ls\`/\`find\`; verify spelling.
40
- - \`E_TOO_LARGE\` Use range/head or \`read_many\`.
41
- - \`E_TIMEOUT\` Reduce scope or result limits.
38
+ - \`E_ACCESS_DENIED\` => call \`roots\`, then use an allowed path.
39
+ - \`E_NOT_FOUND\` => call \`ls\` or \`find\`, then verify spelling.
40
+ - \`E_TOO_LARGE\` => use \`head\`, line ranges, or \`read_many\`.
41
+ - \`E_TIMEOUT\` => reduce scope or result limits.
42
42
  </error_handling>
43
43
  `;
44
44
  function formatToolSection(tool) {
45
45
  const parts = [`### ${tool.name}\n${tool.description}`];
46
46
  if (tool.nuances && tool.nuances.length > 0) {
47
- parts.push(...tool.nuances.map((n) => ${n}`));
47
+ parts.push(...tool.nuances.map((n) => `- Nuance: ${n}`));
48
48
  }
49
49
  if (tool.gotchas && tool.gotchas.length > 0) {
50
- parts.push(...tool.gotchas.map((g) => `⚠ ${g}`));
50
+ parts.push(...tool.gotchas.map((g) => `- Gotcha: ${g}`));
51
51
  }
52
52
  return parts.join('\n');
53
53
  }
@@ -9,21 +9,21 @@ diff_files (patch text) -> apply_patch.patch
9
9
 
10
10
  ## Search Strategy
11
11
 
12
- - Use \`find\` for glob-based file discovery.
13
- - Use \`grep\` for content-based searches.
14
- - Use \`search_and_replace\` ONLY for bulk replacements, not for discovery.
12
+ - Use \`find\` for glob file discovery.
13
+ - Use \`grep\` for text search.
14
+ - Use \`search_and_replace\` only for replacement, never discovery.
15
15
 
16
16
  ## Write Strategy
17
17
 
18
- - Use \`edit\` for precise, single-occurrence string replacements in existing files.
19
- - Use \`write\` to create new files or completely overwrite existing content.
20
- - Use \`search_and_replace\` for bulk regex replacements across multiple files.
18
+ - Use \`edit\` for precise, first-occurrence replacements in existing files.
19
+ - Use \`write\` to create files or overwrite full contents.
20
+ - Use \`search_and_replace\` for bulk multi-file replacements.
21
21
 
22
22
  ## Patch Management
23
23
 
24
- - Always generate a patch with \`diff_files\` first.
25
- - Always use \`dryRun: true\` with \`apply_patch\` to verify changes.
26
- - \`apply_patch\` works on unified diff format.
24
+ - Generate patches with \`diff_files\` first.
25
+ - Run \`apply_patch\` with \`dryRun: true\` before writing.
26
+ - \`apply_patch\` accepts unified diffs only.
27
27
  </tool_selection_guide>
28
28
  `;
29
29
  export function buildToolCatalog() {
@@ -37,10 +37,10 @@ export function buildCoreContextPack() {
37
37
  }
38
38
  export function getSharedConstraints() {
39
39
  return [
40
- 'Allowed roots only (negotiated via CLI).',
41
- 'Sensitive files denylisted by default.',
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.',
40
+ 'Use allowed roots only (provided by CLI negotiation).',
41
+ 'Sensitive paths are denylisted by default.',
42
+ `Limits are enforced: max file size ${Math.floor(MAX_TEXT_FILE_SIZE / 1024 / 1024)}MB; search caps ${MAX_SEARCH_RESULTS} files and ${DEFAULT_SEARCH_CONTENT_RESULTS} lines.`,
43
+ 'If a response includes `resourceUri`, call `resources/read` immediately; cached results expire on process restart.',
44
44
  ];
45
45
  }
46
46
  export function buildToolInfo(name) {
@@ -49,7 +49,7 @@ export function buildToolInfo(name) {
49
49
  return undefined;
50
50
  const lines = [`## ${entry.name}`, '', entry.description];
51
51
  if (entry.annotations && entry.annotations.length > 0) {
52
- lines.push('', `**Annotations:** ${entry.annotations.join(', ')}`);
52
+ lines.push('', `**Hints:** ${entry.annotations.join(', ')}`);
53
53
  }
54
54
  if (entry.nuances && entry.nuances.length > 0) {
55
55
  lines.push('', '**Nuances:**');
@@ -1,33 +1,33 @@
1
1
  export function buildWorkflowGuide() {
2
2
  return `<workflows>
3
3
  ### A: EXPLORE
4
- Use when: navigating an unfamiliar directory or reading file content.
5
- 1. \`roots\` (List allowed paths).
6
- 2. \`ls\` (files) | \`tree\` (structure).
7
- 3. \`stat\` | \`stat_many\` (size/type check).
8
- 4. \`read\` | \`read_many\` (content).
9
- > **Strict:** Never guess paths. Resolve first.
4
+ Use when: you need directory layout or file content.
5
+ 1. \`roots\` (list allowed paths).
6
+ 2. \`ls\` (flat view) or \`tree\` (recursive view).
7
+ 3. \`stat\` or \`stat_many\` (type and size checks).
8
+ 4. \`read\` or \`read_many\` (read content).
9
+ > **Strict:** Resolve paths first. Never guess.
10
10
 
11
11
  ### B: SEARCH
12
- Use when: locating files by name pattern or by content match.
12
+ Use when: you need files by pattern or content.
13
13
  1. \`find\` (glob candidates).
14
- 2. \`grep\` (content search).
15
- 3. \`read\` (verify context).
16
- > **Strict:** Use \`grep\` for content search, not \`find\`.
14
+ 2. \`grep\` (content matches).
15
+ 3. \`read\` (verify matched context).
16
+ > **Strict:** Do content search with \`grep\`, not \`find\`.
17
17
 
18
18
  ### C: EDIT
19
- Use when: modifying existing files or reorganizing the filesystem.
20
- 1. \`edit\` (precise string match).
21
- 2. \`search_and_replace\` (bulk regex/glob).
22
- 3. \`mv\` | \`rm\` (file layout).
23
- 4. \`mkdir\` (create dirs).
19
+ Use when: you need to modify files or layout.
20
+ 1. \`edit\` (targeted string replacement).
21
+ 2. \`search_and_replace\` (bulk replacements).
22
+ 3. \`mv\` or \`rm\` (layout changes).
23
+ 4. \`mkdir\` (directory creation).
24
24
  > **Strict:** Confirm destructive ops (\`write\`, \`mv\`, \`rm\`, bulk replace).
25
25
 
26
26
  ### D: PATCH
27
- Use when: applying structured diffs produced by \`diff_files\`.
27
+ Use when: applying unified diffs from \`diff_files\`.
28
28
  1. \`diff_files\` (generate).
29
29
  2. \`apply_patch\` (dryRun: true).
30
30
  3. \`apply_patch\` (dryRun: false).
31
- > **Tip:** Pass \`diff_files\` output directly into \`apply_patch\`.
31
+ > **Tip:** Feed \`diff_files\` output directly to \`apply_patch\`.
32
32
  </workflows>`;
33
33
  }
package/dist/schemas.js CHANGED
@@ -18,6 +18,9 @@ function isSafeGlobPattern(value) {
18
18
  const MAX_PATH_LENGTH = 4096;
19
19
  const DESC_PATH_ROOT = 'Base directory (default: root). Absolute path required if multiple roots exist. Examples: "src", "src/components"';
20
20
  const DESC_PATH_REQUIRED = 'Absolute path to file or directory. Examples: "src/index.ts", "README.md"';
21
+ function defaultFalseBoolean(description) {
22
+ return z.boolean().optional().default(false).describe(description);
23
+ }
21
24
  const PathSchemaBase = z
22
25
  .string()
23
26
  .max(MAX_PATH_LENGTH, `Path too long (max ${MAX_PATH_LENGTH} chars)`);
@@ -109,16 +112,8 @@ const OperationSummarySchema = z.strictObject({
109
112
  });
110
113
  export const ListDirectoryInputSchema = z.strictObject({
111
114
  path: OptionalPathSchema.describe(DESC_PATH_ROOT),
112
- includeHidden: z
113
- .boolean()
114
- .optional()
115
- .default(false)
116
- .describe('Include hidden items (starting with .)'),
117
- includeIgnored: z
118
- .boolean()
119
- .optional()
120
- .default(false)
121
- .describe('Include ignored items (node_modules, .git, etc).'),
115
+ includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
116
+ includeIgnored: defaultFalseBoolean('Include ignored items (node_modules, .git, etc).'),
122
117
  maxDepth: z
123
118
  .number()
124
119
  .int({ error: 'Must be integer' })
@@ -143,11 +138,7 @@ export const ListDirectoryInputSchema = z.strictObject({
143
138
  .max(1000, 'Max 1000 chars')
144
139
  .optional()
145
140
  .describe('Optional glob pattern filter (e.g. "**/*.ts")'),
146
- includeSymlinkTargets: z
147
- .boolean()
148
- .optional()
149
- .default(false)
150
- .describe('Resolve and include symlink targets in results'),
141
+ includeSymlinkTargets: defaultFalseBoolean('Resolve and include symlink targets in results'),
151
142
  cursor: z
152
143
  .string()
153
144
  .optional()
@@ -174,16 +165,8 @@ export const SearchFilesInputSchema = z.strictObject({
174
165
  .optional()
175
166
  .default(DEFAULT_SEARCH_RESULTS)
176
167
  .describe(`Max results (1-${MAX_SEARCH_RESULTS}). Default: ${DEFAULT_SEARCH_RESULTS}`),
177
- includeIgnored: z
178
- .boolean()
179
- .optional()
180
- .default(false)
181
- .describe('Include ignored items (node_modules, etc).'),
182
- includeHidden: z
183
- .boolean()
184
- .optional()
185
- .default(false)
186
- .describe('Include hidden items (starting with .)'),
168
+ includeIgnored: defaultFalseBoolean('Include ignored items (node_modules, etc).'),
169
+ includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
187
170
  sortBy: SearchFilesSortSchema.optional()
188
171
  .default('path')
189
172
  .describe('Sort by path, name, size, or modified'),
@@ -217,16 +200,8 @@ export const TreeInputSchema = z.strictObject({
217
200
  .optional()
218
201
  .default(DEFAULT_TREE_ENTRIES)
219
202
  .describe(`Max entries. Default: ${DEFAULT_TREE_ENTRIES}`),
220
- includeHidden: z
221
- .boolean()
222
- .optional()
223
- .default(false)
224
- .describe('Include hidden items (starting with .)'),
225
- includeIgnored: z
226
- .boolean()
227
- .optional()
228
- .default(false)
229
- .describe('Include ignored items. Disables .gitignore.'),
203
+ includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
204
+ includeIgnored: defaultFalseBoolean('Include ignored items. Disables .gitignore.'),
230
205
  });
231
206
  export const SearchContentInputSchema = z.strictObject({
232
207
  path: OptionalPathSchema.describe(DESC_PATH_ROOT),
@@ -235,21 +210,9 @@ export const SearchContentInputSchema = z.strictObject({
235
210
  .min(1, 'Pattern required')
236
211
  .max(1000, 'Max 1000 chars')
237
212
  .describe('Literal text to search for by default; treated as RE2 regex when isRegex is true.'),
238
- isRegex: z
239
- .boolean()
240
- .optional()
241
- .default(false)
242
- .describe('Treat pattern as a RE2 regular expression. RE2 does not support lookahead, lookbehind, or backreferences.'),
243
- caseSensitive: z
244
- .boolean()
245
- .optional()
246
- .default(false)
247
- .describe('Case-sensitive matching (default: false — searches are case-insensitive).'),
248
- wholeWord: z
249
- .boolean()
250
- .optional()
251
- .default(false)
252
- .describe('Match whole words only'),
213
+ isRegex: defaultFalseBoolean('Treat pattern as a RE2 regular expression. RE2 does not support lookahead, lookbehind, or backreferences.'),
214
+ caseSensitive: defaultFalseBoolean('Case-sensitive matching (default: false — searches are case-insensitive).'),
215
+ wholeWord: defaultFalseBoolean('Match whole words only'),
253
216
  contextLines: z
254
217
  .number()
255
218
  .int({ error: 'Must be integer' })
@@ -273,16 +236,8 @@ export const SearchContentInputSchema = z.strictObject({
273
236
  .optional()
274
237
  .default('**/*')
275
238
  .describe('Glob for candidate files (e.g. "**/*.ts")'),
276
- includeHidden: z
277
- .boolean()
278
- .optional()
279
- .default(false)
280
- .describe('Include hidden items (starting with .)'),
281
- includeIgnored: z
282
- .boolean()
283
- .optional()
284
- .default(false)
285
- .describe('Include ignored items (node_modules, etc).'),
239
+ includeHidden: defaultFalseBoolean('Include hidden items (starting with .)'),
240
+ includeIgnored: defaultFalseBoolean('Include ignored items (node_modules, etc).'),
286
241
  });
287
242
  export const ReadFileInputSchema = z
288
243
  .strictObject({
@@ -511,16 +466,8 @@ export const EditFileInputSchema = z.strictObject({
511
466
  }))
512
467
  .min(1, 'Min 1 edit required')
513
468
  .describe('List of replacements to apply sequentially. Each edit replaces the first occurrence of oldText.'),
514
- dryRun: z
515
- .boolean()
516
- .optional()
517
- .default(false)
518
- .describe('Preview edits without writing. Check unmatchedEdits in the response to verify all oldText values were found.'),
519
- ignoreWhitespace: z
520
- .boolean()
521
- .optional()
522
- .default(false)
523
- .describe('Ignore leading/trailing whitespace and treat all whitespace sequences as equivalent when matching oldText.'),
469
+ dryRun: defaultFalseBoolean('Preview edits without writing. Check unmatchedEdits in the response to verify all oldText values were found.'),
470
+ ignoreWhitespace: defaultFalseBoolean('Ignore leading/trailing whitespace and treat all whitespace sequences as equivalent when matching oldText.'),
524
471
  });
525
472
  export const EditFileOutputSchema = z.strictObject({
526
473
  ok: z.boolean(),
@@ -562,16 +509,8 @@ export const MoveFileOutputSchema = z.strictObject({
562
509
  });
563
510
  export const DeleteFileInputSchema = z.strictObject({
564
511
  path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
565
- recursive: z
566
- .boolean()
567
- .optional()
568
- .default(false)
569
- .describe('Delete non-empty directories'),
570
- ignoreIfNotExists: z
571
- .boolean()
572
- .optional()
573
- .default(false)
574
- .describe('No error if missing'),
512
+ recursive: defaultFalseBoolean('Delete non-empty directories'),
513
+ ignoreIfNotExists: defaultFalseBoolean('No error if missing'),
575
514
  });
576
515
  export const DeleteFileOutputSchema = z.strictObject({
577
516
  ok: z.boolean(),
@@ -665,16 +604,8 @@ export const SearchAndReplaceInputSchema = z.strictObject({
665
604
  .min(1, 'Search pattern required')
666
605
  .describe('Text to search for. Matched literally by default; treated as RE2 regex when isRegex is true.'),
667
606
  replacement: z.string().describe('Replacement text'),
668
- isRegex: z
669
- .boolean()
670
- .optional()
671
- .default(false)
672
- .describe('Treat searchPattern as a RE2 regular expression. Supports capture group references ($1, $2) in replacement.'),
673
- dryRun: z
674
- .boolean()
675
- .optional()
676
- .default(false)
677
- .describe('Preview matches without writing. Check changedFiles and matches in the response before committing.'),
607
+ isRegex: defaultFalseBoolean('Treat searchPattern as a RE2 regular expression. Supports capture group references ($1, $2) in replacement.'),
608
+ dryRun: defaultFalseBoolean('Preview matches without writing. Check changedFiles and matches in the response before committing.'),
678
609
  includeHidden: z
679
610
  .boolean()
680
611
  .optional()
@@ -66,8 +66,8 @@ export async function createServer(options = {}) {
66
66
  if (serverInstructions) {
67
67
  serverConfig.instructions =
68
68
  'filesystem-mcp: Secure local filesystem MCP server. ' +
69
- 'Always begin with: roots ls/find stat read. Never guess paths. ' +
70
- 'Full reference: read the internal://instructions resource or invoke the get-help prompt.';
69
+ 'Start with: roots -> ls/find -> stat -> read. Never guess paths. ' +
70
+ 'For full guidance, read internal://instructions or run the get-help prompt.';
71
71
  }
72
72
  const server = new McpServer(withDefaultIcons({
73
73
  name: 'filesystem-mcp',