@j0hanz/filesystem-mcp 1.1.2 → 1.2.1

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 (62) hide show
  1. package/README.md +514 -188
  2. package/dist/cli.js +29 -12
  3. package/dist/completions.js +50 -24
  4. package/dist/config.d.ts +4 -2
  5. package/dist/config.js +2 -1
  6. package/dist/index.js +14 -12
  7. package/dist/instructions.md +109 -97
  8. package/dist/lib/constants.js +25 -14
  9. package/dist/lib/errors.js +15 -8
  10. package/dist/lib/file-operations/common.d.ts +4 -0
  11. package/dist/lib/file-operations/common.js +9 -0
  12. package/dist/lib/file-operations/file-info.js +22 -10
  13. package/dist/lib/file-operations/gitignore.js +14 -11
  14. package/dist/lib/file-operations/glob-engine.d.ts +1 -0
  15. package/dist/lib/file-operations/glob-engine.js +46 -33
  16. package/dist/lib/file-operations/list-directory.js +31 -35
  17. package/dist/lib/file-operations/read-multiple-files.js +70 -62
  18. package/dist/lib/file-operations/search-content.js +83 -64
  19. package/dist/lib/file-operations/search-files.js +32 -30
  20. package/dist/lib/file-operations/search-worker.js +22 -12
  21. package/dist/lib/file-operations/tree.js +43 -34
  22. package/dist/lib/fs-helpers.js +61 -124
  23. package/dist/lib/observability.js +29 -28
  24. package/dist/lib/path-format.d.ts +1 -0
  25. package/dist/lib/path-format.js +7 -0
  26. package/dist/lib/path-policy.js +22 -20
  27. package/dist/lib/path-validation.js +13 -7
  28. package/dist/lib/resource-store.d.ts +2 -0
  29. package/dist/lib/resource-store.js +26 -5
  30. package/dist/lib/type-guards.d.ts +1 -0
  31. package/dist/lib/type-guards.js +3 -0
  32. package/dist/prompts.d.ts +1 -5
  33. package/dist/prompts.js +9 -16
  34. package/dist/resources.d.ts +1 -5
  35. package/dist/resources.js +12 -26
  36. package/dist/schemas.d.ts +232 -30
  37. package/dist/schemas.js +52 -90
  38. package/dist/server.js +96 -44
  39. package/dist/tools/apply-patch.js +23 -22
  40. package/dist/tools/calculate-hash.js +41 -43
  41. package/dist/tools/create-directory.js +17 -19
  42. package/dist/tools/delete-file.js +35 -37
  43. package/dist/tools/diff-files.js +15 -19
  44. package/dist/tools/edit-file.js +15 -18
  45. package/dist/tools/list-directory.js +24 -23
  46. package/dist/tools/move-file.js +17 -19
  47. package/dist/tools/read-multiple.js +55 -66
  48. package/dist/tools/read.js +26 -30
  49. package/dist/tools/replace-in-files.js +27 -33
  50. package/dist/tools/roots.js +8 -8
  51. package/dist/tools/search-content.js +73 -72
  52. package/dist/tools/search-files.js +44 -50
  53. package/dist/tools/shared.d.ts +44 -6
  54. package/dist/tools/shared.js +86 -64
  55. package/dist/tools/stat-many.js +44 -66
  56. package/dist/tools/stat.js +10 -37
  57. package/dist/tools/task-support.d.ts +9 -1
  58. package/dist/tools/task-support.js +86 -81
  59. package/dist/tools/tree.js +12 -28
  60. package/dist/tools/write-file.js +17 -19
  61. package/dist/tools.js +23 -18
  62. package/package.json +6 -7
@@ -2,10 +2,11 @@ import * as path from 'node:path';
2
2
  import { platform } from 'node:os';
3
3
  import { SENSITIVE_FILE_ALLOWLIST, SENSITIVE_FILE_DENYLIST, } from './constants.js';
4
4
  import { ErrorCode, McpError } from './errors.js';
5
+ import { toPosixPath } from './path-format.js';
5
6
  const IS_WINDOWS = platform() === 'win32';
6
7
  const WINDOWS_ABSOLUTE_RE = /^[a-z]:\//iu;
7
8
  function normalizePathForMatch(input) {
8
- return path.normalize(input).replace(/\\/gu, '/');
9
+ return toPosixPath(path.normalize(input));
9
10
  }
10
11
  function normalizeForMatch(input) {
11
12
  const normalized = normalizePathForMatch(input);
@@ -23,21 +24,32 @@ function compilePatternGlobs(normalizedPattern) {
23
24
  return [...globs];
24
25
  }
25
26
  function compilePatterns(patterns) {
26
- const unique = new Set(patterns
27
- .map((pattern) => pattern.trim())
28
- .filter((pattern) => pattern.length > 0));
29
- return [...unique].map((pattern) => {
27
+ const unique = new Set();
28
+ for (const pattern of patterns) {
29
+ const trimmed = pattern.trim();
30
+ if (trimmed.length > 0) {
31
+ unique.add(trimmed);
32
+ }
33
+ }
34
+ const compiled = [];
35
+ for (const pattern of unique) {
30
36
  const normalized = normalizeForMatch(pattern);
31
37
  const matchesPath = normalized.includes('/');
32
- return {
38
+ compiled.push({
33
39
  raw: normalized,
34
40
  globs: matchesPath ? compilePatternGlobs(normalized) : [normalized],
35
41
  matchesPath,
36
- };
37
- });
42
+ });
43
+ }
44
+ return compiled;
38
45
  }
39
46
  const DENY_PATTERNS = compilePatterns(SENSITIVE_FILE_DENYLIST);
40
47
  const ALLOW_PATTERNS = compilePatterns(SENSITIVE_FILE_ALLOWLIST);
48
+ function uniquePair(primary, secondary) {
49
+ if (!secondary || secondary === primary)
50
+ return [primary];
51
+ return [primary, secondary];
52
+ }
41
53
  function matchesAny(patterns, pathCandidates, nameCandidates) {
42
54
  for (const pattern of patterns) {
43
55
  const candidates = pattern.matchesPath ? pathCandidates : nameCandidates;
@@ -57,18 +69,8 @@ export function isSensitivePath(requestedPath, resolvedPath) {
57
69
  const normalizedResolved = resolvedPath
58
70
  ? normalizeForMatch(resolvedPath)
59
71
  : undefined;
60
- const pathCandidates = [
61
- normalizedRequested,
62
- ...(normalizedResolved && normalizedResolved !== normalizedRequested
63
- ? [normalizedResolved]
64
- : []),
65
- ];
66
- const nameCandidates = [
67
- path.posix.basename(normalizedRequested),
68
- ...(normalizedResolved && normalizedResolved !== normalizedRequested
69
- ? [path.posix.basename(normalizedResolved)]
70
- : []),
71
- ];
72
+ const pathCandidates = uniquePair(normalizedRequested, normalizedResolved);
73
+ const nameCandidates = uniquePair(path.posix.basename(normalizedRequested), normalizedResolved ? path.posix.basename(normalizedResolved) : undefined);
72
74
  if (matchesAny(ALLOW_PATTERNS, pathCandidates, nameCandidates)) {
73
75
  return false;
74
76
  }
@@ -77,6 +77,9 @@ function stripTrailingSeparator(normalized) {
77
77
  ? normalized.slice(0, -1)
78
78
  : normalized;
79
79
  }
80
+ function isFileSystemRootPath(normalized, root) {
81
+ return isSamePath(normalized, root);
82
+ }
80
83
  function normalizeAllowedDirectory(dir) {
81
84
  const trimmed = dir.trim();
82
85
  if (trimmed.length === 0)
@@ -84,16 +87,19 @@ function normalizeAllowedDirectory(dir) {
84
87
  const normalized = normalizePath(trimmed);
85
88
  const { root } = path.parse(normalized);
86
89
  // Keep filesystem roots as-is ("/", "c:\\", "\\\\server\\share\\").
87
- if (normalized === root ||
88
- normalizeForComparison(normalized) === normalizeForComparison(root)) {
90
+ if (isFileSystemRootPath(normalized, root)) {
89
91
  return root;
90
92
  }
91
93
  return stripTrailingSeparator(normalized);
92
94
  }
93
95
  function normalizeAllowedDirectories(dirs) {
94
- const normalized = dirs
95
- .map(normalizeAllowedDirectory)
96
- .filter((dir) => dir.length > 0);
96
+ const normalized = [];
97
+ for (const dir of dirs) {
98
+ const entry = normalizeAllowedDirectory(dir);
99
+ if (entry.length > 0) {
100
+ normalized.push(entry);
101
+ }
102
+ }
97
103
  // Preserve first-seen order while deduping.
98
104
  return dedupePreserveOrder(normalized);
99
105
  }
@@ -109,8 +115,8 @@ export function getAllowedDirectories() {
109
115
  }
110
116
  function getAllowedDirectoriesForRelativeResolution() {
111
117
  return allowedDirectoriesPrimary.length > 0
112
- ? [...allowedDirectoriesPrimary]
113
- : [...allowedDirectoriesExpanded];
118
+ ? allowedDirectoriesPrimary
119
+ : allowedDirectoriesExpanded;
114
120
  }
115
121
  function isPathInsideDirectory(normalizedDirectory, normalizedCandidate) {
116
122
  const root = normalizeForComparison(normalizedDirectory);
@@ -4,6 +4,8 @@ export interface TextResourceEntry {
4
4
  mimeType: string;
5
5
  text: string;
6
6
  hash: string;
7
+ size: number;
8
+ storedAt: string;
7
9
  }
8
10
  export interface ResourceStore {
9
11
  putText(params: {
@@ -11,12 +11,24 @@ function estimateBytes(text) {
11
11
  function computeSha256(text) {
12
12
  return hash('sha256', text, 'hex');
13
13
  }
14
+ function createTextEntry(params) {
15
+ return {
16
+ uri: params.uri,
17
+ name: params.name,
18
+ mimeType: params.mimeType,
19
+ text: params.text,
20
+ hash: computeSha256(params.text),
21
+ size: estimateBytes(params.text),
22
+ storedAt: new Date().toISOString(),
23
+ };
24
+ }
14
25
  export function createInMemoryResourceStore(options = {}) {
15
26
  const resolved = {
16
27
  ...DEFAULT_RESOURCE_STORE_OPTIONS,
17
28
  ...options,
18
29
  };
19
30
  const byUri = new Map();
31
+ const byHashIndex = new Map(); // sha256hex → uri
20
32
  let totalBytes = 0;
21
33
  function evictOldest() {
22
34
  const first = byUri.keys().next();
@@ -26,8 +38,9 @@ export function createInMemoryResourceStore(options = {}) {
26
38
  const existing = byUri.get(uri);
27
39
  if (!existing)
28
40
  return;
29
- totalBytes -= estimateBytes(existing.text);
41
+ totalBytes -= existing.size;
30
42
  byUri.delete(uri);
43
+ byHashIndex.delete(existing.hash);
31
44
  }
32
45
  function enforceLimits() {
33
46
  while (byUri.size > resolved.maxEntries)
@@ -44,17 +57,24 @@ export function createInMemoryResourceStore(options = {}) {
44
57
  if (entryBytes > resolved.maxEntryBytes) {
45
58
  throw new McpError(ErrorCode.E_TOO_LARGE, `Resource too large to cache (${entryBytes} bytes)`);
46
59
  }
60
+ const contentHash = computeSha256(params.text);
61
+ const existingUri = byHashIndex.get(contentHash);
62
+ if (existingUri !== undefined) {
63
+ const cached = byUri.get(existingUri);
64
+ if (cached !== undefined) {
65
+ return cached;
66
+ }
67
+ }
47
68
  const id = randomUUID();
48
69
  const uri = `filesystem-mcp://result/${id}`;
49
- const hash = computeSha256(params.text);
50
- const entry = {
70
+ const entry = createTextEntry({
51
71
  uri,
52
72
  name: params.name,
53
73
  mimeType,
54
74
  text: params.text,
55
- hash,
56
- };
75
+ });
57
76
  byUri.set(uri, entry);
77
+ byHashIndex.set(contentHash, uri);
58
78
  totalBytes += entryBytes;
59
79
  enforceLimits();
60
80
  if (!byUri.has(uri)) {
@@ -71,6 +91,7 @@ export function createInMemoryResourceStore(options = {}) {
71
91
  }
72
92
  function clear() {
73
93
  byUri.clear();
94
+ byHashIndex.clear();
74
95
  totalBytes = 0;
75
96
  }
76
97
  return { putText, getText, clear };
@@ -0,0 +1 @@
1
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1,3 @@
1
+ export function isRecord(value) {
2
+ return value !== null && typeof value === 'object';
3
+ }
package/dist/prompts.d.ts CHANGED
@@ -1,7 +1,3 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- interface IconInfo {
3
- src: string;
4
- mimeType: string;
5
- }
2
+ import { type IconInfo } from './tools/shared.js';
6
3
  export declare function registerGetHelpPrompt(server: McpServer, instructions: string, iconInfo?: IconInfo): void;
7
- export {};
package/dist/prompts.js CHANGED
@@ -1,20 +1,13 @@
1
+ import { withDefaultIcons } from './tools/shared.js';
2
+ const HELP_PROMPT_NAME = 'get-help';
3
+ const HELP_PROMPT_TITLE = 'Get Help';
4
+ const HELP_PROMPT_DESCRIPTION = 'Return the filesystem-mcp usage instructions.';
1
5
  export function registerGetHelpPrompt(server, instructions, iconInfo) {
2
- const description = 'Return the filesystem-mcp usage instructions.';
3
- server.registerPrompt('get-help', {
4
- title: 'Get Help',
5
- description,
6
- ...(iconInfo
7
- ? {
8
- icons: [
9
- {
10
- src: iconInfo.src,
11
- mimeType: iconInfo.mimeType,
12
- },
13
- ],
14
- }
15
- : {}),
16
- }, () => ({
17
- description,
6
+ server.registerPrompt(HELP_PROMPT_NAME, withDefaultIcons({
7
+ title: HELP_PROMPT_TITLE,
8
+ description: HELP_PROMPT_DESCRIPTION,
9
+ }, iconInfo), () => ({
10
+ description: HELP_PROMPT_DESCRIPTION,
18
11
  messages: [
19
12
  {
20
13
  role: 'user',
@@ -1,9 +1,5 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { ResourceStore } from './lib/resource-store.js';
3
- interface IconInfo {
4
- src: string;
5
- mimeType: string;
6
- }
3
+ import { type IconInfo } from './tools/shared.js';
7
4
  export declare function registerInstructionResource(server: McpServer, instructions: string, iconInfo?: IconInfo): void;
8
5
  export declare function registerResultResources(server: McpServer, store: ResourceStore, iconInfo?: IconInfo): void;
9
- export {};
package/dist/resources.js CHANGED
@@ -1,28 +1,24 @@
1
1
  import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { ErrorCode, McpError } from './lib/errors.js';
3
+ import { withDefaultIcons } from './tools/shared.js';
3
4
  const RESULT_TEMPLATE = new ResourceTemplate('filesystem-mcp://result/{id}', {
4
5
  list: undefined,
5
6
  });
7
+ const INSTRUCTIONS_RESOURCE_NAME = 'filesystem-mcp-instructions';
8
+ const INSTRUCTIONS_RESOURCE_URI = 'internal://instructions';
9
+ const INSTRUCTIONS_RESOURCE_DESCRIPTION = 'Guidance for using the filesystem-mcp MCP tools effectively.';
10
+ const RESULT_RESOURCE_NAME = 'filesystem-mcp-result';
11
+ const RESULT_RESOURCE_DESCRIPTION = 'Ephemeral cached tool output exposed as an MCP resource. Not guaranteed to be listed via resources/list.';
6
12
  export function registerInstructionResource(server, instructions, iconInfo) {
7
- server.registerResource('filesystem-mcp-instructions', 'internal://instructions', {
13
+ server.registerResource(INSTRUCTIONS_RESOURCE_NAME, INSTRUCTIONS_RESOURCE_URI, withDefaultIcons({
8
14
  title: 'Server Instructions',
9
- description: 'Guidance for using the filesystem-mcp MCP tools effectively.',
15
+ description: INSTRUCTIONS_RESOURCE_DESCRIPTION,
10
16
  mimeType: 'text/markdown',
11
17
  annotations: {
12
18
  audience: ['assistant'],
13
19
  priority: 0.8,
14
20
  },
15
- ...(iconInfo
16
- ? {
17
- icons: [
18
- {
19
- src: iconInfo.src,
20
- mimeType: iconInfo.mimeType,
21
- },
22
- ],
23
- }
24
- : {}),
25
- }, (uri) => ({
21
+ }, iconInfo), (uri) => ({
26
22
  contents: [
27
23
  {
28
24
  uri: uri.href,
@@ -33,25 +29,15 @@ export function registerInstructionResource(server, instructions, iconInfo) {
33
29
  }));
34
30
  }
35
31
  export function registerResultResources(server, store, iconInfo) {
36
- server.registerResource('filesystem-mcp-result', RESULT_TEMPLATE, {
32
+ server.registerResource(RESULT_RESOURCE_NAME, RESULT_TEMPLATE, withDefaultIcons({
37
33
  title: 'Cached Tool Result',
38
- description: 'Ephemeral cached tool output exposed as an MCP resource. Not guaranteed to be listed via resources/list.',
34
+ description: RESULT_RESOURCE_DESCRIPTION,
39
35
  mimeType: 'text/plain',
40
36
  annotations: {
41
37
  audience: ['assistant'],
42
38
  priority: 0.3,
43
39
  },
44
- ...(iconInfo
45
- ? {
46
- icons: [
47
- {
48
- src: iconInfo.src,
49
- mimeType: iconInfo.mimeType,
50
- },
51
- ],
52
- }
53
- : {}),
54
- }, (uri, variables) => {
40
+ }, iconInfo), (uri, variables) => {
55
41
  const { id } = variables;
56
42
  if (typeof id !== 'string' || id.length === 0) {
57
43
  throw new McpError(ErrorCode.E_INVALID_INPUT, 'Missing resource id');