@j0hanz/fs-context-mcp 2.2.0 → 2.3.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 (131) hide show
  1. package/README.md +171 -417
  2. package/dist/assets/logo.svg +3766 -0
  3. package/dist/config.d.ts +0 -1
  4. package/dist/config.js +0 -1
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.js +0 -1
  7. package/dist/lib/constants.d.ts +0 -1
  8. package/dist/lib/constants.js +0 -1
  9. package/dist/lib/errors.d.ts +0 -1
  10. package/dist/lib/errors.js +0 -1
  11. package/dist/lib/file-operations/file-info.d.ts +0 -1
  12. package/dist/lib/file-operations/file-info.js +0 -1
  13. package/dist/lib/file-operations/gitignore.d.ts +0 -1
  14. package/dist/lib/file-operations/gitignore.js +0 -1
  15. package/dist/lib/file-operations/glob-engine.d.ts +0 -1
  16. package/dist/lib/file-operations/glob-engine.js +101 -84
  17. package/dist/lib/file-operations/list-directory.d.ts +0 -1
  18. package/dist/lib/file-operations/list-directory.js +91 -62
  19. package/dist/lib/file-operations/read-multiple-files.d.ts +0 -1
  20. package/dist/lib/file-operations/read-multiple-files.js +0 -1
  21. package/dist/lib/file-operations/search-content.d.ts +0 -1
  22. package/dist/lib/file-operations/search-content.js +68 -21
  23. package/dist/lib/file-operations/search-files.d.ts +0 -1
  24. package/dist/lib/file-operations/search-files.js +21 -11
  25. package/dist/lib/file-operations/search-worker.d.ts +0 -1
  26. package/dist/lib/file-operations/search-worker.js +0 -1
  27. package/dist/lib/file-operations/tree.d.ts +0 -1
  28. package/dist/lib/file-operations/tree.js +18 -8
  29. package/dist/lib/fs-helpers.d.ts +0 -1
  30. package/dist/lib/fs-helpers.js +19 -25
  31. package/dist/lib/observability.d.ts +0 -1
  32. package/dist/lib/observability.js +49 -43
  33. package/dist/lib/path-policy.d.ts +0 -1
  34. package/dist/lib/path-policy.js +0 -1
  35. package/dist/lib/path-validation.d.ts +0 -1
  36. package/dist/lib/path-validation.js +54 -38
  37. package/dist/lib/resource-store.d.ts +0 -1
  38. package/dist/lib/resource-store.js +0 -1
  39. package/dist/resources.d.ts +2 -3
  40. package/dist/resources.js +16 -3
  41. package/dist/schemas.d.ts +0 -1
  42. package/dist/schemas.js +35 -51
  43. package/dist/server.d.ts +0 -1
  44. package/dist/server.js +42 -11
  45. package/dist/tools/list-directory.d.ts +0 -1
  46. package/dist/tools/list-directory.js +14 -2
  47. package/dist/tools/read-multiple.d.ts +0 -1
  48. package/dist/tools/read-multiple.js +14 -2
  49. package/dist/tools/read.d.ts +0 -1
  50. package/dist/tools/read.js +14 -2
  51. package/dist/tools/roots.d.ts +0 -1
  52. package/dist/tools/roots.js +14 -2
  53. package/dist/tools/search-content.d.ts +0 -1
  54. package/dist/tools/search-content.js +29 -14
  55. package/dist/tools/search-files.d.ts +0 -1
  56. package/dist/tools/search-files.js +14 -2
  57. package/dist/tools/shared.d.ts +7 -1
  58. package/dist/tools/shared.js +7 -4
  59. package/dist/tools/stat-many.d.ts +0 -1
  60. package/dist/tools/stat-many.js +14 -2
  61. package/dist/tools/stat.d.ts +0 -1
  62. package/dist/tools/stat.js +14 -2
  63. package/dist/tools/tree.d.ts +0 -1
  64. package/dist/tools/tree.js +14 -2
  65. package/dist/tools.d.ts +0 -1
  66. package/dist/tools.js +0 -1
  67. package/package.json +22 -19
  68. package/dist/config.d.ts.map +0 -1
  69. package/dist/config.js.map +0 -1
  70. package/dist/index.d.ts.map +0 -1
  71. package/dist/index.js.map +0 -1
  72. package/dist/lib/constants.d.ts.map +0 -1
  73. package/dist/lib/constants.js.map +0 -1
  74. package/dist/lib/errors.d.ts.map +0 -1
  75. package/dist/lib/errors.js.map +0 -1
  76. package/dist/lib/file-operations/file-info.d.ts.map +0 -1
  77. package/dist/lib/file-operations/file-info.js.map +0 -1
  78. package/dist/lib/file-operations/gitignore.d.ts.map +0 -1
  79. package/dist/lib/file-operations/gitignore.js.map +0 -1
  80. package/dist/lib/file-operations/glob-engine.d.ts.map +0 -1
  81. package/dist/lib/file-operations/glob-engine.js.map +0 -1
  82. package/dist/lib/file-operations/list-directory.d.ts.map +0 -1
  83. package/dist/lib/file-operations/list-directory.js.map +0 -1
  84. package/dist/lib/file-operations/read-multiple-files.d.ts.map +0 -1
  85. package/dist/lib/file-operations/read-multiple-files.js.map +0 -1
  86. package/dist/lib/file-operations/search-content.d.ts.map +0 -1
  87. package/dist/lib/file-operations/search-content.js.map +0 -1
  88. package/dist/lib/file-operations/search-files.d.ts.map +0 -1
  89. package/dist/lib/file-operations/search-files.js.map +0 -1
  90. package/dist/lib/file-operations/search-worker.d.ts.map +0 -1
  91. package/dist/lib/file-operations/search-worker.js.map +0 -1
  92. package/dist/lib/file-operations/tree.d.ts.map +0 -1
  93. package/dist/lib/file-operations/tree.js.map +0 -1
  94. package/dist/lib/fs-helpers.d.ts.map +0 -1
  95. package/dist/lib/fs-helpers.js.map +0 -1
  96. package/dist/lib/observability.d.ts.map +0 -1
  97. package/dist/lib/observability.js.map +0 -1
  98. package/dist/lib/path-policy.d.ts.map +0 -1
  99. package/dist/lib/path-policy.js.map +0 -1
  100. package/dist/lib/path-validation.d.ts.map +0 -1
  101. package/dist/lib/path-validation.js.map +0 -1
  102. package/dist/lib/resource-store.d.ts.map +0 -1
  103. package/dist/lib/resource-store.js.map +0 -1
  104. package/dist/resources.d.ts.map +0 -1
  105. package/dist/resources.js.map +0 -1
  106. package/dist/schemas.d.ts.map +0 -1
  107. package/dist/schemas.js.map +0 -1
  108. package/dist/server.d.ts.map +0 -1
  109. package/dist/server.js.map +0 -1
  110. package/dist/tools/list-directory.d.ts.map +0 -1
  111. package/dist/tools/list-directory.js.map +0 -1
  112. package/dist/tools/read-multiple.d.ts.map +0 -1
  113. package/dist/tools/read-multiple.js.map +0 -1
  114. package/dist/tools/read.d.ts.map +0 -1
  115. package/dist/tools/read.js.map +0 -1
  116. package/dist/tools/roots.d.ts.map +0 -1
  117. package/dist/tools/roots.js.map +0 -1
  118. package/dist/tools/search-content.d.ts.map +0 -1
  119. package/dist/tools/search-content.js.map +0 -1
  120. package/dist/tools/search-files.d.ts.map +0 -1
  121. package/dist/tools/search-files.js.map +0 -1
  122. package/dist/tools/shared.d.ts.map +0 -1
  123. package/dist/tools/shared.js.map +0 -1
  124. package/dist/tools/stat-many.d.ts.map +0 -1
  125. package/dist/tools/stat-many.js.map +0 -1
  126. package/dist/tools/stat.d.ts.map +0 -1
  127. package/dist/tools/stat.js.map +0 -1
  128. package/dist/tools/tree.d.ts.map +0 -1
  129. package/dist/tools/tree.js.map +0 -1
  130. package/dist/tools.d.ts.map +0 -1
  131. package/dist/tools.js.map +0 -1
@@ -4,6 +4,8 @@ import * as path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { ErrorCode, isNodeError, McpError } from './errors.js';
6
6
  import { assertNotAborted, withAbort } from './fs-helpers.js';
7
+ const IS_WINDOWS = process.platform === 'win32';
8
+ const PATH_SEPARATOR = path.sep;
7
9
  function expandHome(filepath) {
8
10
  if (filepath.startsWith('~/') || filepath === '~') {
9
11
  return path.join(os.homedir(), filepath.slice(1));
@@ -13,16 +15,24 @@ function expandHome(filepath) {
13
15
  export function normalizePath(p) {
14
16
  const expanded = expandHome(p);
15
17
  const resolved = path.resolve(expanded);
16
- if (process.platform === 'win32' && /^[A-Z]:/.test(resolved)) {
18
+ if (IS_WINDOWS && /^[A-Z]:/.test(resolved)) {
17
19
  return resolved.charAt(0).toLowerCase() + resolved.slice(1);
18
20
  }
19
21
  return resolved;
20
22
  }
23
+ function isEscapingRoot(relative) {
24
+ if (relative === '')
25
+ return false;
26
+ if (relative === '..')
27
+ return true;
28
+ const parentPrefix = `..${PATH_SEPARATOR}`;
29
+ return relative.startsWith(parentPrefix);
30
+ }
21
31
  function resolveWithinRoot(root, input) {
22
32
  const resolved = path.resolve(root, input);
23
33
  const relative = path.relative(root, resolved);
24
34
  if (relative === '' ||
25
- (!relative.startsWith('..') && !path.isAbsolute(relative))) {
35
+ (!isEscapingRoot(relative) && !path.isAbsolute(relative))) {
26
36
  return resolved;
27
37
  }
28
38
  return null;
@@ -35,9 +45,8 @@ function rethrowIfAborted(error) {
35
45
  throw error;
36
46
  }
37
47
  }
38
- const PATH_SEPARATOR = process.platform === 'win32' ? '\\' : '/';
39
48
  function normalizeForComparison(value) {
40
- return process.platform === 'win32' ? value.toLowerCase() : value;
49
+ return IS_WINDOWS ? value.toLowerCase() : value;
41
50
  }
42
51
  function isSamePath(left, right) {
43
52
  return normalizeForComparison(left) === normalizeForComparison(right);
@@ -57,51 +66,60 @@ function normalizeAllowedDirectory(dir) {
57
66
  return root;
58
67
  return stripTrailingSeparator(normalized);
59
68
  }
60
- let allowedDirectories = [];
61
- function setAllowedDirectories(dirs) {
69
+ function normalizeAllowedDirectories(dirs) {
62
70
  const normalized = dirs
63
71
  .map(normalizeAllowedDirectory)
64
72
  .filter((dir) => dir.length > 0);
65
- allowedDirectories = [...new Set(normalized)];
73
+ return [...new Set(normalized)];
74
+ }
75
+ let allowedDirectoriesExpanded = [];
76
+ let allowedDirectoriesPrimary = [];
77
+ function setAllowedDirectoriesState(primary, expanded) {
78
+ allowedDirectoriesPrimary = [...new Set(primary)];
79
+ allowedDirectoriesExpanded = [...new Set(expanded)];
66
80
  }
67
81
  export function getAllowedDirectories() {
68
- return [...allowedDirectories];
82
+ return [...allowedDirectoriesExpanded];
83
+ }
84
+ function getAllowedDirectoriesForRelativeResolution() {
85
+ return allowedDirectoriesPrimary.length > 0
86
+ ? [...allowedDirectoriesPrimary]
87
+ : [...allowedDirectoriesExpanded];
69
88
  }
70
89
  export function isPathWithinDirectories(normalizedPath, allowedDirs) {
71
90
  const candidate = normalizeForComparison(normalizedPath);
72
91
  return allowedDirs.some((allowedDir) => isPathWithinRoot(normalizeForComparison(allowedDir), candidate));
73
92
  }
74
- async function expandAllowedDirectories(dirs, signal) {
75
- const normalizedDirs = dirs
76
- .map(normalizeAllowedDirectory)
77
- .filter((dir) => Boolean(dir) && dir.length > 0);
78
- const realPaths = await Promise.all(normalizedDirs.map((dir) => resolveRealPath(dir, signal)));
79
- const expanded = [];
80
- for (let i = 0; i < normalizedDirs.length; i++) {
81
- const normalized = normalizedDirs[i];
82
- if (!normalized)
83
- continue;
84
- expanded.push(normalized);
85
- const normalizedReal = realPaths[i];
86
- if (normalizedReal && !isSamePath(normalizedReal, normalized)) {
87
- expanded.push(normalizedReal);
88
- }
89
- }
90
- return [...new Set(expanded)];
91
- }
92
93
  async function resolveRealPath(normalized, signal) {
93
94
  try {
94
95
  assertNotAborted(signal);
95
96
  const realPath = await withAbort(fs.realpath(normalized), signal);
96
97
  return normalizeAllowedDirectory(realPath);
97
98
  }
98
- catch {
99
+ catch (error) {
100
+ rethrowIfAborted(error);
99
101
  return null;
100
102
  }
101
103
  }
104
+ async function expandAllowedDirectories(primaryDirs, signal) {
105
+ const realPaths = await Promise.all(primaryDirs.map((dir) => resolveRealPath(dir, signal)));
106
+ const expanded = [];
107
+ for (let i = 0; i < primaryDirs.length; i++) {
108
+ const primary = primaryDirs[i];
109
+ if (!primary)
110
+ continue;
111
+ expanded.push(primary);
112
+ const real = realPaths[i];
113
+ if (real && !isSamePath(real, primary)) {
114
+ expanded.push(real);
115
+ }
116
+ }
117
+ return [...new Set(expanded)];
118
+ }
102
119
  export async function setAllowedDirectoriesResolved(dirs, signal) {
103
- const expanded = await expandAllowedDirectories(dirs, signal);
104
- setAllowedDirectories(expanded);
120
+ const primary = normalizeAllowedDirectories(dirs);
121
+ const expanded = await expandAllowedDirectories(primary, signal);
122
+ setAllowedDirectoriesState(primary, expanded);
105
123
  }
106
124
  export const RESERVED_DEVICE_NAMES = new Set([
107
125
  'CON',
@@ -158,7 +176,7 @@ function getReservedDeviceName(segment) {
158
176
  return RESERVED_DEVICE_NAMES.has(baseName) ? baseName : undefined;
159
177
  }
160
178
  export function getReservedDeviceNameForPath(requestedPath) {
161
- if (process.platform !== 'win32')
179
+ if (!IS_WINDOWS)
162
180
  return undefined;
163
181
  const segments = requestedPath.split(/[\\/]/);
164
182
  for (const segment of segments) {
@@ -169,7 +187,7 @@ export function getReservedDeviceNameForPath(requestedPath) {
169
187
  return undefined;
170
188
  }
171
189
  function ensureNoReservedWindowsNames(requestedPath) {
172
- if (process.platform !== 'win32')
190
+ if (!IS_WINDOWS)
173
191
  return;
174
192
  const segments = requestedPath.split(/[\\/]/);
175
193
  for (const segment of segments) {
@@ -180,7 +198,7 @@ function ensureNoReservedWindowsNames(requestedPath) {
180
198
  }
181
199
  }
182
200
  export function isWindowsDriveRelativePath(requestedPath) {
183
- if (process.platform !== 'win32')
201
+ if (!IS_WINDOWS)
184
202
  return false;
185
203
  const driveLetter = requestedPath.charCodeAt(0);
186
204
  const isAsciiLetter = (driveLetter >= 65 && driveLetter <= 90) ||
@@ -198,11 +216,11 @@ function ensureNoWindowsDriveRelativePath(requestedPath) {
198
216
  function resolveRequestedPath(requestedPath) {
199
217
  const expanded = expandHome(requestedPath);
200
218
  if (!path.isAbsolute(expanded)) {
201
- const allowedDirs = getAllowedDirectories();
202
- if (allowedDirs.length > 1) {
219
+ const roots = getAllowedDirectoriesForRelativeResolution();
220
+ if (roots.length > 1) {
203
221
  throw new McpError(ErrorCode.E_INVALID_INPUT, 'Relative paths are ambiguous when multiple roots are configured. Provide an absolute path or specify the full root path.', requestedPath);
204
222
  }
205
- const baseDir = allowedDirs[0];
223
+ const baseDir = roots[0];
206
224
  if (baseDir) {
207
225
  return normalizePath(path.resolve(baseDir, expanded));
208
226
  }
@@ -252,8 +270,7 @@ function toMcpError(requestedPath, error) {
252
270
  }
253
271
  let message = '';
254
272
  if (error instanceof Error) {
255
- const { message: errorMessage } = error;
256
- message = errorMessage;
273
+ ({ message } = error);
257
274
  }
258
275
  else if (typeof error === 'string') {
259
276
  message = error;
@@ -368,4 +385,3 @@ export async function getValidRootDirectories(roots, signal) {
368
385
  }
369
386
  return validDirs;
370
387
  }
371
- //# sourceMappingURL=path-validation.js.map
@@ -20,4 +20,3 @@ interface ResourceStoreOptions {
20
20
  }
21
21
  export declare function createInMemoryResourceStore(options?: Partial<ResourceStoreOptions>): ResourceStore;
22
22
  export {};
23
- //# sourceMappingURL=resource-store.d.ts.map
@@ -72,4 +72,3 @@ export function createInMemoryResourceStore(options = {}) {
72
72
  }
73
73
  return { putText, getText, clear };
74
74
  }
75
- //# sourceMappingURL=resource-store.js.map
@@ -1,5 +1,4 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { ResourceStore } from './lib/resource-store.js';
3
- export declare function registerInstructionResource(server: McpServer, instructions: string): void;
4
- export declare function registerResultResources(server: McpServer, store: ResourceStore): void;
5
- //# sourceMappingURL=resources.d.ts.map
3
+ export declare function registerInstructionResource(server: McpServer, instructions: string, serverIcon?: string): void;
4
+ export declare function registerResultResources(server: McpServer, store: ResourceStore, serverIcon?: string): void;
package/dist/resources.js CHANGED
@@ -3,11 +3,18 @@ import { ErrorCode, McpError } from './lib/errors.js';
3
3
  const RESULT_TEMPLATE = new ResourceTemplate('fs-context://result/{id}', {
4
4
  list: undefined,
5
5
  });
6
- export function registerInstructionResource(server, instructions) {
6
+ export function registerInstructionResource(server, instructions, serverIcon) {
7
7
  server.registerResource('fs-context-instructions', 'internal://instructions', {
8
8
  title: 'Server Instructions',
9
9
  description: 'Guidance for using the fs-context MCP tools effectively.',
10
10
  mimeType: 'text/markdown',
11
+ ...(serverIcon
12
+ ? {
13
+ icons: [
14
+ { src: serverIcon, mimeType: 'image/svg+xml', sizes: ['any'] },
15
+ ],
16
+ }
17
+ : {}),
11
18
  }, (uri) => ({
12
19
  contents: [
13
20
  {
@@ -18,11 +25,18 @@ export function registerInstructionResource(server, instructions) {
18
25
  ],
19
26
  }));
20
27
  }
21
- export function registerResultResources(server, store) {
28
+ export function registerResultResources(server, store, serverIcon) {
22
29
  server.registerResource('fs-context-result', RESULT_TEMPLATE, {
23
30
  title: 'Cached Tool Result',
24
31
  description: 'Ephemeral cached tool output exposed as an MCP resource. Not guaranteed to be listed via resources/list.',
25
32
  mimeType: 'text/plain',
33
+ ...(serverIcon
34
+ ? {
35
+ icons: [
36
+ { src: serverIcon, mimeType: 'image/svg+xml', sizes: ['any'] },
37
+ ],
38
+ }
39
+ : {}),
26
40
  }, (uri, variables) => {
27
41
  const { id } = variables;
28
42
  if (typeof id !== 'string' || id.length === 0) {
@@ -40,4 +54,3 @@ export function registerResultResources(server, store) {
40
54
  };
41
55
  });
42
56
  }
43
- //# sourceMappingURL=resources.js.map
package/dist/schemas.d.ts CHANGED
@@ -259,4 +259,3 @@ export declare const GetMultipleFileInfoOutputSchema: z.ZodObject<{
259
259
  }, z.core.$strip>>;
260
260
  }, z.core.$strip>;
261
261
  export {};
262
- //# sourceMappingURL=schemas.d.ts.map
package/dist/schemas.js CHANGED
@@ -2,6 +2,8 @@ import { z } from 'zod';
2
2
  function isSafeGlobPattern(value) {
3
3
  if (value.length === 0)
4
4
  return false;
5
+ if (value.includes('**/**/**'))
6
+ return false;
5
7
  const absolutePattern = /^([/\\]|[A-Za-z]:[/\\]|\\\\)/u;
6
8
  if (absolutePattern.test(value)) {
7
9
  return false;
@@ -12,14 +14,15 @@ function isSafeGlobPattern(value) {
12
14
  return true;
13
15
  }
14
16
  const MAX_PATH_LENGTH = 4096;
15
- const OptionalPathSchema = z
16
- .string()
17
- .max(MAX_PATH_LENGTH, `Path is too long (max ${MAX_PATH_LENGTH} characters)`)
18
- .optional();
19
- const RequiredPathSchema = z
17
+ const DESC_PATH_ROOT = 'Base directory for the operation (leave empty for workspace root). ' +
18
+ 'If multiple roots are configured, path is required and must be absolute. ' +
19
+ 'Examples: "src", "src/components"';
20
+ const DESC_PATH_REQUIRED = 'Absolute path to file or directory. Examples: "src/index.ts", "README.md"';
21
+ const PathSchemaBase = z
20
22
  .string()
21
- .min(1, 'Path cannot be empty')
22
- .max(MAX_PATH_LENGTH, `Path is too long (max ${MAX_PATH_LENGTH} characters)`);
23
+ .max(MAX_PATH_LENGTH, `Path exceeds maximum length of ${MAX_PATH_LENGTH} characters`);
24
+ const OptionalPathSchema = PathSchemaBase.optional();
25
+ const RequiredPathSchema = PathSchemaBase.min(1, 'Path is required');
23
26
  const FileTypeSchema = z.enum(['file', 'directory', 'symlink', 'other']);
24
27
  const TreeEntryTypeSchema = z.enum(['file', 'directory', 'symlink', 'other']);
25
28
  const TreeEntrySchema = z.lazy(() => z.object({
@@ -100,14 +103,12 @@ const ReadRangeInputSchema = z.strictObject({
100
103
  endLine: LineNumberSchema.optional(),
101
104
  });
102
105
  export const ListDirectoryInputSchema = z.strictObject({
103
- path: OptionalPathSchema.describe('Directory path to list (leave empty for workspace root). ' +
104
- 'If multiple roots are configured, path is required and must be absolute. ' +
105
- 'Examples: "src", "src/components", "lib/utils"'),
106
+ path: OptionalPathSchema.describe(DESC_PATH_ROOT),
106
107
  includeHidden: z
107
108
  .boolean()
108
109
  .optional()
109
110
  .default(false)
110
- .describe('Include hidden files and directories'),
111
+ .describe('Include hidden files and directories (starting with .)'),
111
112
  includeIgnored: z
112
113
  .boolean()
113
114
  .optional()
@@ -118,27 +119,15 @@ export const ListAllowedDirectoriesInputSchema = z
118
119
  .strictObject({})
119
120
  .describe('No input parameters.');
120
121
  export const SearchFilesInputSchema = z.strictObject({
121
- path: OptionalPathSchema.describe('Base directory to search from (leave empty for workspace root). ' +
122
- 'If multiple roots are configured, path is required and must be absolute. ' +
123
- 'Examples: "src", "lib", "tests"'),
122
+ path: OptionalPathSchema.describe(DESC_PATH_ROOT),
124
123
  pattern: z
125
124
  .string()
126
- .min(1, 'Pattern cannot be empty')
127
- .max(1000, 'Pattern is too long (max 1000 characters)')
128
- .refine((val) => {
129
- try {
130
- if (val.includes('**/**/**')) {
131
- return false;
132
- }
133
- return isSafeGlobPattern(val);
134
- }
135
- catch {
136
- return false;
137
- }
138
- }, {
125
+ .min(1, 'Pattern is required')
126
+ .max(1000, 'Pattern exceeds 1000 characters')
127
+ .refine((val) => isSafeGlobPattern(val), {
139
128
  error: 'Invalid glob pattern syntax or unsafe path (absolute/.. segments not allowed)',
140
129
  })
141
- .describe('Glob pattern to match files. Examples: "**/*.ts" (all TypeScript files), "src/**/*.js" (JS files in src), "*.json" (JSON files in current dir)'),
130
+ .describe('Glob pattern to match files. Examples: "**/*.ts", "src/**/*.js", "*.json"'),
142
131
  maxResults: z
143
132
  .number()
144
133
  .int({ error: 'maxResults must be an integer' })
@@ -155,9 +144,7 @@ export const SearchFilesInputSchema = z.strictObject({
155
144
  'Set to true when debugging in dependencies.'),
156
145
  });
157
146
  export const TreeInputSchema = z.strictObject({
158
- path: OptionalPathSchema.describe('Base directory to render as a tree (leave empty for workspace root). ' +
159
- 'If multiple roots are configured, path is required and must be absolute. ' +
160
- 'Examples: "src", "lib"'),
147
+ path: OptionalPathSchema.describe(DESC_PATH_ROOT),
161
148
  maxDepth: z
162
149
  .number()
163
150
  .int({ error: 'maxDepth must be an integer' })
@@ -178,7 +165,7 @@ export const TreeInputSchema = z.strictObject({
178
165
  .boolean()
179
166
  .optional()
180
167
  .default(false)
181
- .describe('Include hidden files and directories'),
168
+ .describe('Include hidden files and directories (starting with .)'),
182
169
  includeIgnored: z
183
170
  .boolean()
184
171
  .optional()
@@ -187,49 +174,47 @@ export const TreeInputSchema = z.strictObject({
187
174
  'When true, also disables root .gitignore filtering.'),
188
175
  });
189
176
  export const SearchContentInputSchema = z.strictObject({
190
- path: OptionalPathSchema.describe('Base directory or file path to search within (leave empty for workspace root). ' +
191
- 'If multiple roots are configured, path is required and must be absolute. ' +
192
- 'Examples: "src", "lib", "tests", "src/index.ts"'),
177
+ path: OptionalPathSchema.describe(DESC_PATH_ROOT),
193
178
  pattern: z
194
179
  .string()
195
- .min(1, 'Pattern cannot be empty')
196
- .max(1000, 'Pattern is too long (max 1000 characters)')
197
- .describe('Text to search for. Examples: "console.log", "import React", "className"'),
180
+ .min(1, 'Pattern is required')
181
+ .max(1000, 'Pattern exceeds 1000 characters')
182
+ .describe('Text to search for (regex). Examples: "console\\.log", "class User"'),
198
183
  includeHidden: z
199
184
  .boolean()
200
185
  .optional()
201
186
  .default(false)
202
- .describe('Include hidden files and directories'),
187
+ .describe('Include hidden files and directories (starting with .)'),
203
188
  });
204
189
  export const ReadFileInputSchema = ReadRangeInputSchema.extend({
205
- path: RequiredPathSchema.describe('Path to the file to read. ' +
206
- 'Examples: "README.md", "src/index.ts", "package.json"'),
190
+ path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
207
191
  head: HeadLinesSchema.describe('Read only the first N lines of the file (useful for previewing large files)'),
208
- startLine: LineNumberSchema.optional().describe('1-based line number to start reading from (inclusive). Useful for reading context around a match.'),
192
+ startLine: LineNumberSchema.optional().describe('1-based line number to start reading from (inclusive).'),
209
193
  endLine: LineNumberSchema.optional().describe('1-based line number to stop reading at (inclusive). Requires startLine.'),
210
- }).superRefine(validateReadRange);
194
+ })
195
+ .strict()
196
+ .superRefine(validateReadRange);
211
197
  export const ReadMultipleFilesInputSchema = ReadRangeInputSchema.extend({
212
198
  paths: z
213
199
  .array(RequiredPathSchema)
214
200
  .min(1, 'At least one path is required')
215
201
  .max(100, 'Cannot read more than 100 files at once')
216
- .describe('Array of file paths to read. ' +
217
- 'Examples: ["README.md", "package.json"], ["src/index.ts", "src/server.ts"]'),
202
+ .describe('Array of file paths to read. Examples: ["README.md", "src/index.ts"]'),
218
203
  head: HeadLinesSchema.describe('Read only the first N lines of each file'),
219
204
  startLine: LineNumberSchema.optional().describe('1-based line number to start reading from (inclusive), applied to each file.'),
220
205
  endLine: LineNumberSchema.optional().describe('1-based line number to stop reading at (inclusive), applied to each file. Requires startLine.'),
221
- }).superRefine(validateReadRange);
206
+ })
207
+ .strict()
208
+ .superRefine(validateReadRange);
222
209
  export const GetFileInfoInputSchema = z.strictObject({
223
- path: RequiredPathSchema.describe('Path to file or directory. ' +
224
- 'Examples: "src", "README.md", "src/index.ts"'),
210
+ path: RequiredPathSchema.describe(DESC_PATH_REQUIRED),
225
211
  });
226
212
  export const GetMultipleFileInfoInputSchema = z.strictObject({
227
213
  paths: z
228
214
  .array(RequiredPathSchema)
229
215
  .min(1, 'At least one path is required')
230
216
  .max(100, 'Cannot get info for more than 100 files at once')
231
- .describe('Array of file or directory paths. ' +
232
- 'Examples: ["src", "lib"], ["package.json", "tsconfig.json"]'),
217
+ .describe('Array of file or directory paths. Examples: ["src", "lib"]'),
233
218
  });
234
219
  export const ListAllowedDirectoriesOutputSchema = z.object({
235
220
  ok: z.boolean(),
@@ -333,4 +318,3 @@ export const GetMultipleFileInfoOutputSchema = z.object({
333
318
  summary: OperationSummarySchema.optional(),
334
319
  error: ErrorSchema.optional(),
335
320
  });
336
- //# sourceMappingURL=schemas.js.map
package/dist/server.d.ts CHANGED
@@ -11,4 +11,3 @@ interface ServerOptions {
11
11
  export declare function createServer(options?: ServerOptions): McpServer;
12
12
  export declare function startServer(server: McpServer): Promise<void>;
13
13
  export {};
14
- //# sourceMappingURL=server.d.ts.map
package/dist/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
+ import { readFileSync } from 'node:fs';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { parseArgs as parseNodeArgs } from 'node:util';
5
6
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -41,7 +42,7 @@ async function validateDirectoryPath(inputPath) {
41
42
  function assertDirectory(stats, inputPath) {
42
43
  if (stats.isDirectory())
43
44
  return;
44
- throw new Error(`Error: '${inputPath}' is not a directory`);
45
+ throw new Error(`Error: ${inputPath} is not a directory`);
45
46
  }
46
47
  function isCliError(error) {
47
48
  return error instanceof Error && error.message.startsWith('Error:');
@@ -49,7 +50,7 @@ function isCliError(error) {
49
50
  function normalizeDirectoryError(error, inputPath) {
50
51
  if (isCliError(error))
51
52
  return error;
52
- return new Error(`Error: Cannot access directory '${inputPath}'`);
53
+ return new Error(`Error: Cannot access directory ${inputPath}`);
53
54
  }
54
55
  async function normalizeCliDirectories(args) {
55
56
  return Promise.all(args.map(validateDirectoryPath));
@@ -79,8 +80,11 @@ export async function parseArgs() {
79
80
  const ROOTS_TIMEOUT_MS = 5000;
80
81
  const ROOTS_DEBOUNCE_MS = 100;
81
82
  const MCP_LOGGER_NAME = 'fs-context';
83
+ function canSendMcpLogs(server) {
84
+ return Boolean(server.server.getClientCapabilities());
85
+ }
82
86
  function logToMcp(server, level, data) {
83
- if (!server) {
87
+ if (!server || !canSendMcpLogs(server)) {
84
88
  console.error(data);
85
89
  return;
86
90
  }
@@ -90,7 +94,7 @@ function logToMcp(server, level, data) {
90
94
  data,
91
95
  };
92
96
  void server.sendLoggingMessage(params).catch((error) => {
93
- console.error(`Failed to send MCP log (${level}):`, data, error instanceof Error ? error.message : String(error));
97
+ console.error(`Failed to send MCP log: ${level} │ ${data}`, data, error instanceof Error ? error.message : String(error));
94
98
  });
95
99
  }
96
100
  class RootsManager {
@@ -143,10 +147,10 @@ class RootsManager {
143
147
  }
144
148
  logMissingDirectories(server) {
145
149
  if (this.options.allowCwd) {
146
- logToMcp(server, 'notice', 'No directories specified. Using current working directory.');
150
+ logToMcp(server, 'notice', 'No allowed directories specified. Using the current working directory as an allowed directory.');
147
151
  return;
148
152
  }
149
- logToMcp(server, 'warning', 'No directories configured. Use --allow-cwd flag or specify directories via CLI/roots protocol. The server will not be able to access any files until directories are configured.');
153
+ logToMcp(server, 'warning', 'No allowed directories specified. Please provide directories as command-line arguments or enable --allow-cwd to use the current working directory.');
150
154
  }
151
155
  async updateRootsFromClient(server) {
152
156
  try {
@@ -177,15 +181,21 @@ function getRootsManager(server) {
177
181
  }
178
182
  return manager;
179
183
  }
184
+ const RootSchema = z
185
+ .object({
186
+ uri: z.string(),
187
+ name: z.string().optional(),
188
+ })
189
+ .strict();
180
190
  const RootsResponseSchema = z.object({
181
- roots: z.array(z.any()).optional(),
191
+ roots: z.array(RootSchema).optional(),
182
192
  });
183
193
  function extractRoots(value) {
184
194
  const parsed = RootsResponseSchema.safeParse(value);
185
195
  if (!parsed.success || !parsed.data.roots) {
186
196
  return [];
187
197
  }
188
- return parsed.data.roots.filter(isRoot);
198
+ return parsed.data.roots.filter(isRoot).map(normalizeRoot);
189
199
  }
190
200
  async function resolveRootDirectories(roots) {
191
201
  if (roots.length === 0)
@@ -204,6 +214,9 @@ function isRoot(value) {
204
214
  'uri' in value &&
205
215
  typeof value.uri === 'string');
206
216
  }
217
+ function normalizeRoot(root) {
218
+ return root.name ? { uri: root.uri, name: root.name } : { uri: root.uri };
219
+ }
207
220
  async function filterRootsWithinBaseline(roots, baseline, signal) {
208
221
  const normalizedBaseline = normalizeAllowedDirectories(baseline);
209
222
  const filtered = [];
@@ -242,6 +255,16 @@ try {
242
255
  catch (error) {
243
256
  console.error('[WARNING] Failed to load instructions.md:', error instanceof Error ? error.message : String(error));
244
257
  }
258
+ function getLocalIconData() {
259
+ try {
260
+ const iconPath = new URL('../assets/logo.svg', import.meta.url);
261
+ const buffer = readFileSync(iconPath);
262
+ return `data:image/svg+xml;base64,${buffer.toString('base64')}`;
263
+ }
264
+ catch {
265
+ return undefined;
266
+ }
267
+ }
245
268
  function resolveToolErrorCode(message) {
246
269
  const explicit = extractExplicitErrorCode(message);
247
270
  if (explicit)
@@ -288,6 +311,7 @@ function patchToolErrorHandling(server) {
288
311
  }
289
312
  export function createServer(options = {}) {
290
313
  const resourceStore = createInMemoryResourceStore();
314
+ const localIcon = getLocalIconData();
291
315
  const serverConfig = {
292
316
  capabilities: {
293
317
  logging: {},
@@ -301,15 +325,23 @@ export function createServer(options = {}) {
301
325
  const server = new McpServer({
302
326
  name: 'fs-context-mcp',
303
327
  version: SERVER_VERSION,
328
+ ...(localIcon
329
+ ? {
330
+ icons: [
331
+ { src: localIcon, mimeType: 'image/svg+xml', sizes: ['any'] },
332
+ ],
333
+ }
334
+ : {}),
304
335
  }, serverConfig);
305
336
  patchToolErrorHandling(server);
306
337
  const rootsManager = new RootsManager(options);
307
338
  rootsManagers.set(server, rootsManager);
308
- registerInstructionResource(server, serverInstructions);
309
- registerResultResources(server, resourceStore);
339
+ registerInstructionResource(server, serverInstructions, localIcon);
340
+ registerResultResources(server, resourceStore, localIcon);
310
341
  registerAllTools(server, {
311
342
  resourceStore,
312
343
  isInitialized: () => rootsManager.isInitialized(),
344
+ ...(localIcon ? { serverIcon: localIcon } : {}),
313
345
  });
314
346
  return server;
315
347
  }
@@ -321,4 +353,3 @@ export async function startServer(server) {
321
353
  await server.connect(transport);
322
354
  rootsManager.logMissingDirectoriesIfNeeded(server);
323
355
  }
324
- //# sourceMappingURL=server.js.map
@@ -1,4 +1,3 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { type ToolRegistrationOptions } from './shared.js';
3
3
  export declare function registerListDirectoryTool(server: McpServer, options?: ToolRegistrationOptions): void;
4
- //# sourceMappingURL=list-directory.d.ts.map
@@ -80,6 +80,18 @@ async function handleListDirectory(args, signal) {
80
80
  }
81
81
  export function registerListDirectoryTool(server, options = {}) {
82
82
  const handler = (args, extra) => withToolDiagnostics('ls', () => withToolErrorHandling(() => handleListDirectory(args, extra.signal), (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_DIRECTORY, args.path ?? '.')), { path: args.path ?? '.' });
83
- server.registerTool('ls', LIST_DIRECTORY_TOOL, wrapToolHandler(handler, { guard: options.isInitialized }));
83
+ server.registerTool('ls', {
84
+ ...LIST_DIRECTORY_TOOL,
85
+ ...(options.serverIcon
86
+ ? {
87
+ icons: [
88
+ {
89
+ src: options.serverIcon,
90
+ mimeType: 'image/svg+xml',
91
+ sizes: ['any'],
92
+ },
93
+ ],
94
+ }
95
+ : {}),
96
+ }, wrapToolHandler(handler, { guard: options.isInitialized }));
84
97
  }
85
- //# sourceMappingURL=list-directory.js.map
@@ -1,4 +1,3 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { type ToolRegistrationOptions } from './shared.js';
3
3
  export declare function registerReadMultipleFilesTool(server: McpServer, options?: ToolRegistrationOptions): void;
4
- //# sourceMappingURL=read-multiple.d.ts.map
@@ -104,6 +104,18 @@ export function registerReadMultipleFilesTool(server, options = {}) {
104
104
  }
105
105
  }, (error) => buildToolErrorResponse(error, ErrorCode.E_NOT_FILE, primaryPath)), { path: primaryPath });
106
106
  };
107
- server.registerTool('read_many', READ_MULTIPLE_FILES_TOOL, wrapToolHandler(handler, { guard: options.isInitialized }));
107
+ server.registerTool('read_many', {
108
+ ...READ_MULTIPLE_FILES_TOOL,
109
+ ...(options.serverIcon
110
+ ? {
111
+ icons: [
112
+ {
113
+ src: options.serverIcon,
114
+ mimeType: 'image/svg+xml',
115
+ sizes: ['any'],
116
+ },
117
+ ],
118
+ }
119
+ : {}),
120
+ }, wrapToolHandler(handler, { guard: options.isInitialized }));
108
121
  }
109
- //# sourceMappingURL=read-multiple.js.map
@@ -1,4 +1,3 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { type ToolRegistrationOptions } from './shared.js';
3
3
  export declare function registerReadFileTool(server: McpServer, options?: ToolRegistrationOptions): void;
4
- //# sourceMappingURL=read.d.ts.map