@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.
- package/README.md +171 -417
- package/dist/assets/logo.svg +3766 -0
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/lib/constants.d.ts +0 -1
- package/dist/lib/constants.js +0 -1
- package/dist/lib/errors.d.ts +0 -1
- package/dist/lib/errors.js +0 -1
- package/dist/lib/file-operations/file-info.d.ts +0 -1
- package/dist/lib/file-operations/file-info.js +0 -1
- package/dist/lib/file-operations/gitignore.d.ts +0 -1
- package/dist/lib/file-operations/gitignore.js +0 -1
- package/dist/lib/file-operations/glob-engine.d.ts +0 -1
- package/dist/lib/file-operations/glob-engine.js +101 -84
- package/dist/lib/file-operations/list-directory.d.ts +0 -1
- package/dist/lib/file-operations/list-directory.js +91 -62
- package/dist/lib/file-operations/read-multiple-files.d.ts +0 -1
- package/dist/lib/file-operations/read-multiple-files.js +0 -1
- package/dist/lib/file-operations/search-content.d.ts +0 -1
- package/dist/lib/file-operations/search-content.js +68 -21
- package/dist/lib/file-operations/search-files.d.ts +0 -1
- package/dist/lib/file-operations/search-files.js +21 -11
- package/dist/lib/file-operations/search-worker.d.ts +0 -1
- package/dist/lib/file-operations/search-worker.js +0 -1
- package/dist/lib/file-operations/tree.d.ts +0 -1
- package/dist/lib/file-operations/tree.js +18 -8
- package/dist/lib/fs-helpers.d.ts +0 -1
- package/dist/lib/fs-helpers.js +19 -25
- package/dist/lib/observability.d.ts +0 -1
- package/dist/lib/observability.js +49 -43
- package/dist/lib/path-policy.d.ts +0 -1
- package/dist/lib/path-policy.js +0 -1
- package/dist/lib/path-validation.d.ts +0 -1
- package/dist/lib/path-validation.js +54 -38
- package/dist/lib/resource-store.d.ts +0 -1
- package/dist/lib/resource-store.js +0 -1
- package/dist/resources.d.ts +2 -3
- package/dist/resources.js +16 -3
- package/dist/schemas.d.ts +0 -1
- package/dist/schemas.js +35 -51
- package/dist/server.d.ts +0 -1
- package/dist/server.js +42 -11
- package/dist/tools/list-directory.d.ts +0 -1
- package/dist/tools/list-directory.js +14 -2
- package/dist/tools/read-multiple.d.ts +0 -1
- package/dist/tools/read-multiple.js +14 -2
- package/dist/tools/read.d.ts +0 -1
- package/dist/tools/read.js +14 -2
- package/dist/tools/roots.d.ts +0 -1
- package/dist/tools/roots.js +14 -2
- package/dist/tools/search-content.d.ts +0 -1
- package/dist/tools/search-content.js +29 -14
- package/dist/tools/search-files.d.ts +0 -1
- package/dist/tools/search-files.js +14 -2
- package/dist/tools/shared.d.ts +7 -1
- package/dist/tools/shared.js +7 -4
- package/dist/tools/stat-many.d.ts +0 -1
- package/dist/tools/stat-many.js +14 -2
- package/dist/tools/stat.d.ts +0 -1
- package/dist/tools/stat.js +14 -2
- package/dist/tools/tree.d.ts +0 -1
- package/dist/tools/tree.js +14 -2
- package/dist/tools.d.ts +0 -1
- package/dist/tools.js +0 -1
- package/package.json +22 -19
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/constants.d.ts.map +0 -1
- package/dist/lib/constants.js.map +0 -1
- package/dist/lib/errors.d.ts.map +0 -1
- package/dist/lib/errors.js.map +0 -1
- package/dist/lib/file-operations/file-info.d.ts.map +0 -1
- package/dist/lib/file-operations/file-info.js.map +0 -1
- package/dist/lib/file-operations/gitignore.d.ts.map +0 -1
- package/dist/lib/file-operations/gitignore.js.map +0 -1
- package/dist/lib/file-operations/glob-engine.d.ts.map +0 -1
- package/dist/lib/file-operations/glob-engine.js.map +0 -1
- package/dist/lib/file-operations/list-directory.d.ts.map +0 -1
- package/dist/lib/file-operations/list-directory.js.map +0 -1
- package/dist/lib/file-operations/read-multiple-files.d.ts.map +0 -1
- package/dist/lib/file-operations/read-multiple-files.js.map +0 -1
- package/dist/lib/file-operations/search-content.d.ts.map +0 -1
- package/dist/lib/file-operations/search-content.js.map +0 -1
- package/dist/lib/file-operations/search-files.d.ts.map +0 -1
- package/dist/lib/file-operations/search-files.js.map +0 -1
- package/dist/lib/file-operations/search-worker.d.ts.map +0 -1
- package/dist/lib/file-operations/search-worker.js.map +0 -1
- package/dist/lib/file-operations/tree.d.ts.map +0 -1
- package/dist/lib/file-operations/tree.js.map +0 -1
- package/dist/lib/fs-helpers.d.ts.map +0 -1
- package/dist/lib/fs-helpers.js.map +0 -1
- package/dist/lib/observability.d.ts.map +0 -1
- package/dist/lib/observability.js.map +0 -1
- package/dist/lib/path-policy.d.ts.map +0 -1
- package/dist/lib/path-policy.js.map +0 -1
- package/dist/lib/path-validation.d.ts.map +0 -1
- package/dist/lib/path-validation.js.map +0 -1
- package/dist/lib/resource-store.d.ts.map +0 -1
- package/dist/lib/resource-store.js.map +0 -1
- package/dist/resources.d.ts.map +0 -1
- package/dist/resources.js.map +0 -1
- package/dist/schemas.d.ts.map +0 -1
- package/dist/schemas.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/tools/list-directory.d.ts.map +0 -1
- package/dist/tools/list-directory.js.map +0 -1
- package/dist/tools/read-multiple.d.ts.map +0 -1
- package/dist/tools/read-multiple.js.map +0 -1
- package/dist/tools/read.d.ts.map +0 -1
- package/dist/tools/read.js.map +0 -1
- package/dist/tools/roots.d.ts.map +0 -1
- package/dist/tools/roots.js.map +0 -1
- package/dist/tools/search-content.d.ts.map +0 -1
- package/dist/tools/search-content.js.map +0 -1
- package/dist/tools/search-files.d.ts.map +0 -1
- package/dist/tools/search-files.js.map +0 -1
- package/dist/tools/shared.d.ts.map +0 -1
- package/dist/tools/shared.js.map +0 -1
- package/dist/tools/stat-many.d.ts.map +0 -1
- package/dist/tools/stat-many.js.map +0 -1
- package/dist/tools/stat.d.ts.map +0 -1
- package/dist/tools/stat.js.map +0 -1
- package/dist/tools/tree.d.ts.map +0 -1
- package/dist/tools/tree.js.map +0 -1
- package/dist/tools.d.ts.map +0 -1
- 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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 [...
|
|
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
|
|
104
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
202
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
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
|
package/dist/resources.d.ts
CHANGED
|
@@ -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
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
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
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
|
-
.
|
|
22
|
-
|
|
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(
|
|
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(
|
|
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
|
|
127
|
-
.max(1000, 'Pattern
|
|
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"
|
|
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(
|
|
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(
|
|
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
|
|
196
|
-
.max(1000, 'Pattern
|
|
197
|
-
.describe('Text to search for. Examples: "console
|
|
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(
|
|
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).
|
|
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
|
-
})
|
|
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
|
-
})
|
|
206
|
+
})
|
|
207
|
+
.strict()
|
|
208
|
+
.superRefine(validateReadRange);
|
|
222
209
|
export const GetFileInfoInputSchema = z.strictObject({
|
|
223
|
-
path: RequiredPathSchema.describe(
|
|
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
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:
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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',
|
|
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',
|
|
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
|
package/dist/tools/read.d.ts
CHANGED
|
@@ -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
|