@j0hanz/filesystem-mcp 1.13.2 → 1.14.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.
Files changed (43) hide show
  1. package/README.md +117 -100
  2. package/dist/config.d.ts +0 -1
  3. package/dist/lib/errors.js +5 -2
  4. package/dist/lib/file-operations/metadata.js +9 -3
  5. package/dist/lib/file-operations/search.d.ts +0 -1
  6. package/dist/lib/file-operations/search.js +5 -12
  7. package/dist/lib/fs-helpers.js +10 -11
  8. package/dist/lib/globs.d.ts +2 -0
  9. package/dist/lib/globs.js +19 -0
  10. package/dist/lib/zod-codecs.d.ts +2 -0
  11. package/dist/lib/zod-codecs.js +18 -0
  12. package/dist/pkg-info.d.ts +1 -0
  13. package/dist/pkg-info.js +2 -2
  14. package/dist/prompts.js +3 -3
  15. package/dist/resources/generated-instructions.js +3 -12
  16. package/dist/resources/tool-catalog.js +10 -41
  17. package/dist/resources/tool-info.d.ts +0 -1
  18. package/dist/resources/tool-info.js +11 -39
  19. package/dist/resources/workflows.js +8 -1
  20. package/dist/schemas.d.ts +179 -459
  21. package/dist/schemas.js +156 -165
  22. package/dist/server/roots-manager.js +1 -1
  23. package/dist/tools/apply-patch.js +19 -8
  24. package/dist/tools/calculate-hash.js +3 -5
  25. package/dist/tools/create-directory.js +1 -1
  26. package/dist/tools/delete-file.js +2 -4
  27. package/dist/tools/diff-files.js +1 -3
  28. package/dist/tools/edit-file.js +5 -2
  29. package/dist/tools/list-directory.js +10 -15
  30. package/dist/tools/move-file.js +14 -26
  31. package/dist/tools/read-multiple.js +12 -7
  32. package/dist/tools/read.js +1 -2
  33. package/dist/tools/replace-in-files.js +58 -94
  34. package/dist/tools/roots.js +2 -6
  35. package/dist/tools/search-content.js +150 -186
  36. package/dist/tools/search-files.js +5 -9
  37. package/dist/tools/shared.d.ts +7 -0
  38. package/dist/tools/shared.js +38 -11
  39. package/dist/tools/stat-many.js +6 -4
  40. package/dist/tools/stat.js +2 -2
  41. package/dist/tools/tree.js +1 -1
  42. package/dist/tools/write-file.js +1 -5
  43. package/package.json +2 -1
@@ -287,13 +287,11 @@ function validateReadOptions(options) {
287
287
  if (hasTail && (hasHead || hasStart || hasEnd)) {
288
288
  throw new McpError(ErrorCode.E_INVALID_INPUT, 'tail cannot be used together with head/startLine/endLine');
289
289
  }
290
- if (hasEnd && !hasStart) {
291
- throw new McpError(ErrorCode.E_INVALID_INPUT, 'endLine requires startLine');
292
- }
293
- if (options.startLine !== undefined &&
294
- options.endLine !== undefined &&
295
- options.endLine < options.startLine) {
296
- throw new McpError(ErrorCode.E_INVALID_INPUT, 'endLine must be greater than or equal to startLine');
290
+ {
291
+ const effectiveStart = options.startLine ?? 1;
292
+ if (options.endLine !== undefined && options.endLine < effectiveStart) {
293
+ throw new McpError(ErrorCode.E_INVALID_INPUT, 'endLine must be greater than or equal to startLine (default: 1)');
294
+ }
297
295
  }
298
296
  }
299
297
  function normalizeOptions(options) {
@@ -309,12 +307,13 @@ function normalizeOptions(options) {
309
307
  if (options.tail !== undefined) {
310
308
  normalized.tail = options.tail;
311
309
  }
312
- if (options.startLine !== undefined) {
313
- normalized.startLine = options.startLine;
314
- }
315
310
  if (options.endLine !== undefined) {
311
+ normalized.startLine = options.startLine ?? 1;
316
312
  normalized.endLine = options.endLine;
317
313
  }
314
+ else if (options.startLine !== undefined) {
315
+ normalized.startLine = options.startLine;
316
+ }
318
317
  if (options.signal) {
319
318
  normalized.signal = options.signal;
320
319
  }
@@ -340,7 +339,7 @@ function resolveReadMode(options) {
340
339
  return 'head';
341
340
  if (options.tail !== undefined)
342
341
  return 'tail';
343
- if (options.startLine !== undefined)
342
+ if (options.startLine !== undefined || options.endLine !== undefined)
344
343
  return 'range';
345
344
  return 'full';
346
345
  }
@@ -0,0 +1,2 @@
1
+ export declare function isSafeGlobPattern(value: string): boolean;
2
+ export declare function assertSafeGlobPattern(value: string, message?: string): void;
@@ -0,0 +1,19 @@
1
+ import { ErrorCode, McpError } from './errors.js';
2
+ const ABSOLUTE_GLOB_RE = /^([/\\]|[A-Za-z]:[/\\]|\\\\)/u;
3
+ const PARENT_SEGMENT_RE = /[\\/]\.\.(?:[/\\]|$)/u;
4
+ export function isSafeGlobPattern(value) {
5
+ if (value.length === 0)
6
+ return false;
7
+ if (value.includes('**/**/**'))
8
+ return false;
9
+ if (ABSOLUTE_GLOB_RE.test(value))
10
+ return false;
11
+ if (value.startsWith('..') || PARENT_SEGMENT_RE.test(value))
12
+ return false;
13
+ return true;
14
+ }
15
+ export function assertSafeGlobPattern(value, message = 'Invalid glob or unsafe path (absolute/.. forbidden)') {
16
+ if (!isSafeGlobPattern(value)) {
17
+ throw new McpError(ErrorCode.E_INVALID_PATTERN, message);
18
+ }
19
+ }
@@ -0,0 +1,2 @@
1
+ import { z } from 'zod';
2
+ export declare function createBase64JsonCodec<Schema extends z.ZodType>(schema: Schema): z.ZodCodec<z.ZodString, Schema>;
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ export function createBase64JsonCodec(schema) {
3
+ return z.codec(z.string(), schema, {
4
+ decode: (value) => {
5
+ let parsed;
6
+ try {
7
+ parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf-8'));
8
+ }
9
+ catch (error) {
10
+ throw new Error('Invalid base64url-encoded JSON payload.', {
11
+ cause: error,
12
+ });
13
+ }
14
+ return parsed;
15
+ },
16
+ encode: (value) => Buffer.from(JSON.stringify(value)).toString('base64url'),
17
+ });
18
+ }
@@ -1,4 +1,5 @@
1
1
  export declare const pkgInfo: {
2
+ [x: string]: unknown;
2
3
  name: string;
3
4
  version: string;
4
5
  description?: string | undefined;
package/dist/pkg-info.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import packageJsonRaw from '../package.json' with { type: 'json' };
3
- const PkgInfoSchema = z.object({
3
+ const PkgInfoSchema = z.looseObject({
4
4
  name: z.string(),
5
5
  version: z.string(),
6
6
  description: z.string().optional(),
7
- homepage: z.string().optional(),
7
+ homepage: z.url().optional(),
8
8
  });
9
9
  export const pkgInfo = PkgInfoSchema.parse(packageJsonRaw);
package/dist/prompts.js CHANGED
@@ -1,5 +1,5 @@
1
+ import { ErrorCode as SdkErrorCode, McpError as SdkMcpError, } from '@modelcontextprotocol/sdk/types.js';
1
2
  import { z } from 'zod';
2
- import { ErrorCode, McpError } from './lib/errors.js';
3
3
  import { buildToolInfo, getSortedToolContracts, } from './resources/tool-info.js';
4
4
  import { withDefaultIcons } from './tools/shared.js';
5
5
  const HELP_PROMPT_NAME = 'get-help';
@@ -123,11 +123,11 @@ export function registerGetToolHelpPrompt(server, iconInfo) {
123
123
  }, ({ name }) => {
124
124
  const toolName = findKnownToolName(name);
125
125
  if (!toolName) {
126
- throw new McpError(ErrorCode.E_INVALID_INPUT, `Unknown tool: ${name}`);
126
+ throw new SdkMcpError(SdkErrorCode.InvalidParams, `Unknown tool: ${name}`);
127
127
  }
128
128
  const toolInfo = buildToolInfo(toolName);
129
129
  if (!toolInfo) {
130
- throw new McpError(ErrorCode.E_INVALID_INPUT, `Unknown tool: ${toolName}`);
130
+ throw new SdkMcpError(SdkErrorCode.InvalidParams, `Unknown tool: ${toolName}`);
131
131
  }
132
132
  return {
133
133
  description: GET_TOOL_HELP_PROMPT_DESCRIPTION,
@@ -1,9 +1,6 @@
1
1
  import { buildToolCatalogDetailsOnly } from './tool-catalog.js';
2
- import { buildCoreContextPack, formatToolNameList, getSharedConstraints, getTaskCapableToolNames, getTaskToolNamesBySupport, getToolContracts, pickAvailableToolNames, } from './tool-info.js';
2
+ import { buildCoreContextPack, formatToolNameList, getSharedConstraints, getToolContracts, pickAvailableToolNames, } from './tool-info.js';
3
3
  import { buildWorkflowGuide } from './workflows.js';
4
- function formatTaskModeLine(label, names) {
5
- return `${label}: ${names.length > 0 ? formatToolNameList(names) : 'none'}.`;
6
- }
7
4
  function buildToolsOverview() {
8
5
  const rows = [
9
6
  ['Navigate', pickAvailableToolNames(['roots', 'ls', 'tree', 'find'])],
@@ -31,9 +28,6 @@ function buildToolsOverview() {
31
28
  .join('\n');
32
29
  }
33
30
  function buildInstructionsHeader() {
34
- const taskCapable = formatToolNameList(getTaskCapableToolNames());
35
- const optionalTaskTools = getTaskToolNamesBySupport('optional');
36
- const requiredTaskTools = getTaskToolNamesBySupport('required');
37
31
  return `<role>
38
32
  Filesystem agent. Scope: allowed roots only. Discover paths before acting — never guess.
39
33
  </role>
@@ -54,12 +48,9 @@ ${buildToolsOverview()}
54
48
  </resources>
55
49
 
56
50
  <task_protocol>
57
- Task execution: Call task-capable tools inline by default; add \`task\` only when durable polling or deferred results are needed.
58
- Task results: When a task is requested, poll via \`tasks/get\`, then retrieve the final payload via \`tasks/result\`.
51
+ Task execution: Check \`execution.taskSupport\` per tool \`forbidden\` (default): never send \`task\`; \`optional\`: send \`task\` only when durable polling or deferred results are needed; \`required\`: always send \`task\`.
52
+ Task results: Poll via \`tasks/get\`, then retrieve the final payload via \`tasks/result\`.
59
53
  Progress: Pass \`_meta.progressToken\` in \`tools/call\` to receive \`notifications/progress\`.
60
- Task-capable: ${taskCapable || 'none'}.
61
- ${formatTaskModeLine('Optional task mode', optionalTaskTools)}
62
- ${formatTaskModeLine('Required task mode', requiredTaskTools)}
63
54
  </task_protocol>
64
55
  `;
65
56
  }
@@ -15,27 +15,13 @@ function buildCrossToolDataFlow() {
15
15
  }
16
16
  function buildCatalogGuide() {
17
17
  const taskCapable = getTaskCapableToolNames();
18
- return (`<tool_selection_guide>
18
+ return `<tool_selection_guide>
19
19
  ## Primitive Routing
20
20
 
21
- - ` +
22
- '`tools`' +
23
- `: model-controlled operations that inspect or mutate the allowed filesystem.
24
- - ` +
25
- '`resources`' +
26
- `: application-driven context such as ` +
27
- '`internal://instructions`' +
28
- `, ` +
29
- '`internal://tool-info/{name}`' +
30
- `, and cached ` +
31
- '`filesystem-mcp://result/{id}`' +
32
- ` output.
33
- - ` +
34
- '`prompts`' +
35
- `: user-controlled workflow templates for help, comparison, and guided inspection.
36
- - ` +
37
- '`completion`' +
38
- `: argument suggestions for prompts and resource templates; not a discovery mechanism.
21
+ - \`tools\`: model-controlled operations that inspect or mutate the allowed filesystem.
22
+ - \`resources\`: application-driven context such as \`internal://instructions\`, \`internal://tool-info/{name}\`, and cached \`filesystem-mcp://result/{id}\` output.
23
+ - \`prompts\`: user-controlled workflow templates for help, comparison, and guided inspection.
24
+ - \`completion\`: argument suggestions for prompts and resource templates; not a discovery mechanism.
39
25
 
40
26
  ## Cross-Tool Data Flow
41
27
 
@@ -45,21 +31,10 @@ ${buildCrossToolDataFlow()}
45
31
 
46
32
  ## Result Contract
47
33
 
48
- - Successful tools return ` +
49
- '`content`' +
50
- ` and usually ` +
51
- '`structuredContent`' +
52
- `.
53
- - Tool/business failures return ` +
54
- '`isError: true`' +
55
- ` inside the tool result, not a JSON-RPC protocol error.
56
- - When a tool returns ` +
57
- '`resourceUri`' +
58
- ` or a ` +
59
- '`resource_link`' +
60
- `, follow it with ` +
61
- '`resources/read`' +
62
- ` immediately.
34
+ - Successful tools return \`content\` and \`structuredContent\` (when \`outputSchema\` is declared).
35
+ - When \`isError: true\`, \`structuredContent\` is omitted — parse the \`content\` text instead.
36
+ - Tool/business failures return \`isError: true\` inside the tool result, not a JSON-RPC protocol error.
37
+ - When a tool returns \`resourceUri\` or a \`resource_link\`, follow it with \`resources/read\` immediately.
63
38
 
64
39
  ## Task Mode Routing
65
40
 
@@ -75,12 +50,6 @@ ${buildCrossToolDataFlow()}
75
50
 
76
51
  ## Write Strategy
77
52
 
78
- - \`edit\`: precise first-occurrence replacements.
79
- - \`write\`: create files or overwrite full contents.
80
- - \`search_and_replace\`: bulk multi-file replacements.
81
-
82
- ### edit vs write vs search_and_replace Decision
83
-
84
53
  1. **Single file, targeted change?** -> \`edit\` (match exact text, replace first occurrence)
85
54
  2. **Single file, full rewrite?** -> \`write\` (overwrite entire content)
86
55
  3. **Multiple files, same change?** -> \`search_and_replace\` (glob + pattern across files)
@@ -93,7 +62,7 @@ ${buildCrossToolDataFlow()}
93
62
  - \`apply_patch\` accepts unified diffs - single-file or multi-file.
94
63
  - Multi-file: \`path\` is base directory; each file is best-effort with per-file \`results[]\`.
95
64
  </tool_selection_guide>
96
- `);
65
+ `;
97
66
  }
98
67
  export function buildToolCatalog() {
99
68
  return `${buildCoreContextPack()}\n\n${buildCatalogGuide()}`;
@@ -4,7 +4,6 @@ export declare function getSortedToolContracts(): ToolContract[];
4
4
  export declare function pickAvailableToolNames(names: readonly string[]): string[];
5
5
  export declare function formatToolNameList(names: readonly string[]): string;
6
6
  export declare function getTaskCapableToolNames(): string[];
7
- export declare function getTaskToolNamesBySupport(taskSupport: Extract<ToolContract['taskSupport'], 'optional' | 'required'>): string[];
8
7
  export declare function buildCoreContextPack(): string;
9
8
  export declare function getSharedConstraints(): string[];
10
9
  export declare function buildToolInfo(name: string): string | undefined;
@@ -55,11 +55,6 @@ export function getTaskCapableToolNames() {
55
55
  contract.taskSupport === 'required')
56
56
  .map((contract) => contract.name);
57
57
  }
58
- export function getTaskToolNamesBySupport(taskSupport) {
59
- return getSortedToolContracts()
60
- .filter((contract) => contract.taskSupport === taskSupport)
61
- .map((contract) => contract.name);
62
- }
63
58
  export function buildCoreContextPack() {
64
59
  const rows = getSortedToolContracts().map((contract) => {
65
60
  const e = ENTRIES[contract.name];
@@ -88,9 +83,6 @@ function formatTaskSupportLabel(taskSupport) {
88
83
  return 'forbidden';
89
84
  }
90
85
  }
91
- function formatAnnotationValue(value) {
92
- return value ? 'true' : 'false';
93
- }
94
86
  function toJsonSchemaObject(schema, io = 'output') {
95
87
  return z.toJSONSchema(schema, { io });
96
88
  }
@@ -158,29 +150,20 @@ function buildSchemaFieldLines(label, schema) {
158
150
  }), `</${label}>`);
159
151
  return lines;
160
152
  }
161
- function buildProtocolNotes(contract) {
162
- const notes = [
163
- '- Protocol failures use JSON-RPC `error`; execution failures use tool result `isError: true`.',
164
- ];
165
- if (contract.outputSchema) {
166
- notes.push('- Successful responses include `structuredContent` that must match the declared output schema.');
167
- }
168
- if (contract.taskSupport === 'optional') {
169
- notes.push('- Supports inline execution by default and task mode when durable polling or deferred results are needed.');
170
- }
171
- if (contract.taskSupport === 'required') {
172
- notes.push('- Must run in task mode; callers should poll `tasks/get` and fetch the payload via `tasks/result`.');
173
- }
174
- if (contract.taskSupport === 'forbidden') {
175
- notes.push('- Runs inline only; task augmentation is not supported for this tool.');
176
- }
177
- return notes;
178
- }
179
153
  export function buildToolInfo(name) {
180
154
  const contract = CONTRACTS_BY_NAME.get(name);
181
155
  const entry = ENTRIES[name];
182
156
  if (!entry || !contract)
183
157
  return undefined;
158
+ const annotationLines = [];
159
+ if (contract.annotations?.readOnlyHint)
160
+ annotationLines.push('- readOnlyHint: true');
161
+ if (contract.annotations?.idempotentHint)
162
+ annotationLines.push('- idempotentHint: true');
163
+ if (contract.annotations?.destructiveHint)
164
+ annotationLines.push('- destructiveHint: true');
165
+ if (contract.annotations?.openWorldHint)
166
+ annotationLines.push('- openWorldHint: true');
184
167
  const lines = [
185
168
  `<tool_info name="${entry.name}">`,
186
169
  `## ${entry.name}`,
@@ -192,23 +175,12 @@ export function buildToolInfo(name) {
192
175
  `- taskSupport: ${formatTaskSupportLabel(contract.taskSupport)}`,
193
176
  '</execution>',
194
177
  '',
195
- '<annotations>',
196
- `- readOnlyHint: ${formatAnnotationValue(contract.annotations?.readOnlyHint)}`,
197
- `- idempotentHint: ${formatAnnotationValue(contract.annotations?.idempotentHint)}`,
198
- `- destructiveHint: ${formatAnnotationValue(contract.annotations?.destructiveHint)}`,
199
- `- openWorldHint: ${formatAnnotationValue(contract.annotations?.openWorldHint)}`,
200
- '</annotations>',
201
- '',
202
178
  ...buildSchemaFieldLines('input_fields', contract.inputSchema),
203
179
  '',
204
180
  ...buildSchemaFieldLines('output_fields', contract.outputSchema),
205
- '',
206
- '<protocol_notes>',
207
- ...buildProtocolNotes(contract),
208
- '</protocol_notes>',
209
181
  ];
210
- if (entry.annotations && entry.annotations.length > 0) {
211
- lines.push('', `<quick_hints>${entry.annotations.join(' ')}</quick_hints>`);
182
+ if (annotationLines.length > 0) {
183
+ lines.push('', '<annotations>', ...annotationLines, '</annotations>');
212
184
  }
213
185
  if (entry.nuances && entry.nuances.length > 0) {
214
186
  lines.push('', '<nuances>');
@@ -10,7 +10,14 @@ export function buildWorkflowGuide() {
10
10
  'read_many',
11
11
  ]));
12
12
  const searchTools = formatToolNameList(pickAvailableToolNames(['find', 'grep', 'read']));
13
- const editTools = formatToolNameList(pickAvailableToolNames(['edit', 'search_and_replace', 'mv', 'rm', 'mkdir']));
13
+ const editTools = formatToolNameList(pickAvailableToolNames([
14
+ 'edit',
15
+ 'write',
16
+ 'search_and_replace',
17
+ 'mv',
18
+ 'rm',
19
+ 'mkdir',
20
+ ]));
14
21
  return `<workflows>
15
22
  ### A: EXPLORE — directory layout or file content
16
23
  1. ${exploreTools}.