@j0hanz/filesystem-mcp 1.19.0 → 2.0.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 +333 -838
- package/dist/cli-help.d.ts +3 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +169 -0
- package/dist/cli-help.js.map +1 -0
- package/dist/cli.d.ts +28 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +168 -123
- package/dist/cli.js.map +1 -0
- package/dist/core/concurrency.d.ts +70 -0
- package/dist/core/concurrency.d.ts.map +1 -0
- package/dist/core/concurrency.js +173 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/config.d.ts +19 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +11 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/cursor.d.ts +23 -0
- package/dist/core/cursor.d.ts.map +1 -0
- package/dist/core/cursor.js +62 -0
- package/dist/core/cursor.js.map +1 -0
- package/dist/core/errors.d.ts +96 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +356 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/file-uri.d.ts +24 -0
- package/dist/core/file-uri.d.ts.map +1 -0
- package/dist/core/file-uri.js +52 -0
- package/dist/core/file-uri.js.map +1 -0
- package/dist/core/fmt.d.ts +38 -0
- package/dist/core/fmt.d.ts.map +1 -0
- package/dist/core/fmt.js +109 -0
- package/dist/core/fmt.js.map +1 -0
- package/dist/core/fs.d.ts +108 -0
- package/dist/core/fs.d.ts.map +1 -0
- package/dist/core/fs.js +243 -0
- package/dist/core/fs.js.map +1 -0
- package/dist/core/glob.d.ts +41 -0
- package/dist/core/glob.d.ts.map +1 -0
- package/dist/core/glob.js +399 -0
- package/dist/core/glob.js.map +1 -0
- package/dist/core/input-required.d.ts +147 -0
- package/dist/core/input-required.d.ts.map +1 -0
- package/dist/core/input-required.js +261 -0
- package/dist/core/input-required.js.map +1 -0
- package/dist/core/mime.d.ts +15 -0
- package/dist/core/mime.d.ts.map +1 -0
- package/dist/core/mime.js +188 -0
- package/dist/core/mime.js.map +1 -0
- package/dist/core/observability.d.ts +10 -0
- package/dist/core/observability.d.ts.map +1 -0
- package/dist/core/observability.js +76 -0
- package/dist/core/observability.js.map +1 -0
- package/dist/core/page-store.d.ts +41 -0
- package/dist/core/page-store.d.ts.map +1 -0
- package/dist/core/page-store.js +74 -0
- package/dist/core/page-store.js.map +1 -0
- package/dist/core/path-completer.d.ts +10 -0
- package/dist/core/path-completer.d.ts.map +1 -0
- package/dist/core/path-completer.js +227 -0
- package/dist/core/path-completer.js.map +1 -0
- package/dist/core/path-discovery.d.ts +8 -0
- package/dist/core/path-discovery.d.ts.map +1 -0
- package/dist/core/path-discovery.js +105 -0
- package/dist/core/path-discovery.js.map +1 -0
- package/dist/core/path-utils.d.ts +9 -0
- package/dist/core/path-utils.d.ts.map +1 -0
- package/dist/core/path-utils.js +121 -0
- package/dist/core/path-utils.js.map +1 -0
- package/dist/core/path.d.ts +148 -0
- package/dist/core/path.d.ts.map +1 -0
- package/dist/core/path.js +674 -0
- package/dist/core/path.js.map +1 -0
- package/dist/core/primitives.d.ts +23 -0
- package/dist/core/primitives.d.ts.map +1 -0
- package/dist/core/primitives.js +54 -0
- package/dist/core/primitives.js.map +1 -0
- package/dist/core/read.d.ts +74 -0
- package/dist/core/read.d.ts.map +1 -0
- package/dist/core/read.js +562 -0
- package/dist/core/read.js.map +1 -0
- package/dist/core/schema.d.ts +139 -0
- package/dist/core/schema.d.ts.map +1 -0
- package/dist/core/schema.js +345 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/core/search.d.ts +100 -0
- package/dist/core/search.d.ts.map +1 -0
- package/dist/core/search.js +248 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/sensitive.d.ts +6 -0
- package/dist/core/sensitive.d.ts.map +1 -0
- package/dist/core/sensitive.js +142 -0
- package/dist/core/sensitive.js.map +1 -0
- package/dist/core/store.d.ts +57 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +162 -0
- package/dist/core/store.js.map +1 -0
- package/dist/core/util.d.ts +26 -0
- package/dist/core/util.d.ts.map +1 -0
- package/dist/core/util.js +66 -0
- package/dist/core/util.js.map +1 -0
- package/dist/core/watcher-registry.d.ts +68 -0
- package/dist/core/watcher-registry.d.ts.map +1 -0
- package/dist/core/watcher-registry.js +312 -0
- package/dist/core/watcher-registry.js.map +1 -0
- package/dist/http-policy.d.ts +121 -0
- package/dist/http-policy.d.ts.map +1 -0
- package/dist/http-policy.js +340 -0
- package/dist/http-policy.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -41
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +12 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +71 -0
- package/dist/instructions.js.map +1 -0
- package/dist/prompts.d.ts +8 -6
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +74 -168
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +73 -9
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +392 -163
- package/dist/resources.js.map +1 -0
- package/dist/server.d.ts +62 -1
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +139 -1
- package/dist/server.js.map +1 -0
- package/dist/tools/batch.d.ts +89 -0
- package/dist/tools/batch.d.ts.map +1 -0
- package/dist/tools/batch.js +157 -0
- package/dist/tools/batch.js.map +1 -0
- package/dist/tools/create.d.ts +2 -0
- package/dist/tools/create.d.ts.map +1 -0
- package/dist/tools/create.js +142 -0
- package/dist/tools/create.js.map +1 -0
- package/dist/tools/define.d.ts +110 -0
- package/dist/tools/define.d.ts.map +1 -0
- package/dist/tools/define.js +394 -0
- package/dist/tools/define.js.map +1 -0
- package/dist/tools/delete-file.d.ts +2 -4
- package/dist/tools/delete-file.d.ts.map +1 -0
- package/dist/tools/delete-file.js +330 -87
- package/dist/tools/delete-file.js.map +1 -0
- package/dist/tools/diff.d.ts +2 -0
- package/dist/tools/diff.d.ts.map +1 -0
- package/dist/tools/diff.js +94 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/edit.d.ts +2 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +442 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/index.d.ts +26 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/list.d.ts +2 -0
- package/dist/tools/list.d.ts.map +1 -0
- package/dist/tools/list.js +291 -0
- package/dist/tools/list.js.map +1 -0
- package/dist/tools/move.d.ts +2 -0
- package/dist/tools/move.d.ts.map +1 -0
- package/dist/tools/move.js +321 -0
- package/dist/tools/move.js.map +1 -0
- package/dist/tools/patch.d.ts +2 -0
- package/dist/tools/patch.d.ts.map +1 -0
- package/dist/tools/patch.js +155 -0
- package/dist/tools/patch.js.map +1 -0
- package/dist/tools/progress.d.ts +52 -0
- package/dist/tools/progress.d.ts.map +1 -0
- package/dist/tools/progress.js +172 -0
- package/dist/tools/progress.js.map +1 -0
- package/dist/tools/read.d.ts +2 -4
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +398 -152
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/replace-in-files.d.ts +2 -4
- package/dist/tools/replace-in-files.d.ts.map +1 -0
- package/dist/tools/replace-in-files.js +435 -264
- package/dist/tools/replace-in-files.js.map +1 -0
- package/dist/tools/roots.d.ts +2 -4
- package/dist/tools/roots.d.ts.map +1 -0
- package/dist/tools/roots.js +29 -52
- package/dist/tools/roots.js.map +1 -0
- package/dist/tools/search-content.d.ts +2 -4
- package/dist/tools/search-content.d.ts.map +1 -0
- package/dist/tools/search-content.js +262 -228
- package/dist/tools/search-content.js.map +1 -0
- package/dist/tools/search-files.d.ts +2 -4
- package/dist/tools/search-files.d.ts.map +1 -0
- package/dist/tools/search-files.js +162 -161
- package/dist/tools/search-files.js.map +1 -0
- package/dist/tools/stat.d.ts +2 -4
- package/dist/tools/stat.d.ts.map +1 -0
- package/dist/tools/stat.js +202 -62
- package/dist/tools/stat.js.map +1 -0
- package/dist/transport/http.d.ts +5 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +276 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/shared.d.ts +45 -0
- package/dist/transport/shared.d.ts.map +1 -0
- package/dist/transport/shared.js +65 -0
- package/dist/transport/shared.js.map +1 -0
- package/dist/transport/stdio.d.ts +32 -0
- package/dist/transport/stdio.d.ts.map +1 -0
- package/dist/transport/stdio.js +279 -0
- package/dist/transport/stdio.js.map +1 -0
- package/dist/transport.d.ts +5 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +4 -0
- package/dist/transport.js.map +1 -0
- package/package.json +37 -46
- package/dist/assets/logo.svg +0 -35
- package/dist/completions.d.ts +0 -2
- package/dist/completions.js +0 -572
- package/dist/config.d.ts +0 -119
- package/dist/config.js +0 -31
- package/dist/lib/abort.d.ts +0 -7
- package/dist/lib/abort.js +0 -81
- package/dist/lib/constants.d.ts +0 -42
- package/dist/lib/constants.js +0 -313
- package/dist/lib/errors.d.ts +0 -28
- package/dist/lib/errors.js +0 -301
- package/dist/lib/file-operations/core.d.ts +0 -53
- package/dist/lib/file-operations/core.js +0 -144
- package/dist/lib/file-operations/metadata.d.ts +0 -77
- package/dist/lib/file-operations/metadata.js +0 -878
- package/dist/lib/file-operations/search.d.ts +0 -46
- package/dist/lib/file-operations/search.js +0 -1194
- package/dist/lib/file-operations/traversal.d.ts +0 -40
- package/dist/lib/file-operations/traversal.js +0 -310
- package/dist/lib/fs-helpers.d.ts +0 -46
- package/dist/lib/fs-helpers.js +0 -606
- package/dist/lib/globs.d.ts +0 -2
- package/dist/lib/globs.js +0 -19
- package/dist/lib/logger.d.ts +0 -27
- package/dist/lib/logger.js +0 -91
- package/dist/lib/observability.d.ts +0 -33
- package/dist/lib/observability.js +0 -373
- package/dist/lib/paths.d.ts +0 -35
- package/dist/lib/paths.js +0 -634
- package/dist/lib/resource-store.d.ts +0 -28
- package/dist/lib/resource-store.js +0 -195
- package/dist/lib/utils.d.ts +0 -7
- package/dist/lib/utils.js +0 -37
- package/dist/lib/zod-codecs.d.ts +0 -2
- package/dist/lib/zod-codecs.js +0 -18
- package/dist/pkg-info.d.ts +0 -7
- package/dist/pkg-info.js +0 -9
- package/dist/resources/generated-instructions.d.ts +0 -1
- package/dist/resources/generated-instructions.js +0 -105
- package/dist/resources/tool-catalog.d.ts +0 -2
- package/dist/resources/tool-catalog.js +0 -78
- package/dist/resources/tool-info.d.ts +0 -9
- package/dist/resources/tool-info.js +0 -206
- package/dist/resources/workflows.d.ts +0 -1
- package/dist/resources/workflows.js +0 -72
- package/dist/schemas.d.ts +0 -505
- package/dist/schemas.js +0 -676
- package/dist/server/bootstrap.d.ts +0 -6
- package/dist/server/bootstrap.js +0 -563
- package/dist/server/event-store.d.ts +0 -18
- package/dist/server/event-store.js +0 -71
- package/dist/server/roots-manager.d.ts +0 -28
- package/dist/server/roots-manager.js +0 -227
- package/dist/server/task-store.d.ts +0 -10
- package/dist/server/task-store.js +0 -91
- package/dist/tools/apply-patch.d.ts +0 -4
- package/dist/tools/apply-patch.js +0 -235
- package/dist/tools/calculate-hash.d.ts +0 -4
- package/dist/tools/calculate-hash.js +0 -187
- package/dist/tools/contract.d.ts +0 -50
- package/dist/tools/contract.js +0 -1
- package/dist/tools/create-directory.d.ts +0 -4
- package/dist/tools/create-directory.js +0 -68
- package/dist/tools/diff-files.d.ts +0 -4
- package/dist/tools/diff-files.js +0 -136
- package/dist/tools/edit-file.d.ts +0 -4
- package/dist/tools/edit-file.js +0 -256
- package/dist/tools/icons.d.ts +0 -15
- package/dist/tools/icons.js +0 -24
- package/dist/tools/list-directory.d.ts +0 -4
- package/dist/tools/list-directory.js +0 -218
- package/dist/tools/move-file.d.ts +0 -4
- package/dist/tools/move-file.js +0 -178
- package/dist/tools/read-multiple.d.ts +0 -4
- package/dist/tools/read-multiple.js +0 -207
- package/dist/tools/shared.d.ts +0 -172
- package/dist/tools/shared.js +0 -599
- package/dist/tools/stat-many.d.ts +0 -4
- package/dist/tools/stat-many.js +0 -104
- package/dist/tools/task-support.d.ts +0 -29
- package/dist/tools/task-support.js +0 -472
- package/dist/tools/tree.d.ts +0 -4
- package/dist/tools/tree.js +0 -102
- package/dist/tools/write-file.d.ts +0 -4
- package/dist/tools/write-file.js +0 -60
- package/dist/tools.d.ts +0 -5
- package/dist/tools.js +0 -54
package/dist/tools/stat.js
CHANGED
|
@@ -1,67 +1,207 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { ErrorCode } from '../
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
1
|
+
import { parse } from 'node:path';
|
|
2
|
+
import * as z from 'zod/v4';
|
|
3
|
+
import { ErrorCode, rethrowIfAborted } from '../core/errors.js';
|
|
4
|
+
import { formatBytes } from '../core/fmt.js';
|
|
5
|
+
import { getFileType, isHidden } from '../core/fs.js';
|
|
6
|
+
import { detectMimeType } from '../core/mime.js';
|
|
7
|
+
import { FileInfoSchema, NonNegInt, OperationSummarySchema, PerFileErrorSchema, singleOrBatchAccessPaths, singleOrBatchPathsInput, } from '../core/schema.js';
|
|
8
|
+
import { putJsonResource } from '../core/store.js';
|
|
9
|
+
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../core/util.js';
|
|
10
|
+
import { runOverPaths } from './batch.js';
|
|
11
|
+
import { defineTool } from './define.js';
|
|
12
|
+
const StatInputSchema = singleOrBatchPathsInput({ extra: {} });
|
|
13
|
+
const StatPerPathSchema = z.strictObject({
|
|
14
|
+
path: z.string().describe('Requested path'),
|
|
15
|
+
value: FileInfoSchema.optional().describe('File metadata; present on success'),
|
|
16
|
+
error: PerFileErrorSchema.optional().describe('Error details; present on failure'),
|
|
17
|
+
});
|
|
18
|
+
const StatOutputSchema = z.strictObject({
|
|
19
|
+
results: z
|
|
20
|
+
.array(StatPerPathSchema)
|
|
21
|
+
.describe('Per-path metadata results ordered to match the input paths'),
|
|
22
|
+
summary: OperationSummarySchema,
|
|
23
|
+
fileCount: NonNegInt.optional().describe('Number of regular files in the results'),
|
|
24
|
+
dirCount: NonNegInt.optional().describe('Number of directories in the results'),
|
|
25
|
+
resourceUri: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('URI to aggregated stats.json in the resource store (present when resource store is available)'),
|
|
29
|
+
});
|
|
30
|
+
function getPermissions(mode) {
|
|
31
|
+
let out = '';
|
|
32
|
+
for (const shift of [6, 3, 0]) {
|
|
33
|
+
const bits = (mode >> shift) & 0b111;
|
|
34
|
+
out += (bits & 0b100 ? 'r' : '-') + (bits & 0b010 ? 'w' : '-') + (bits & 0b001 ? 'x' : '-');
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
33
37
|
}
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
function buildFileInfoResult(name, requestedPath, isSymlink, stats, mimeType, symlinkTarget) {
|
|
39
|
+
const tokenEstimate = stats.isFile() ? Math.ceil(stats.size / 4) : undefined;
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
path: requestedPath,
|
|
43
|
+
type: isSymlink ? 'symlink' : getFileType(stats),
|
|
44
|
+
size: stats.size,
|
|
45
|
+
...(tokenEstimate !== undefined ? { tokenEstimate } : {}),
|
|
46
|
+
created: stats.birthtime,
|
|
47
|
+
modified: stats.mtime,
|
|
48
|
+
accessed: stats.atime,
|
|
49
|
+
permissions: getPermissions(stats.mode),
|
|
50
|
+
isHidden: isHidden(name),
|
|
51
|
+
...(mimeType !== undefined ? { mimeType } : {}),
|
|
52
|
+
...(symlinkTarget !== undefined ? { symlinkTarget } : {}),
|
|
42
53
|
};
|
|
43
|
-
return buildToolResponse(formatFileInfoDetails(info), structured);
|
|
44
54
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
async function getSymlinkTarget(pathToRead, fs, signal, log) {
|
|
56
|
+
signal?.throwIfAborted();
|
|
57
|
+
try {
|
|
58
|
+
const { linkString } = await fs.readlink(pathToRead);
|
|
59
|
+
return linkString;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
rethrowIfAborted(error);
|
|
63
|
+
log?.('warning', `stat: readlink failed for "${pathToRead}": ${String(error)}`, 'stat');
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function getFileInfo(filePath, options) {
|
|
68
|
+
const { signal, fs, log } = options;
|
|
69
|
+
signal?.throwIfAborted();
|
|
70
|
+
const { requestedPath, isSymlink, stats: followedStats, } = await fs.statDetailed(filePath, signal ? { signal } : undefined);
|
|
71
|
+
const { base: name, ext: rawExt } = parse(requestedPath);
|
|
72
|
+
const includeMimeType = options.includeMimeType !== false;
|
|
73
|
+
const mimeType = includeMimeType && rawExt.length > 0 ? detectMimeType(requestedPath).mimeType : undefined;
|
|
74
|
+
// For a symlink, fsStat follows the link and reports the target's metadata.
|
|
75
|
+
// Report the link's own metadata instead so size/permissions/timestamps
|
|
76
|
+
// describe the link the user asked about, not its target.
|
|
77
|
+
let stats = followedStats;
|
|
78
|
+
if (isSymlink) {
|
|
79
|
+
try {
|
|
80
|
+
({ stats } = await fs.lstat(requestedPath, signal ? { signal } : undefined));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
// A cancelled request is not a "link metadata unavailable" fallback.
|
|
84
|
+
rethrowIfAborted(error);
|
|
85
|
+
// Guarded lstat can be refused where raw lstat succeeded (e.g. an allowed
|
|
86
|
+
// root that is itself a symlink, whose parent sits outside the roots).
|
|
87
|
+
// Falling back to the followed stats degrades the report to the target's
|
|
88
|
+
// metadata, so leave a trace instead of degrading silently.
|
|
89
|
+
log?.('warning', `stat: lstat failed for "${requestedPath}": ${String(error)}`, 'stat');
|
|
90
|
+
stats = followedStats;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// isSymlink above is true whenever ANY ancestor is a symlink, because it
|
|
94
|
+
// compares the requested path with its fully-resolved real path. Only the
|
|
95
|
+
// entry's own lstat says whether THIS path is a link, so a regular file under
|
|
96
|
+
// a symlinked parent is reported as a file and no readlink is attempted.
|
|
97
|
+
const isOwnSymlink = stats.isSymbolicLink();
|
|
98
|
+
const symlinkTarget = isOwnSymlink
|
|
99
|
+
? await getSymlinkTarget(requestedPath, fs, signal, log)
|
|
100
|
+
: undefined;
|
|
101
|
+
return buildFileInfoResult(name, requestedPath, isOwnSymlink, stats, mimeType, symlinkTarget);
|
|
102
|
+
}
|
|
103
|
+
function classifyTypeCounts(results) {
|
|
104
|
+
let fileCount = 0;
|
|
105
|
+
let dirCount = 0;
|
|
106
|
+
for (const result of results) {
|
|
107
|
+
if ('error' in result)
|
|
108
|
+
continue;
|
|
109
|
+
if (result.value.type === 'directory')
|
|
110
|
+
dirCount += 1;
|
|
111
|
+
else
|
|
112
|
+
fileCount += 1;
|
|
113
|
+
}
|
|
114
|
+
return { fileCount, dirCount };
|
|
115
|
+
}
|
|
116
|
+
function toStatPerPathPayload(r) {
|
|
117
|
+
if ('error' in r) {
|
|
118
|
+
return { path: r.path, error: r.error };
|
|
119
|
+
}
|
|
120
|
+
const info = r.value;
|
|
121
|
+
return {
|
|
122
|
+
path: r.path,
|
|
123
|
+
value: {
|
|
124
|
+
name: info.name,
|
|
125
|
+
path: info.path,
|
|
126
|
+
type: info.type,
|
|
127
|
+
size: info.size,
|
|
128
|
+
...(info.tokenEstimate !== undefined ? { tokenEstimate: info.tokenEstimate } : {}),
|
|
129
|
+
created: info.created.toISOString(),
|
|
130
|
+
modified: info.modified.toISOString(),
|
|
131
|
+
accessed: info.accessed.toISOString(),
|
|
132
|
+
permissions: info.permissions,
|
|
133
|
+
isHidden: info.isHidden,
|
|
134
|
+
...(info.mimeType !== undefined ? { mimeType: info.mimeType } : {}),
|
|
135
|
+
...(info.symlinkTarget !== undefined ? { symlinkTarget: info.symlinkTarget } : {}),
|
|
65
136
|
},
|
|
66
|
-
}
|
|
137
|
+
};
|
|
67
138
|
}
|
|
139
|
+
export const GET_FILE_INFO = defineTool({
|
|
140
|
+
name: 'stat',
|
|
141
|
+
title: 'Get File Info',
|
|
142
|
+
description: 'Get metadata for one or more files or directories: size, type, permissions, MIME type, timestamps, and tokenEstimate. ' +
|
|
143
|
+
'Use tokenEstimate to pre-screen read cost before calling read. ' +
|
|
144
|
+
'Single path: pass path. Batch mode: pass paths[].',
|
|
145
|
+
input: StatInputSchema,
|
|
146
|
+
output: StatOutputSchema,
|
|
147
|
+
annotations: {
|
|
148
|
+
readOnlyHint: true,
|
|
149
|
+
idempotentHint: true,
|
|
150
|
+
destructiveHint: false,
|
|
151
|
+
openWorldHint: false,
|
|
152
|
+
},
|
|
153
|
+
timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS,
|
|
154
|
+
defaultErrorCode: ErrorCode.NOT_FOUND,
|
|
155
|
+
progress: (args) => {
|
|
156
|
+
if (args.paths !== undefined) {
|
|
157
|
+
return { label: 'Stat', subject: `${String(args.paths.length)} paths` };
|
|
158
|
+
}
|
|
159
|
+
return { label: 'Stat', subject: args.path ?? '' };
|
|
160
|
+
},
|
|
161
|
+
accessPaths: singleOrBatchAccessPaths,
|
|
162
|
+
run: async (args, ctx) => {
|
|
163
|
+
const batchInput = args.path !== undefined ? { path: args.path } : { paths: args.paths ?? [] };
|
|
164
|
+
const batch = await runOverPaths(batchInput, ctx, async ({ path }) => getFileInfo(path, {
|
|
165
|
+
includeMimeType: true,
|
|
166
|
+
fs: ctx.fs,
|
|
167
|
+
signal: ctx.signal,
|
|
168
|
+
log: ctx.log,
|
|
169
|
+
}), { defaultErrorCode: ErrorCode.NOT_FOUND });
|
|
170
|
+
const { fileCount, dirCount } = classifyTypeCounts(batch.results);
|
|
171
|
+
const perPathPayload = batch.results.map(toStatPerPathPayload);
|
|
172
|
+
let resourceUri;
|
|
173
|
+
const resources = [];
|
|
174
|
+
if (ctx.resourceStore && batch.summary.total > 1) {
|
|
175
|
+
const { entry, link } = putJsonResource(ctx.resourceStore, `${String(batch.summary.total)} paths`, perPathPayload);
|
|
176
|
+
resourceUri = entry.uri;
|
|
177
|
+
resources.push(link);
|
|
178
|
+
}
|
|
179
|
+
const text = perPathPayload
|
|
180
|
+
.map((r) => {
|
|
181
|
+
if (r.value) {
|
|
182
|
+
const { name, type, size } = r.value;
|
|
183
|
+
// A directory's own inode size says nothing about what is inside it
|
|
184
|
+
// (0 on Windows, 4096 on ext4), and "directory, 0 B" reads as empty.
|
|
185
|
+
// Use `list` for contents; report the byte size only where it means
|
|
186
|
+
// something.
|
|
187
|
+
if (type === 'directory')
|
|
188
|
+
return `${name}: directory`;
|
|
189
|
+
return `${name}: ${type}, ${formatBytes(size)}`;
|
|
190
|
+
}
|
|
191
|
+
return `${r.path}: ${r.error?.message ?? 'error'}`;
|
|
192
|
+
})
|
|
193
|
+
.join('\n');
|
|
194
|
+
return {
|
|
195
|
+
structured: {
|
|
196
|
+
results: perPathPayload,
|
|
197
|
+
summary: batch.summary,
|
|
198
|
+
fileCount,
|
|
199
|
+
dirCount,
|
|
200
|
+
...(resourceUri ? { resourceUri } : {}),
|
|
201
|
+
},
|
|
202
|
+
text,
|
|
203
|
+
...(resources.length > 0 ? { resources } : {}),
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
//# sourceMappingURL=stat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stat.js","sourceRoot":"","sources":["../../src/tools/stat.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ContentBlock } from '@modelcontextprotocol/server';\n\nimport { parse } from 'node:path';\n\nimport * as z from 'zod/v4';\n\nimport { ErrorCode, rethrowIfAborted } from '../core/errors.js';\nimport { formatBytes } from '../core/fmt.js';\nimport type { FileInfo, GuardedFileSystem, Stats } from '../core/fs.js';\nimport { getFileType, isHidden } from '../core/fs.js';\nimport { detectMimeType } from '../core/mime.js';\nimport {\n FileInfoSchema,\n NonNegInt,\n OperationSummarySchema,\n PerFileErrorSchema,\n singleOrBatchAccessPaths,\n singleOrBatchPathsInput,\n} from '../core/schema.js';\nimport { putJsonResource } from '../core/store.js';\nimport { DEFAULT_SEARCH_TIMEOUT_MS } from '../core/util.js';\nimport type { PerPathResult } from './batch.js';\nimport { runOverPaths } from './batch.js';\nimport type { ToolCtx } from './define.js';\nimport { defineTool } from './define.js';\n\nconst StatInputSchema = singleOrBatchPathsInput({ extra: {} });\n\nconst StatPerPathSchema = z.strictObject({\n path: z.string().describe('Requested path'),\n value: FileInfoSchema.optional().describe('File metadata; present on success'),\n error: PerFileErrorSchema.optional().describe('Error details; present on failure'),\n});\n\nconst StatOutputSchema = z.strictObject({\n results: z\n .array(StatPerPathSchema)\n .describe('Per-path metadata results ordered to match the input paths'),\n summary: OperationSummarySchema,\n fileCount: NonNegInt.optional().describe('Number of regular files in the results'),\n dirCount: NonNegInt.optional().describe('Number of directories in the results'),\n resourceUri: z\n .string()\n .optional()\n .describe(\n 'URI to aggregated stats.json in the resource store (present when resource store is available)',\n ),\n});\n\nfunction getPermissions(mode: number): string {\n let out = '';\n for (const shift of [6, 3, 0]) {\n const bits = (mode >> shift) & 0b111;\n out += (bits & 0b100 ? 'r' : '-') + (bits & 0b010 ? 'w' : '-') + (bits & 0b001 ? 'x' : '-');\n }\n return out;\n}\n\nfunction buildFileInfoResult(\n name: string,\n requestedPath: string,\n isSymlink: boolean,\n stats: Stats,\n mimeType: string | undefined,\n symlinkTarget: string | undefined,\n): FileInfo {\n const tokenEstimate = stats.isFile() ? Math.ceil(stats.size / 4) : undefined;\n return {\n name,\n path: requestedPath,\n type: isSymlink ? 'symlink' : getFileType(stats),\n size: stats.size,\n ...(tokenEstimate !== undefined ? { tokenEstimate } : {}),\n created: stats.birthtime,\n modified: stats.mtime,\n accessed: stats.atime,\n permissions: getPermissions(stats.mode),\n isHidden: isHidden(name),\n ...(mimeType !== undefined ? { mimeType } : {}),\n ...(symlinkTarget !== undefined ? { symlinkTarget } : {}),\n };\n}\n\nasync function getSymlinkTarget(\n pathToRead: string,\n fs: GuardedFileSystem,\n signal?: AbortSignal,\n log?: ToolCtx['log'],\n): Promise<string | undefined> {\n signal?.throwIfAborted();\n try {\n const { linkString } = await fs.readlink(pathToRead);\n return linkString;\n } catch (error) {\n rethrowIfAborted(error);\n log?.('warning', `stat: readlink failed for \"${pathToRead}\": ${String(error)}`, 'stat');\n return undefined;\n }\n}\n\ninterface FileInfoOptions {\n includeMimeType?: boolean | undefined;\n signal?: AbortSignal | undefined;\n onProgress?: (() => void) | undefined;\n fs: GuardedFileSystem;\n log?: ToolCtx['log'] | undefined;\n}\n\nasync function getFileInfo(filePath: string, options: FileInfoOptions): Promise<FileInfo> {\n const { signal, fs, log } = options;\n signal?.throwIfAborted();\n\n const {\n requestedPath,\n isSymlink,\n stats: followedStats,\n } = await fs.statDetailed(filePath, signal ? { signal } : undefined);\n\n const { base: name, ext: rawExt } = parse(requestedPath);\n const includeMimeType = options.includeMimeType !== false;\n const mimeType =\n includeMimeType && rawExt.length > 0 ? detectMimeType(requestedPath).mimeType : undefined;\n\n // For a symlink, fsStat follows the link and reports the target's metadata.\n // Report the link's own metadata instead so size/permissions/timestamps\n // describe the link the user asked about, not its target.\n let stats = followedStats;\n if (isSymlink) {\n try {\n ({ stats } = await fs.lstat(requestedPath, signal ? { signal } : undefined));\n } catch (error) {\n // A cancelled request is not a \"link metadata unavailable\" fallback.\n rethrowIfAborted(error);\n // Guarded lstat can be refused where raw lstat succeeded (e.g. an allowed\n // root that is itself a symlink, whose parent sits outside the roots).\n // Falling back to the followed stats degrades the report to the target's\n // metadata, so leave a trace instead of degrading silently.\n log?.('warning', `stat: lstat failed for \"${requestedPath}\": ${String(error)}`, 'stat');\n stats = followedStats;\n }\n }\n\n // isSymlink above is true whenever ANY ancestor is a symlink, because it\n // compares the requested path with its fully-resolved real path. Only the\n // entry's own lstat says whether THIS path is a link, so a regular file under\n // a symlinked parent is reported as a file and no readlink is attempted.\n const isOwnSymlink = stats.isSymbolicLink();\n const symlinkTarget = isOwnSymlink\n ? await getSymlinkTarget(requestedPath, fs, signal, log)\n : undefined;\n\n return buildFileInfoResult(name, requestedPath, isOwnSymlink, stats, mimeType, symlinkTarget);\n}\n\nfunction classifyTypeCounts(results: readonly PerPathResult<FileInfo>[]): {\n fileCount: number;\n dirCount: number;\n} {\n let fileCount = 0;\n let dirCount = 0;\n for (const result of results) {\n if ('error' in result) continue;\n if (result.value.type === 'directory') dirCount += 1;\n else fileCount += 1;\n }\n return { fileCount, dirCount };\n}\n\nfunction toStatPerPathPayload(r: PerPathResult<FileInfo>): z.infer<typeof StatPerPathSchema> {\n if ('error' in r) {\n return { path: r.path, error: r.error };\n }\n const info = r.value;\n return {\n path: r.path,\n value: {\n name: info.name,\n path: info.path,\n type: info.type,\n size: info.size,\n ...(info.tokenEstimate !== undefined ? { tokenEstimate: info.tokenEstimate } : {}),\n created: info.created.toISOString(),\n modified: info.modified.toISOString(),\n accessed: info.accessed.toISOString(),\n permissions: info.permissions,\n isHidden: info.isHidden,\n ...(info.mimeType !== undefined ? { mimeType: info.mimeType } : {}),\n ...(info.symlinkTarget !== undefined ? { symlinkTarget: info.symlinkTarget } : {}),\n },\n };\n}\n\nexport const GET_FILE_INFO = defineTool({\n name: 'stat',\n title: 'Get File Info',\n description:\n 'Get metadata for one or more files or directories: size, type, permissions, MIME type, timestamps, and tokenEstimate. ' +\n 'Use tokenEstimate to pre-screen read cost before calling read. ' +\n 'Single path: pass path. Batch mode: pass paths[].',\n input: StatInputSchema,\n output: StatOutputSchema,\n annotations: {\n readOnlyHint: true,\n idempotentHint: true,\n destructiveHint: false,\n openWorldHint: false,\n },\n timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS,\n defaultErrorCode: ErrorCode.NOT_FOUND,\n progress: (args) => {\n if (args.paths !== undefined) {\n return { label: 'Stat', subject: `${String(args.paths.length)} paths` };\n }\n return { label: 'Stat', subject: args.path ?? '' };\n },\n accessPaths: singleOrBatchAccessPaths,\n run: async (args, ctx) => {\n const batchInput = args.path !== undefined ? { path: args.path } : { paths: args.paths ?? [] };\n\n const batch = await runOverPaths<undefined, FileInfo>(\n batchInput,\n ctx,\n async ({ path }) =>\n getFileInfo(path, {\n includeMimeType: true,\n fs: ctx.fs,\n signal: ctx.signal,\n log: ctx.log,\n }),\n { defaultErrorCode: ErrorCode.NOT_FOUND },\n );\n\n const { fileCount, dirCount } = classifyTypeCounts(batch.results);\n const perPathPayload = batch.results.map(toStatPerPathPayload);\n\n let resourceUri: string | undefined;\n const resources: ContentBlock[] = [];\n if (ctx.resourceStore && batch.summary.total > 1) {\n const { entry, link } = putJsonResource(\n ctx.resourceStore,\n `${String(batch.summary.total)} paths`,\n perPathPayload,\n );\n resourceUri = entry.uri;\n resources.push(link);\n }\n\n const text = perPathPayload\n .map((r) => {\n if (r.value) {\n const { name, type, size } = r.value;\n // A directory's own inode size says nothing about what is inside it\n // (0 on Windows, 4096 on ext4), and \"directory, 0 B\" reads as empty.\n // Use `list` for contents; report the byte size only where it means\n // something.\n if (type === 'directory') return `${name}: directory`;\n return `${name}: ${type}, ${formatBytes(size)}`;\n }\n return `${r.path}: ${r.error?.message ?? 'error'}`;\n })\n .join('\\n');\n\n return {\n structured: {\n results: perPathPayload,\n summary: batch.summary,\n fileCount,\n dirCount,\n ...(resourceUri ? { resourceUri } : {}),\n },\n text,\n ...(resources.length > 0 ? { resources } : {}),\n };\n },\n});\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Server } from 'node:http';
|
|
2
|
+
import type { ServerOptions } from '../core/path.js';
|
|
3
|
+
import type { RuntimeConfig } from './shared.js';
|
|
4
|
+
export declare function startHttpServer(port: number, options: ServerOptions, config?: RuntimeConfig): Promise<Server>;
|
|
5
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/transport/http.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAUxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAuBrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAyOjD,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,aAAa,EACtB,MAAM,GAAE,aAAkB,GACzB,OAAO,CAAC,MAAM,CAAC,CAgIjB"}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// HTTP hosting: express app assembly, security middleware wiring, the modern
|
|
2
|
+
// per-request server factory, and listen-filter watcher gating on POST /mcp.
|
|
3
|
+
import { createMcpExpressApp } from '@modelcontextprotocol/express';
|
|
4
|
+
import { toNodeHandler } from '@modelcontextprotocol/node';
|
|
5
|
+
import { createMcpHandler, DEFAULT_REQUEST_TIMEOUT_MSEC, ProtocolErrorCode, } from '@modelcontextprotocol/server';
|
|
6
|
+
import { createServer as createHttpServer } from 'node:http';
|
|
7
|
+
import { formatUnknownErrorMessage } from '../core/errors.js';
|
|
8
|
+
import { assertFleetRequestStateKey } from '../core/input-required.js';
|
|
9
|
+
import { Logger } from '../core/observability.js';
|
|
10
|
+
import { PageSnapshotStore } from '../core/page-store.js';
|
|
11
|
+
import { PathGuard } from '../core/path.js';
|
|
12
|
+
import { parseTrueEnvFlag } from '../core/primitives.js';
|
|
13
|
+
import { ResourceStore } from '../core/store.js';
|
|
14
|
+
import { MIB, parseEnvInt } from '../core/util.js';
|
|
15
|
+
import { createWatcherRegistry, MAX_WATCHERS, } from '../core/watcher-registry.js';
|
|
16
|
+
import { assertHttpBindingPolicy, assertHttpHostPolicy, bearerAuthMiddleware, computeAllowedOriginHostnames, corsOriginMiddleware, corsPreflightHandler, createRateLimiter, protectedResourceUrl, resolveAllowedHosts, resolveTrustProxySetting, sendJsonRpcError, } from '../http-policy.js';
|
|
17
|
+
import { createServer } from '../server.js';
|
|
18
|
+
import { isStructurallyValidListen, jsonRpcRequestId, listenSubscriptionUris, prepareListenWatchers, } from './shared.js';
|
|
19
|
+
const MAX_REQUEST_BODY_BYTES = parseEnvInt('FS_MAX_REQUEST_BYTES', 4 * MIB, 1024, 256 * MIB);
|
|
20
|
+
/**
|
|
21
|
+
* The code the SDK's own non-POST rejection carries. Not a `ProtocolErrorCode`
|
|
22
|
+
* member — the enum has no `-32000` — so it is pinned here to keep the local
|
|
23
|
+
* 405 envelope identical to the one the handler would have produced.
|
|
24
|
+
*/
|
|
25
|
+
const SDK_METHOD_NOT_ALLOWED_CODE = -32000;
|
|
26
|
+
/** Body-parser rejections and anything else that reaches the end of the chain. */
|
|
27
|
+
function errorHandlerMiddleware(err, _req, res, next) {
|
|
28
|
+
if (res.headersSent) {
|
|
29
|
+
next(err);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (err.status === 413) {
|
|
33
|
+
sendJsonRpcError(res, 413, ProtocolErrorCode.InvalidRequest, 'Request body too large');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (err.status === 400) {
|
|
37
|
+
sendJsonRpcError(res, 400, ProtocolErrorCode.ParseError, 'Invalid JSON in request body');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
Logger.error('[HTTP] Unhandled middleware error:', formatUnknownErrorMessage(err));
|
|
41
|
+
sendJsonRpcError(res, 500, ProtocolErrorCode.InternalError, 'Internal Server Error');
|
|
42
|
+
}
|
|
43
|
+
function makeHttpModernFactory(options, getNotifier, sharedRegistry, sharedPathGuard, sharedStore, sharedPageStore, apiKey) {
|
|
44
|
+
return async ({ era }) => {
|
|
45
|
+
const notifier = getNotifier();
|
|
46
|
+
const c = await createServer(options, {
|
|
47
|
+
watcherRegistry: sharedRegistry,
|
|
48
|
+
notifier,
|
|
49
|
+
pathGuard: sharedPathGuard,
|
|
50
|
+
resourceStore: sharedStore,
|
|
51
|
+
pageStore: sharedPageStore,
|
|
52
|
+
era,
|
|
53
|
+
...(apiKey !== undefined ? { apiKey } : {}),
|
|
54
|
+
});
|
|
55
|
+
const previousOnClose = c.mcp.server.onclose;
|
|
56
|
+
c.mcp.server.onclose = () => {
|
|
57
|
+
previousOnClose?.();
|
|
58
|
+
c.disposeRuntimeState();
|
|
59
|
+
};
|
|
60
|
+
return c.mcp;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function setupExpressApp(httpHost, apiKey, allowedHosts, modernNodeHandler, sharedPathGuard, sharedRegistry, notifier) {
|
|
64
|
+
const allowedOriginHostnames = computeAllowedOriginHostnames(process.env['FS_ALLOWED_ORIGINS']);
|
|
65
|
+
// Read once here and passed down, like every other env-derived policy input:
|
|
66
|
+
// http-policy holds no state and reads no env of its own.
|
|
67
|
+
const publicUrl = process.env['FS_PUBLIC_URL'];
|
|
68
|
+
const app = createMcpExpressApp({
|
|
69
|
+
host: httpHost,
|
|
70
|
+
jsonLimit: `${MAX_REQUEST_BODY_BYTES}b`,
|
|
71
|
+
...(allowedHosts.length > 0 ? { allowedHosts: [...allowedHosts] } : {}),
|
|
72
|
+
...(allowedOriginHostnames.length > 0 ? { allowedOrigins: [...allowedOriginHostnames] } : {}),
|
|
73
|
+
});
|
|
74
|
+
const trustProxy = resolveTrustProxySetting(process.env['FS_TRUST_PROXY']);
|
|
75
|
+
if (trustProxy !== undefined) {
|
|
76
|
+
app.set('trust proxy', trustProxy);
|
|
77
|
+
}
|
|
78
|
+
if (allowedHosts.length === 0) {
|
|
79
|
+
Logger.warn('[HTTP] FS_ALLOW_UNRESTRICTED_HOSTS is set: binding globally without Host validation.');
|
|
80
|
+
}
|
|
81
|
+
app.options('/mcp', corsPreflightHandler(allowedOriginHostnames));
|
|
82
|
+
app.use('/mcp', corsOriginMiddleware(allowedOriginHostnames));
|
|
83
|
+
// Unconditional: the spec's rate-limit MUST is not scoped to authenticated
|
|
84
|
+
// binds. A keyless bind is loopback-only, so the cap is looser, not absent.
|
|
85
|
+
const rpm = parseEnvInt('FS_RATE_LIMIT_RPM', apiKey ? 120 : 6_000, 1, 100_000);
|
|
86
|
+
app.use('/mcp', createRateLimiter(rpm));
|
|
87
|
+
if (apiKey && allowedHosts.length > 0) {
|
|
88
|
+
const metadataHandler = (req, res) => {
|
|
89
|
+
const resource = protectedResourceUrl(req, true, publicUrl);
|
|
90
|
+
if (!resource) {
|
|
91
|
+
// Unreachable for the unrestricted case (the gate above excludes it),
|
|
92
|
+
// but defend a FS_PUBLIC_URL parse failure with a bodyless
|
|
93
|
+
// 404 — a generic OAuth client cannot parse a JSON-RPC error envelope.
|
|
94
|
+
res.status(404).end();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
98
|
+
res.status(200).json({
|
|
99
|
+
resource: resource.href,
|
|
100
|
+
bearer_methods_supported: ['header'],
|
|
101
|
+
resource_name: 'filesystem-mcp',
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
app.get(['/.well-known/oauth-protected-resource', '/.well-known/oauth-protected-resource/mcp'], metadataHandler);
|
|
105
|
+
}
|
|
106
|
+
app.get('/healthz', (_req, res) => {
|
|
107
|
+
res.status(200).json({
|
|
108
|
+
status: 'ok',
|
|
109
|
+
uptime: process.uptime(),
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
app.use('/mcp', bearerAuthMiddleware(apiKey, allowedHosts.length > 0, publicUrl));
|
|
113
|
+
const resourceUpdateSink = (uri) => {
|
|
114
|
+
notifier.resourceUpdated(uri);
|
|
115
|
+
};
|
|
116
|
+
app.post('/mcp', (req, res, next) => {
|
|
117
|
+
void (async () => {
|
|
118
|
+
const parsedBody = req.body;
|
|
119
|
+
// Only a structurally valid listen gets watchers, mirroring the stdio
|
|
120
|
+
// gate. A malformed one cannot succeed downstream, so attaching handles
|
|
121
|
+
// for it only gives the response-close release something to undo — and
|
|
122
|
+
// everything that is not a listen takes no watchers either way.
|
|
123
|
+
if (!isStructurallyValidListen(parsedBody)) {
|
|
124
|
+
await modernNodeHandler(req, res, parsedBody);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Reject an over-cap listen before the ack so the client does not believe
|
|
128
|
+
// every requested URI is watched when the watcher budget is exhausted.
|
|
129
|
+
// The per-URI `capped` failure below would also reject, but only after
|
|
130
|
+
// creating and tearing down every watcher up to the cap.
|
|
131
|
+
const requestedUris = listenSubscriptionUris(parsedBody);
|
|
132
|
+
// Only URIs without a live watcher consume a slot; a re-listen to an
|
|
133
|
+
// already-watched URI just retains another lease.
|
|
134
|
+
const newUris = requestedUris.filter((uri) => !sharedRegistry.hasWatcher(uri));
|
|
135
|
+
const available = MAX_WATCHERS - sharedRegistry.size();
|
|
136
|
+
if (newUris.length > available) {
|
|
137
|
+
sendJsonRpcError(res, 400, ProtocolErrorCode.InvalidParams, `subscriptions/listen names ${newUris.length} not-yet-watched URIs but only ${available} watcher slots remain (cap ${MAX_WATCHERS}). Reduce the resourceSubscriptions list.`, jsonRpcRequestId(parsedBody));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const prepared = await prepareListenWatchers(parsedBody, sharedPathGuard, sharedRegistry, resourceUpdateSink);
|
|
141
|
+
if (!prepared.ok) {
|
|
142
|
+
sendJsonRpcError(res, 400, ProtocolErrorCode.InvalidParams, prepared.message, jsonRpcRequestId(parsedBody));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
let released = false;
|
|
146
|
+
const release = () => {
|
|
147
|
+
if (released)
|
|
148
|
+
return;
|
|
149
|
+
released = true;
|
|
150
|
+
for (const uri of prepared.acquiredUris)
|
|
151
|
+
sharedRegistry.release(uri);
|
|
152
|
+
};
|
|
153
|
+
if (prepared.acquiredUris.length > 0) {
|
|
154
|
+
// The attach loop awaits per-URI path validation, so the client can be
|
|
155
|
+
// gone by the time it returns. `close` fires once: a listener attached
|
|
156
|
+
// after it would never run and every lease this batch took would leak
|
|
157
|
+
// until process shutdown.
|
|
158
|
+
if (res.closed || res.destroyed || res.writableEnded)
|
|
159
|
+
release();
|
|
160
|
+
else
|
|
161
|
+
res.once('close', release);
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
await modernNodeHandler(req, res, parsedBody);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
release();
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
})().catch(next);
|
|
171
|
+
});
|
|
172
|
+
// Answer non-POST here rather than handing it to `modernNodeHandler`. The
|
|
173
|
+
// SDK's own 405 is byte-identical to this one, but it omits the `Allow`
|
|
174
|
+
// header RFC 9110 §15.5.6 requires and routes every routine GET probe through
|
|
175
|
+
// the handler's `onerror` — which this server logs at error level.
|
|
176
|
+
app.all('/mcp', (_req, res) => {
|
|
177
|
+
sendJsonRpcError(res, 405, SDK_METHOD_NOT_ALLOWED_CODE, 'Method not allowed.', null, {
|
|
178
|
+
Allow: 'POST, OPTIONS',
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
app.use(errorHandlerMiddleware);
|
|
182
|
+
return app;
|
|
183
|
+
}
|
|
184
|
+
export async function startHttpServer(port, options, config = {}) {
|
|
185
|
+
const httpHost = config.httpHost ?? '127.0.0.1';
|
|
186
|
+
const { apiKey, eventBus } = config;
|
|
187
|
+
const fleet = config.deploymentMode === 'fleet';
|
|
188
|
+
assertHttpBindingPolicy(httpHost, apiKey);
|
|
189
|
+
if (fleet && !apiKey) {
|
|
190
|
+
throw new Error('Fleet deployment mode requires an API key.');
|
|
191
|
+
}
|
|
192
|
+
if (fleet && !eventBus) {
|
|
193
|
+
throw new Error('Fleet deployment mode requires a shared event bus.');
|
|
194
|
+
}
|
|
195
|
+
assertFleetRequestStateKey(fleet);
|
|
196
|
+
const allowedHosts = resolveAllowedHosts(httpHost, process.env['FS_ALLOWED_HOSTS']);
|
|
197
|
+
assertHttpHostPolicy(httpHost, allowedHosts, parseTrueEnvFlag(process.env['FS_ALLOW_UNRESTRICTED_HOSTS'], 'FS_ALLOW_UNRESTRICTED_HOSTS'));
|
|
198
|
+
const sharedRegistry = createWatcherRegistry();
|
|
199
|
+
// One store for the whole endpoint, same lifetime and same one-credential
|
|
200
|
+
// trust argument as the shared guard below: a result a tool externalized in
|
|
201
|
+
// one POST must survive to the follow-up resources/read, and the modern leg
|
|
202
|
+
// builds a fresh McpServer per request so a per-request store would discard
|
|
203
|
+
// it immediately.
|
|
204
|
+
// Fires only from a tool call, which is long after `modernHandler` below is
|
|
205
|
+
// constructed, so reading it from the closure is safe (same as `getNotifier`).
|
|
206
|
+
const sharedStore = new ResourceStore(() => {
|
|
207
|
+
modernHandler.notify.resourcesChanged();
|
|
208
|
+
});
|
|
209
|
+
const sharedPageStore = new PageSnapshotStore();
|
|
210
|
+
// One guard for the whole endpoint. The modern leg builds a fresh McpServer
|
|
211
|
+
// per request, so a per-instance guard would discard every accepted access
|
|
212
|
+
// grant the moment the request ended — re-prompting on each subsequent call
|
|
213
|
+
// and leaving the listen-watcher path validating against a stale allowed set.
|
|
214
|
+
// Grant scope is therefore the endpoint, not the connection: with API_KEY set
|
|
215
|
+
// every caller presents the same key (one auth context by construction), and
|
|
216
|
+
// without it the bind is loopback-only. Split into per-auth-context guards if
|
|
217
|
+
// this ever serves more than one credential.
|
|
218
|
+
const sharedPathGuard = new PathGuard(options, true);
|
|
219
|
+
await sharedPathGuard.recomputeAllowedDirectories();
|
|
220
|
+
const modernHandler = createMcpHandler(makeHttpModernFactory(options, () => modernHandler.notify, sharedRegistry, sharedPathGuard, sharedStore, sharedPageStore, apiKey), {
|
|
221
|
+
legacy: 'reject',
|
|
222
|
+
...(eventBus ? { bus: eventBus } : {}),
|
|
223
|
+
onerror: (error) => {
|
|
224
|
+
Logger.error('[HTTP] modern leg error:', formatUnknownErrorMessage(error));
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
const modernNodeHandler = toNodeHandler(modernHandler);
|
|
228
|
+
const app = setupExpressApp(httpHost, apiKey, allowedHosts, modernNodeHandler, sharedPathGuard, sharedRegistry, modernHandler.notify);
|
|
229
|
+
const httpServer = createHttpServer(app);
|
|
230
|
+
// Must exceed the idle timeout of any proxy in front of this server, or the
|
|
231
|
+
// proxy reuses connections the server already closed (intermittent 502s).
|
|
232
|
+
const keepAliveMs = parseEnvInt('FS_KEEPALIVE_TIMEOUT_MS', 5_000, 1_000, 600_000);
|
|
233
|
+
httpServer.keepAliveTimeout = keepAliveMs;
|
|
234
|
+
httpServer.headersTimeout = keepAliveMs + 5_000;
|
|
235
|
+
httpServer.requestTimeout = DEFAULT_REQUEST_TIMEOUT_MSEC;
|
|
236
|
+
const onHttpServerError = (error) => {
|
|
237
|
+
Logger.error('[HTTP] runtime server error', {
|
|
238
|
+
host: httpHost,
|
|
239
|
+
port,
|
|
240
|
+
error: formatUnknownErrorMessage(error),
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
httpServer.on('error', onHttpServerError);
|
|
244
|
+
const originalClose = httpServer.close.bind(httpServer);
|
|
245
|
+
httpServer.close = function (callback) {
|
|
246
|
+
sharedRegistry.destroy();
|
|
247
|
+
sharedPageStore.clear();
|
|
248
|
+
modernHandler
|
|
249
|
+
.close()
|
|
250
|
+
.then(() => {
|
|
251
|
+
originalClose(callback);
|
|
252
|
+
})
|
|
253
|
+
.catch((err) => {
|
|
254
|
+
Logger.error('[HTTP] Error closing handler during HTTP server close:', formatUnknownErrorMessage(err));
|
|
255
|
+
originalClose(callback);
|
|
256
|
+
});
|
|
257
|
+
return httpServer;
|
|
258
|
+
};
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
const onError = (err) => {
|
|
261
|
+
sharedRegistry.destroy();
|
|
262
|
+
sharedPageStore.clear();
|
|
263
|
+
modernHandler.close().catch((closeErr) => {
|
|
264
|
+
Logger.error('[HTTP] Error closing handler on startup failure:', formatUnknownErrorMessage(closeErr));
|
|
265
|
+
});
|
|
266
|
+
reject(err);
|
|
267
|
+
};
|
|
268
|
+
httpServer.once('error', onError);
|
|
269
|
+
httpServer.listen(port, httpHost, () => {
|
|
270
|
+
httpServer.removeListener('error', onError);
|
|
271
|
+
Logger.info(`[HTTP] Server listening on http://${httpHost}:${port}`);
|
|
272
|
+
resolve(httpServer);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=http.js.map
|