@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
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { getOAuthProtectedResourceMetadataUrl } from '@modelcontextprotocol/express';
|
|
2
|
+
import { JSONRPC_VERSION, localhostAllowedHostnames } from '@modelcontextprotocol/server';
|
|
3
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
4
|
+
import { ErrorCode, FsError } from './core/errors.js';
|
|
5
|
+
import { Logger } from './core/observability.js';
|
|
6
|
+
import { splitCsvList } from './core/util.js';
|
|
7
|
+
const MAX_BEARER_TOKEN_LENGTH = 4096;
|
|
8
|
+
/**
|
|
9
|
+
* JSON-RPC's server-defined error range. No SDK enum maps to "Unauthorized" or
|
|
10
|
+
* "Rate limit exceeded", both of which this module refuses before any handler
|
|
11
|
+
* has parsed a request.
|
|
12
|
+
*/
|
|
13
|
+
const JSONRPC_SERVER_ERROR = -32000;
|
|
14
|
+
/**
|
|
15
|
+
* The one JSON-RPC error envelope every HTTP refusal goes out in — this
|
|
16
|
+
* module's pre-handler rejections and `transport.ts`'s. It lives here because
|
|
17
|
+
* `transport.ts` already imports this module and not the other way round; a
|
|
18
|
+
* second hand-built literal is how the two drift apart.
|
|
19
|
+
*
|
|
20
|
+
* `headers` carries what a particular refusal owes the client on top of the
|
|
21
|
+
* JSON content type — `WWW-Authenticate` on a 401, `Retry-After` on a 429.
|
|
22
|
+
* Goes out through `res.json` so every refusal keeps the `charset=utf-8` and
|
|
23
|
+
* `Content-Length` a hand-rolled `writeHead`/`end` pair drops.
|
|
24
|
+
*/
|
|
25
|
+
export function sendJsonRpcError(res, status, code, message, id = null, headers = {}) {
|
|
26
|
+
res.status(status).set(headers).json({ jsonrpc: JSONRPC_VERSION, id, error: { code, message } });
|
|
27
|
+
}
|
|
28
|
+
const LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/u;
|
|
29
|
+
/**
|
|
30
|
+
* Pure HTTP auth and binding policy. Holds no state; all functions are
|
|
31
|
+
* directly testable without spinning up a server.
|
|
32
|
+
*/
|
|
33
|
+
export function isLoopbackHttpHost(host) {
|
|
34
|
+
const normalizedHost = host.trim().toLowerCase();
|
|
35
|
+
return (normalizedHost === '127.0.0.1' ||
|
|
36
|
+
normalizedHost === 'localhost' ||
|
|
37
|
+
normalizedHost === '[::1]' ||
|
|
38
|
+
normalizedHost === '::1');
|
|
39
|
+
}
|
|
40
|
+
export function isAllowedLocalhostOrigin(origin) {
|
|
41
|
+
return LOCALHOST_ORIGIN_RE.test(origin);
|
|
42
|
+
}
|
|
43
|
+
/** A bind to every interface. Clients never send `Host: 0.0.0.0`, so this bind
|
|
44
|
+
* cannot derive its allowed-host set from itself. */
|
|
45
|
+
function isWildcardHttpHost(host) {
|
|
46
|
+
return host === '0.0.0.0' || host === '::';
|
|
47
|
+
}
|
|
48
|
+
function originHostname(origin) {
|
|
49
|
+
try {
|
|
50
|
+
return new URL(origin).hostname;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* True if `origin` (a raw `Origin` request header) is allowed given the
|
|
58
|
+
* env-derived `allowedHostnames` set (hostname-form, no scheme/port). Localhost
|
|
59
|
+
* origins are always accepted via {@link isAllowedLocalhostOrigin}; a remote
|
|
60
|
+
* origin is accepted iff its parsed hostname is in the set. Both the SDK app's
|
|
61
|
+
* `allowedOrigins` and this OPTIONS-handler check consume hostname-form, so a
|
|
62
|
+
* remote origin allowed via `FS_ALLOWED_ORIGINS` is reflected
|
|
63
|
+
* end-to-end in `Access-Control-Allow-Origin`.
|
|
64
|
+
*/
|
|
65
|
+
export function isOriginAllowed(origin, allowedHostnames) {
|
|
66
|
+
if (isAllowedLocalhostOrigin(origin))
|
|
67
|
+
return true;
|
|
68
|
+
const host = originHostname(origin);
|
|
69
|
+
return host !== undefined && allowedHostnames.includes(host);
|
|
70
|
+
}
|
|
71
|
+
export function validateBearerAuthorization(apiKey, authHeader) {
|
|
72
|
+
const bearerPrefix = 'Bearer ';
|
|
73
|
+
if (typeof authHeader !== 'string' || !authHeader.startsWith(bearerPrefix)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const userKey = authHeader.slice(bearerPrefix.length).trim();
|
|
77
|
+
const normalizedApiKey = apiKey.trim();
|
|
78
|
+
if (userKey.length > MAX_BEARER_TOKEN_LENGTH || normalizedApiKey.length === 0) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
// Pure: hash per call. createHash is negligible next to the timingSafeEqual
|
|
82
|
+
// already done per request, and avoiding module-level cache state keeps this
|
|
83
|
+
// testable without post-import env-mutation footguns.
|
|
84
|
+
const expectedHash = createHash('sha256').update(normalizedApiKey).digest();
|
|
85
|
+
const actualHash = createHash('sha256').update(userKey).digest();
|
|
86
|
+
return timingSafeEqual(expectedHash, actualHash);
|
|
87
|
+
}
|
|
88
|
+
function isSecureApiKey(key) {
|
|
89
|
+
return typeof key === 'string' && key.trim().length >= 16;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Refuse to bind to a non-loopback host without an API key. Throws on
|
|
93
|
+
* policy violation; returns silently when allowed.
|
|
94
|
+
*/
|
|
95
|
+
export function assertHttpBindingPolicy(host, apiKey) {
|
|
96
|
+
if (isLoopbackHttpHost(host)) {
|
|
97
|
+
if (apiKey !== undefined && !isSecureApiKey(apiKey)) {
|
|
98
|
+
throw new FsError(ErrorCode.PERMISSION_DENIED, 'API_KEY is configured but is insecure (minimum 16 characters).');
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (isSecureApiKey(apiKey))
|
|
103
|
+
return;
|
|
104
|
+
throw new FsError(ErrorCode.PERMISSION_DENIED, `Refusing to bind HTTP server to non-loopback host '${host}' without a secure API_KEY (minimum 16 characters).`);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The Host header values this bind accepts. `FS_ALLOWED_HOSTS` wins
|
|
108
|
+
* when set; otherwise a loopback bind takes the whole localhost hostname set (a
|
|
109
|
+
* client dialing http://localhost:<port> sends `Host: localhost`, which a bare
|
|
110
|
+
* ['127.0.0.1'] list would 403), a wildcard bind takes none, and a concrete
|
|
111
|
+
* non-loopback bind takes itself. An empty result means no Host validation is
|
|
112
|
+
* mounted — only reachable under FS_ALLOW_UNRESTRICTED_HOSTS=1.
|
|
113
|
+
*/
|
|
114
|
+
export function resolveAllowedHosts(httpHost, allowedHostsEnv) {
|
|
115
|
+
const configured = splitCsvList(allowedHostsEnv);
|
|
116
|
+
if (configured.length > 0)
|
|
117
|
+
return configured;
|
|
118
|
+
if (isLoopbackHttpHost(httpHost))
|
|
119
|
+
return localhostAllowedHostnames();
|
|
120
|
+
if (isWildcardHttpHost(httpHost))
|
|
121
|
+
return [];
|
|
122
|
+
return [httpHost];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Express's `trust proxy` setting from `FS_TRUST_PROXY`. A
|
|
126
|
+
* non-negative integer hop count parses to a number (Express counts hops from
|
|
127
|
+
* the socket); anything else (a subnet name/expression, or a negative/
|
|
128
|
+
* non-integer string) passes through unchanged for Express to interpret.
|
|
129
|
+
* `undefined` means the env var was unset or empty — leave Express's default
|
|
130
|
+
* (disabled) in place.
|
|
131
|
+
*/
|
|
132
|
+
export function resolveTrustProxySetting(value) {
|
|
133
|
+
if (!value)
|
|
134
|
+
return undefined;
|
|
135
|
+
const hops = Number(value);
|
|
136
|
+
return Number.isInteger(hops) && hops >= 0 ? hops : value;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Refuse to bind a wildcard host (`0.0.0.0` / `::`) without an explicit
|
|
140
|
+
* `FS_ALLOWED_HOSTS` list. Clients never send `Host: 0.0.0.0`, so
|
|
141
|
+
* defaulting the allowed-host set to the wildcard string would reject all real
|
|
142
|
+
* traffic. Operators who accept the risk can set
|
|
143
|
+
* `FS_ALLOW_UNRESTRICTED_HOSTS=1` to restore warn-and-bind.
|
|
144
|
+
* Loopback and concrete non-loopback hosts are unaffected.
|
|
145
|
+
*/
|
|
146
|
+
export function assertHttpHostPolicy(host, allowedHosts, allowUnrestricted) {
|
|
147
|
+
if (isLoopbackHttpHost(host))
|
|
148
|
+
return;
|
|
149
|
+
// concrete non-loopback: Host validated against the bind host.
|
|
150
|
+
if (!isWildcardHttpHost(host))
|
|
151
|
+
return;
|
|
152
|
+
if (allowUnrestricted)
|
|
153
|
+
return;
|
|
154
|
+
if (allowedHosts.length > 0)
|
|
155
|
+
return;
|
|
156
|
+
throw new FsError(ErrorCode.PERMISSION_DENIED, `Refusing to bind wildcard host '${host}' without FS_ALLOWED_HOSTS. Set it to the public hostname(s) clients send, or set FS_ALLOW_UNRESTRICTED_HOSTS=true to accept the risk.`);
|
|
157
|
+
}
|
|
158
|
+
// ─── Protected-resource discovery (RFC 9728 / RFC 6750) ──────────────────────
|
|
159
|
+
/**
|
|
160
|
+
* This server is a resource server with no authorization server: `API_KEY` is a
|
|
161
|
+
* static secret the operator hands out of band, not an issued token. So the
|
|
162
|
+
* metadata document deliberately omits `authorization_servers` — RFC 9728 §2
|
|
163
|
+
* makes it optional, and its absence is the accurate statement that a token
|
|
164
|
+
* cannot be obtained from an endpoint. `mcpAuthMetadataRouter` from
|
|
165
|
+
* `@modelcontextprotocol/express` is the tool for the IdP-backed case: it
|
|
166
|
+
* requires RFC 8414 authorization-server metadata, and inventing an issuer with
|
|
167
|
+
* endpoints that answer nothing would send clients into a flow that cannot
|
|
168
|
+
* complete. Adopt it if this ever moves to a real IdP.
|
|
169
|
+
*
|
|
170
|
+
* What discovery buys here: a client hitting 401 learns the resource identifier
|
|
171
|
+
* and that the credential goes in the Authorization header, instead of a bare
|
|
172
|
+
* challenge plus a 404 on the well-known path.
|
|
173
|
+
*/
|
|
174
|
+
export function protectedResourceUrl(req, hostValidated, configured) {
|
|
175
|
+
if (configured) {
|
|
176
|
+
const parsed = URL.parse(configured);
|
|
177
|
+
if (parsed)
|
|
178
|
+
return parsed;
|
|
179
|
+
Logger.warn(`[HTTP] Ignoring unparseable FS_PUBLIC_URL: ${configured}. Deriving the resource identifier from the Host header instead.`);
|
|
180
|
+
}
|
|
181
|
+
// With no Host validator mounted (FS_ALLOW_UNRESTRICTED_HOSTS=1)
|
|
182
|
+
// the Host header is attacker-controlled, and naming a resource from it would
|
|
183
|
+
// publish an identifier the operator does not own. Answer with nothing instead.
|
|
184
|
+
if (!hostValidated)
|
|
185
|
+
return null;
|
|
186
|
+
const host = req.headers.host ?? '127.0.0.1';
|
|
187
|
+
return URL.parse(`${req.secure ? 'https' : 'http'}://${host}/mcp`);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* RFC 6750 §3: a request with no credentials gets a bare challenge, while one
|
|
191
|
+
* that presented something invalid gets `error="invalid_token"`. Clients use
|
|
192
|
+
* the difference to tell "you must authenticate" from "your token is wrong".
|
|
193
|
+
*/
|
|
194
|
+
function buildAuthChallenge(req, hasCredentials, hostValidated, publicUrl) {
|
|
195
|
+
const resource = protectedResourceUrl(req, hostValidated, publicUrl);
|
|
196
|
+
const params = [];
|
|
197
|
+
if (resource) {
|
|
198
|
+
params.push(`resource_metadata="${getOAuthProtectedResourceMetadataUrl(resource)}"`);
|
|
199
|
+
}
|
|
200
|
+
if (hasCredentials) {
|
|
201
|
+
params.push('error="invalid_token"', 'error_description="Invalid or expired bearer token"');
|
|
202
|
+
}
|
|
203
|
+
// A bare `Bearer` is a complete challenge; avoid emitting a trailing space.
|
|
204
|
+
return params.length > 0 ? `Bearer ${params.join(', ')}` : 'Bearer';
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Express middleware: when `apiKey` is set, require a matching bearer token.
|
|
208
|
+
* No key set = open access (loopback dev mode). `apiKey` is captured once per
|
|
209
|
+
* app setup (passed in from startHttpServer) so the middleware and
|
|
210
|
+
* assertHttpBindingPolicy share one source of truth.
|
|
211
|
+
*
|
|
212
|
+
* `requireBearerAuth` + `verifyAccessToken` from `@modelcontextprotocol/express`
|
|
213
|
+
* is the tool for verifier-backed tokens; this middleware stays hand-rolled
|
|
214
|
+
* because the credential is a static operator key with no verifier, the 401 body
|
|
215
|
+
* must stay a JSON-RPC envelope, and the RFC 6750 bare-vs-`invalid_token`
|
|
216
|
+
* challenge split is implemented here. Adopt `requireBearerAuth` if this ever
|
|
217
|
+
* verifies issued tokens.
|
|
218
|
+
*/
|
|
219
|
+
export function bearerAuthMiddleware(apiKey, hostValidated, publicUrl) {
|
|
220
|
+
return (req, res, next) => {
|
|
221
|
+
if (!apiKey) {
|
|
222
|
+
next();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const authHeader = req.headers.authorization;
|
|
226
|
+
if (isSecureApiKey(apiKey) && validateBearerAuthorization(apiKey, authHeader)) {
|
|
227
|
+
// Forward the validated caller to the SDK pipeline: toNodeHandler reads
|
|
228
|
+
// req.auth and passes it as the handler's pass-through authInfo, which the
|
|
229
|
+
// per-request factory receives and tool handlers read as ctx.http.authInfo.
|
|
230
|
+
// `expiresAt` is deliberately omitted: a static API key has no expiry and
|
|
231
|
+
// there is no `exp` claim or introspection response to read one from.
|
|
232
|
+
// Populate it from the token if this ever moves to issued tokens.
|
|
233
|
+
const presented = authHeader.slice('Bearer '.length).trim();
|
|
234
|
+
req.auth = {
|
|
235
|
+
token: presented,
|
|
236
|
+
clientId: 'api-key',
|
|
237
|
+
scopes: [],
|
|
238
|
+
};
|
|
239
|
+
next();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
sendJsonRpcError(res, 401, JSONRPC_SERVER_ERROR, 'Unauthorized', null, {
|
|
243
|
+
'WWW-Authenticate': buildAuthChallenge(req, req.headers.authorization !== undefined, hostValidated, publicUrl),
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Env-derived CORS origins (hostname-form, no scheme/port — matches the SDK
|
|
249
|
+
* app's `allowedOrigins` consumer). Defaults to the SDK's loopback hostname set
|
|
250
|
+
* so loopback browser clients keep working; operators set
|
|
251
|
+
* `FS_ALLOWED_ORIGINS` to allow remote clients on non-loopback
|
|
252
|
+
* binds. An empty value reads as unset, matching how parseAllowedHostsEnv
|
|
253
|
+
* treats an all-empty list. The same set is consulted by corsPreflightHandler
|
|
254
|
+
* so a remote origin is reflected end-to-end in Access-Control-Allow-Origin.
|
|
255
|
+
*/
|
|
256
|
+
export function computeAllowedOriginHostnames(originsEnv) {
|
|
257
|
+
// Truthiness, not list length: an all-blank value (" , ") is a configured but
|
|
258
|
+
// empty allow-list — deny every remote origin — while an unset or "" value
|
|
259
|
+
// falls back to the loopback defaults.
|
|
260
|
+
return originsEnv ? splitCsvList(originsEnv) : localhostAllowedHostnames();
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Reflect a present Origin on the response if it is allowed — localhost, or
|
|
264
|
+
* in the env-derived `FS_ALLOWED_ORIGINS` set — and avoid
|
|
265
|
+
* emitting a wildcard fallback. Shared by the OPTIONS preflight and the real
|
|
266
|
+
* POST response: `createMcpExpressApp`'s `allowedOrigins` only gates which
|
|
267
|
+
* Origins are accepted, it never sets `Access-Control-Allow-Origin` on the
|
|
268
|
+
* actual response, so without this a browser client would pass preflight and
|
|
269
|
+
* then have the POST response body blocked by CORS.
|
|
270
|
+
*/
|
|
271
|
+
function reflectAllowedOrigin(req, res, allowedOriginHostnames) {
|
|
272
|
+
const origin = req.headers.origin;
|
|
273
|
+
if (origin && isOriginAllowed(origin, allowedOriginHostnames)) {
|
|
274
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
275
|
+
// Key the response by Origin so a CDN/proxy caching one origin's response
|
|
276
|
+
// cannot replay it for a different origin (cache-poison).
|
|
277
|
+
res.header('Vary', 'Origin');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** Mounted ahead of the `/mcp` handlers so every response — not just the
|
|
281
|
+
* OPTIONS preflight — carries `Access-Control-Allow-Origin` for an allowed
|
|
282
|
+
* Origin. */
|
|
283
|
+
export function corsOriginMiddleware(allowedOriginHostnames) {
|
|
284
|
+
return (req, res, next) => {
|
|
285
|
+
reflectAllowedOrigin(req, res, allowedOriginHostnames);
|
|
286
|
+
next();
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* OPTIONS preflight for `/mcp`. The allow-list carries the SEP-2243 standard
|
|
291
|
+
* headers (`mcp-protocol-version`, `mcp-method`, `mcp-name`): SDK clients send
|
|
292
|
+
* all three on every modern request POST and `createMcpHandler` requires them
|
|
293
|
+
* (400 / -32020), so omitting them here would fail every browser preflight —
|
|
294
|
+
* and the HTTP leg is modern-only, so there is no legacy path to fall back to.
|
|
295
|
+
* `mcp-session-id` is deliberately absent: it is 2025-era only and this
|
|
296
|
+
* endpoint rejects legacy traffic.
|
|
297
|
+
*/
|
|
298
|
+
export function corsPreflightHandler(allowedOriginHostnames) {
|
|
299
|
+
return (req, res) => {
|
|
300
|
+
reflectAllowedOrigin(req, res, allowedOriginHostnames);
|
|
301
|
+
res.header('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
302
|
+
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, mcp-protocol-version, mcp-method, mcp-name');
|
|
303
|
+
res.status(204).end();
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
// Fixed 60s window; never tuned, so it is a constant rather than config plumbing.
|
|
307
|
+
const RATE_LIMIT_WINDOW_MS = 60_000;
|
|
308
|
+
/**
|
|
309
|
+
* Fixed-window per-client-IP rate limiter.
|
|
310
|
+
*/
|
|
311
|
+
export function createRateLimiter(max) {
|
|
312
|
+
const buckets = new Map();
|
|
313
|
+
const sweep = setInterval(() => {
|
|
314
|
+
const cutoff = Date.now() - RATE_LIMIT_WINDOW_MS * 2;
|
|
315
|
+
for (const [ip, bucket] of buckets) {
|
|
316
|
+
if (bucket.windowStart < cutoff)
|
|
317
|
+
buckets.delete(ip);
|
|
318
|
+
}
|
|
319
|
+
}, RATE_LIMIT_WINDOW_MS);
|
|
320
|
+
sweep.unref();
|
|
321
|
+
return (req, res, next) => {
|
|
322
|
+
const ip = req.ip ?? req.socket.remoteAddress ?? 'unknown';
|
|
323
|
+
const now = Date.now();
|
|
324
|
+
let bucket = buckets.get(ip);
|
|
325
|
+
if (!bucket || now - bucket.windowStart >= RATE_LIMIT_WINDOW_MS) {
|
|
326
|
+
bucket = { windowStart: now, count: 0 };
|
|
327
|
+
buckets.set(ip, bucket);
|
|
328
|
+
}
|
|
329
|
+
bucket.count += 1;
|
|
330
|
+
if (bucket.count <= max) {
|
|
331
|
+
next();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const retryAfterSec = Math.max(1, Math.ceil((bucket.windowStart + RATE_LIMIT_WINDOW_MS - now) / 1000));
|
|
335
|
+
sendJsonRpcError(res, 429, JSONRPC_SERVER_ERROR, 'Rate limit exceeded', null, {
|
|
336
|
+
'Retry-After': String(retryAfterSec),
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
//# sourceMappingURL=http-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-policy.js","sourceRoot":"","sources":["../src/http-policy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oCAAoC,EAAE,MAAM,+BAA+B,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,8BAA8B,CAAC;AAG1F,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAI1D,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAErC;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,CAAC,KAAK,CAAC;AAEpC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAa,EACb,MAAc,EACd,IAAY,EACZ,OAAe,EACf,KAA6B,IAAI,EACjC,UAAkC,EAAE;IAEpC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;AACnG,CAAC;AAED,MAAM,mBAAmB,GAAG,uDAAuD,CAAC;AAEpF;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACjD,OAAO,CACL,cAAc,KAAK,WAAW;QAC9B,cAAc,KAAK,WAAW;QAC9B,cAAc,KAAK,OAAO;QAC1B,cAAc,KAAK,KAAK,CACzB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,MAAc;IACrD,OAAO,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC1C,CAAC;AAED;qDACqD;AACrD,SAAS,kBAAkB,CAAC,IAAY;IACtC,OAAO,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,CAAC;AAC7C,CAAC;AAED,SAAS,cAAc,CAAC,MAAc;IACpC,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,gBAAmC;IACjF,IAAI,wBAAwB,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAClD,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IACpC,OAAO,IAAI,KAAK,SAAS,IAAI,gBAAgB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,2BAA2B,CACzC,MAAc,EACd,UAAmB;IAEnB,MAAM,YAAY,GAAG,SAAS,CAAC;IAC/B,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC3E,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7D,MAAM,gBAAgB,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IACvC,IAAI,OAAO,CAAC,MAAM,GAAG,uBAAuB,IAAI,gBAAgB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9E,OAAO,KAAK,CAAC;IACf,CAAC;IAED,4EAA4E;IAC5E,6EAA6E;IAC7E,sDAAsD;IACtD,MAAM,YAAY,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,MAAM,EAAE,CAAC;IAC5E,MAAM,UAAU,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;IACjE,OAAO,eAAe,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,cAAc,CAAC,GAAuB;IAC7C,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC;AAC5D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAY,EAAE,MAA0B;IAC9E,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,OAAO,CACf,SAAS,CAAC,iBAAiB,EAC3B,gEAAgE,CACjE,CAAC;QACJ,CAAC;QACD,OAAO;IACT,CAAC;IACD,IAAI,cAAc,CAAC,MAAM,CAAC;QAAE,OAAO;IACnC,MAAM,IAAI,OAAO,CACf,SAAS,CAAC,iBAAiB,EAC3B,sDAAsD,IAAI,qDAAqD,CAChH,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CACjC,QAAgB,EAChB,eAAmC;IAEnC,MAAM,UAAU,GAAG,YAAY,CAAC,eAAe,CAAC,CAAC;IACjD,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,UAAU,CAAC;IAC7C,IAAI,kBAAkB,CAAC,QAAQ,CAAC;QAAE,OAAO,yBAAyB,EAAE,CAAC;IACrE,IAAI,kBAAkB,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IAC5C,OAAO,CAAC,QAAQ,CAAC,CAAC;AACpB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAyB;IAChE,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3B,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;AAC5D,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAY,EACZ,YAA+B,EAC/B,iBAA0B;IAE1B,IAAI,kBAAkB,CAAC,IAAI,CAAC;QAAE,OAAO;IACrC,+DAA+D;IAC/D,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC;QAAE,OAAO;IACtC,IAAI,iBAAiB;QAAE,OAAO;IAC9B,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO;IACpC,MAAM,IAAI,OAAO,CACf,SAAS,CAAC,iBAAiB,EAC3B,mCAAmC,IAAI,wIAAwI,CAChL,CAAC;AACJ,CAAC;AAED,gFAAgF;AAEhF;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAAY,EACZ,aAAsB,EACtB,UAA8B;IAE9B,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACrC,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAC1B,MAAM,CAAC,IAAI,CACT,8CAA8C,UAAU,kEAAkE,CAC3H,CAAC;IACJ,CAAC;IACD,iEAAiE;IACjE,8EAA8E;IAC9E,gFAAgF;IAChF,IAAI,CAAC,aAAa;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,WAAW,CAAC;IAC7C,OAAO,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,MAAM,IAAI,MAAM,CAAC,CAAC;AACrE,CAAC;AAED;;;;GAIG;AACH,SAAS,kBAAkB,CACzB,GAAY,EACZ,cAAuB,EACvB,aAAsB,EACtB,SAA6B;IAE7B,MAAM,QAAQ,GAAG,oBAAoB,CAAC,GAAG,EAAE,aAAa,EAAE,SAAS,CAAC,CAAC;IACrE,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,sBAAsB,oCAAoC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IACvF,CAAC;IACD,IAAI,cAAc,EAAE,CAAC;QACnB,MAAM,CAAC,IAAI,CAAC,uBAAuB,EAAE,qDAAqD,CAAC,CAAC;IAC9F,CAAC;IACD,4EAA4E;IAC5E,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC;AACtE,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAA0B,EAC1B,aAAsB,EACtB,SAAkB;IAElB,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAQ,EAAE;QAC/D,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QACD,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;QAC7C,IAAI,cAAc,CAAC,MAAM,CAAC,IAAI,2BAA2B,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC;YAC9E,wEAAwE;YACxE,2EAA2E;YAC3E,4EAA4E;YAC5E,0EAA0E;YAC1E,sEAAsE;YACtE,kEAAkE;YAClE,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;YAC3D,GAAqC,CAAC,IAAI,GAAG;gBAC5C,KAAK,EAAE,SAAS;gBAChB,QAAQ,EAAE,SAAS;gBACnB,MAAM,EAAE,EAAE;aACX,CAAC;YACF,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QACD,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,oBAAoB,EAAE,cAAc,EAAE,IAAI,EAAE;YACrE,kBAAkB,EAAE,kBAAkB,CACpC,GAAG,EACH,GAAG,CAAC,OAAO,CAAC,aAAa,KAAK,SAAS,EACvC,aAAa,EACb,SAAS,CACV;SACF,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,6BAA6B,CAAC,UAA8B;IAC1E,8EAA8E;IAC9E,2EAA2E;IAC3E,uCAAuC;IACvC,OAAO,UAAU,CAAC,CAAC,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,yBAAyB,EAAE,CAAC;AAC7E,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,oBAAoB,CAC3B,GAAY,EACZ,GAAa,EACb,sBAAyC;IAEzC,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC;IAClC,IAAI,MAAM,IAAI,eAAe,CAAC,MAAM,EAAE,sBAAsB,CAAC,EAAE,CAAC;QAC9D,GAAG,CAAC,MAAM,CAAC,6BAA6B,EAAE,MAAM,CAAC,CAAC;QAClD,0EAA0E;QAC1E,0DAA0D;QAC1D,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC;AAED;;aAEa;AACb,MAAM,UAAU,oBAAoB,CAAC,sBAAyC;IAC5E,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAQ,EAAE;QAC/D,oBAAoB,CAAC,GAAG,EAAE,GAAG,EAAE,sBAAsB,CAAC,CAAC;QACvD,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,sBAAyC;IAC5E,OAAO,CAAC,GAAY,EAAE,GAAa,EAAQ,EAAE;QAC3C,oBAAoB,CAAC,GAAG,EAAE,GAAG,EAAE,sBAAsB,CAAC,CAAC;QACvD,GAAG,CAAC,MAAM,CAAC,8BAA8B,EAAE,eAAe,CAAC,CAAC;QAC5D,GAAG,CAAC,MAAM,CACR,8BAA8B,EAC9B,yEAAyE,CAC1E,CAAC;QACF,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IACxB,CAAC,CAAC;AACJ,CAAC;AASD,kFAAkF;AAClF,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAEpC;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC9C,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,oBAAoB,GAAG,CAAC,CAAC;QACrD,KAAK,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACnC,IAAI,MAAM,CAAC,WAAW,GAAG,MAAM;gBAAE,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACtD,CAAC;IACH,CAAC,EAAE,oBAAoB,CAAC,CAAC;IACzB,KAAK,CAAC,KAAK,EAAE,CAAC;IAEd,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAQ,EAAE;QAC/D,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,SAAS,CAAC;QAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7B,IAAI,CAAC,MAAM,IAAI,GAAG,GAAG,MAAM,CAAC,WAAW,IAAI,oBAAoB,EAAE,CAAC;YAChE,MAAM,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;YACxC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QAC1B,CAAC;QACD,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC;QAClB,IAAI,MAAM,CAAC,KAAK,IAAI,GAAG,EAAE,CAAC;YACxB,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QACD,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAC5B,CAAC,EACD,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,WAAW,GAAG,oBAAoB,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CACpE,CAAC;QACF,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,IAAI,EAAE;YAC5E,aAAa,EAAE,MAAM,CAAC,aAAa,CAAC;SACrC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { getOAuthProtectedResourceMetadataUrl } from '@modelcontextprotocol/express';\nimport { JSONRPC_VERSION, localhostAllowedHostnames } from '@modelcontextprotocol/server';\nimport type { AuthInfo } from '@modelcontextprotocol/server';\n\nimport { createHash, timingSafeEqual } from 'node:crypto';\n\nimport type { NextFunction, Request, RequestHandler, Response } from 'express';\n\nimport { ErrorCode, FsError } from './core/errors.js';\nimport { Logger } from './core/observability.js';\nimport { splitCsvList } from './core/util.js';\n\nconst MAX_BEARER_TOKEN_LENGTH = 4096;\n\n/**\n * JSON-RPC's server-defined error range. No SDK enum maps to \"Unauthorized\" or\n * \"Rate limit exceeded\", both of which this module refuses before any handler\n * has parsed a request.\n */\nconst JSONRPC_SERVER_ERROR = -32000;\n\n/**\n * The one JSON-RPC error envelope every HTTP refusal goes out in — this\n * module's pre-handler rejections and `transport.ts`'s. It lives here because\n * `transport.ts` already imports this module and not the other way round; a\n * second hand-built literal is how the two drift apart.\n *\n * `headers` carries what a particular refusal owes the client on top of the\n * JSON content type — `WWW-Authenticate` on a 401, `Retry-After` on a 429.\n * Goes out through `res.json` so every refusal keeps the `charset=utf-8` and\n * `Content-Length` a hand-rolled `writeHead`/`end` pair drops.\n */\nexport function sendJsonRpcError(\n res: Response,\n status: number,\n code: number,\n message: string,\n id: string | number | null = null,\n headers: Record<string, string> = {},\n): void {\n res.status(status).set(headers).json({ jsonrpc: JSONRPC_VERSION, id, error: { code, message } });\n}\n\nconst LOCALHOST_ORIGIN_RE = /^https?:\\/\\/(localhost|127\\.0\\.0\\.1|\\[::1\\])(:\\d+)?$/u;\n\n/**\n * Pure HTTP auth and binding policy. Holds no state; all functions are\n * directly testable without spinning up a server.\n */\nexport function isLoopbackHttpHost(host: string): boolean {\n const normalizedHost = host.trim().toLowerCase();\n return (\n normalizedHost === '127.0.0.1' ||\n normalizedHost === 'localhost' ||\n normalizedHost === '[::1]' ||\n normalizedHost === '::1'\n );\n}\n\nexport function isAllowedLocalhostOrigin(origin: string): boolean {\n return LOCALHOST_ORIGIN_RE.test(origin);\n}\n\n/** A bind to every interface. Clients never send `Host: 0.0.0.0`, so this bind\n * cannot derive its allowed-host set from itself. */\nfunction isWildcardHttpHost(host: string): boolean {\n return host === '0.0.0.0' || host === '::';\n}\n\nfunction originHostname(origin: string): string | undefined {\n try {\n return new URL(origin).hostname;\n } catch {\n return undefined;\n }\n}\n\n/**\n * True if `origin` (a raw `Origin` request header) is allowed given the\n * env-derived `allowedHostnames` set (hostname-form, no scheme/port). Localhost\n * origins are always accepted via {@link isAllowedLocalhostOrigin}; a remote\n * origin is accepted iff its parsed hostname is in the set. Both the SDK app's\n * `allowedOrigins` and this OPTIONS-handler check consume hostname-form, so a\n * remote origin allowed via `FS_ALLOWED_ORIGINS` is reflected\n * end-to-end in `Access-Control-Allow-Origin`.\n */\nexport function isOriginAllowed(origin: string, allowedHostnames: readonly string[]): boolean {\n if (isAllowedLocalhostOrigin(origin)) return true;\n const host = originHostname(origin);\n return host !== undefined && allowedHostnames.includes(host);\n}\n\nexport function validateBearerAuthorization(\n apiKey: string,\n authHeader: unknown,\n): authHeader is string {\n const bearerPrefix = 'Bearer ';\n if (typeof authHeader !== 'string' || !authHeader.startsWith(bearerPrefix)) {\n return false;\n }\n const userKey = authHeader.slice(bearerPrefix.length).trim();\n const normalizedApiKey = apiKey.trim();\n if (userKey.length > MAX_BEARER_TOKEN_LENGTH || normalizedApiKey.length === 0) {\n return false;\n }\n\n // Pure: hash per call. createHash is negligible next to the timingSafeEqual\n // already done per request, and avoiding module-level cache state keeps this\n // testable without post-import env-mutation footguns.\n const expectedHash = createHash('sha256').update(normalizedApiKey).digest();\n const actualHash = createHash('sha256').update(userKey).digest();\n return timingSafeEqual(expectedHash, actualHash);\n}\n\nfunction isSecureApiKey(key: string | undefined): boolean {\n return typeof key === 'string' && key.trim().length >= 16;\n}\n\n/**\n * Refuse to bind to a non-loopback host without an API key. Throws on\n * policy violation; returns silently when allowed.\n */\nexport function assertHttpBindingPolicy(host: string, apiKey: string | undefined): void {\n if (isLoopbackHttpHost(host)) {\n if (apiKey !== undefined && !isSecureApiKey(apiKey)) {\n throw new FsError(\n ErrorCode.PERMISSION_DENIED,\n 'API_KEY is configured but is insecure (minimum 16 characters).',\n );\n }\n return;\n }\n if (isSecureApiKey(apiKey)) return;\n throw new FsError(\n ErrorCode.PERMISSION_DENIED,\n `Refusing to bind HTTP server to non-loopback host '${host}' without a secure API_KEY (minimum 16 characters).`,\n );\n}\n\n/**\n * The Host header values this bind accepts. `FS_ALLOWED_HOSTS` wins\n * when set; otherwise a loopback bind takes the whole localhost hostname set (a\n * client dialing http://localhost:<port> sends `Host: localhost`, which a bare\n * ['127.0.0.1'] list would 403), a wildcard bind takes none, and a concrete\n * non-loopback bind takes itself. An empty result means no Host validation is\n * mounted — only reachable under FS_ALLOW_UNRESTRICTED_HOSTS=1.\n */\nexport function resolveAllowedHosts(\n httpHost: string,\n allowedHostsEnv: string | undefined,\n): string[] {\n const configured = splitCsvList(allowedHostsEnv);\n if (configured.length > 0) return configured;\n if (isLoopbackHttpHost(httpHost)) return localhostAllowedHostnames();\n if (isWildcardHttpHost(httpHost)) return [];\n return [httpHost];\n}\n\n/**\n * Express's `trust proxy` setting from `FS_TRUST_PROXY`. A\n * non-negative integer hop count parses to a number (Express counts hops from\n * the socket); anything else (a subnet name/expression, or a negative/\n * non-integer string) passes through unchanged for Express to interpret.\n * `undefined` means the env var was unset or empty — leave Express's default\n * (disabled) in place.\n */\nexport function resolveTrustProxySetting(value: string | undefined): number | string | undefined {\n if (!value) return undefined;\n const hops = Number(value);\n return Number.isInteger(hops) && hops >= 0 ? hops : value;\n}\n\n/**\n * Refuse to bind a wildcard host (`0.0.0.0` / `::`) without an explicit\n * `FS_ALLOWED_HOSTS` list. Clients never send `Host: 0.0.0.0`, so\n * defaulting the allowed-host set to the wildcard string would reject all real\n * traffic. Operators who accept the risk can set\n * `FS_ALLOW_UNRESTRICTED_HOSTS=1` to restore warn-and-bind.\n * Loopback and concrete non-loopback hosts are unaffected.\n */\nexport function assertHttpHostPolicy(\n host: string,\n allowedHosts: readonly string[],\n allowUnrestricted: boolean,\n): void {\n if (isLoopbackHttpHost(host)) return;\n // concrete non-loopback: Host validated against the bind host.\n if (!isWildcardHttpHost(host)) return;\n if (allowUnrestricted) return;\n if (allowedHosts.length > 0) return;\n throw new FsError(\n ErrorCode.PERMISSION_DENIED,\n `Refusing to bind wildcard host '${host}' without FS_ALLOWED_HOSTS. Set it to the public hostname(s) clients send, or set FS_ALLOW_UNRESTRICTED_HOSTS=true to accept the risk.`,\n );\n}\n\n// ─── Protected-resource discovery (RFC 9728 / RFC 6750) ──────────────────────\n\n/**\n * This server is a resource server with no authorization server: `API_KEY` is a\n * static secret the operator hands out of band, not an issued token. So the\n * metadata document deliberately omits `authorization_servers` — RFC 9728 §2\n * makes it optional, and its absence is the accurate statement that a token\n * cannot be obtained from an endpoint. `mcpAuthMetadataRouter` from\n * `@modelcontextprotocol/express` is the tool for the IdP-backed case: it\n * requires RFC 8414 authorization-server metadata, and inventing an issuer with\n * endpoints that answer nothing would send clients into a flow that cannot\n * complete. Adopt it if this ever moves to a real IdP.\n *\n * What discovery buys here: a client hitting 401 learns the resource identifier\n * and that the credential goes in the Authorization header, instead of a bare\n * challenge plus a 404 on the well-known path.\n */\nexport function protectedResourceUrl(\n req: Request,\n hostValidated: boolean,\n configured: string | undefined,\n): URL | null {\n if (configured) {\n const parsed = URL.parse(configured);\n if (parsed) return parsed;\n Logger.warn(\n `[HTTP] Ignoring unparseable FS_PUBLIC_URL: ${configured}. Deriving the resource identifier from the Host header instead.`,\n );\n }\n // With no Host validator mounted (FS_ALLOW_UNRESTRICTED_HOSTS=1)\n // the Host header is attacker-controlled, and naming a resource from it would\n // publish an identifier the operator does not own. Answer with nothing instead.\n if (!hostValidated) return null;\n const host = req.headers.host ?? '127.0.0.1';\n return URL.parse(`${req.secure ? 'https' : 'http'}://${host}/mcp`);\n}\n\n/**\n * RFC 6750 §3: a request with no credentials gets a bare challenge, while one\n * that presented something invalid gets `error=\"invalid_token\"`. Clients use\n * the difference to tell \"you must authenticate\" from \"your token is wrong\".\n */\nfunction buildAuthChallenge(\n req: Request,\n hasCredentials: boolean,\n hostValidated: boolean,\n publicUrl: string | undefined,\n): string {\n const resource = protectedResourceUrl(req, hostValidated, publicUrl);\n const params: string[] = [];\n if (resource) {\n params.push(`resource_metadata=\"${getOAuthProtectedResourceMetadataUrl(resource)}\"`);\n }\n if (hasCredentials) {\n params.push('error=\"invalid_token\"', 'error_description=\"Invalid or expired bearer token\"');\n }\n // A bare `Bearer` is a complete challenge; avoid emitting a trailing space.\n return params.length > 0 ? `Bearer ${params.join(', ')}` : 'Bearer';\n}\n\n/**\n * Express middleware: when `apiKey` is set, require a matching bearer token.\n * No key set = open access (loopback dev mode). `apiKey` is captured once per\n * app setup (passed in from startHttpServer) so the middleware and\n * assertHttpBindingPolicy share one source of truth.\n *\n * `requireBearerAuth` + `verifyAccessToken` from `@modelcontextprotocol/express`\n * is the tool for verifier-backed tokens; this middleware stays hand-rolled\n * because the credential is a static operator key with no verifier, the 401 body\n * must stay a JSON-RPC envelope, and the RFC 6750 bare-vs-`invalid_token`\n * challenge split is implemented here. Adopt `requireBearerAuth` if this ever\n * verifies issued tokens.\n */\nexport function bearerAuthMiddleware(\n apiKey: string | undefined,\n hostValidated: boolean,\n publicUrl?: string,\n): RequestHandler {\n return (req: Request, res: Response, next: NextFunction): void => {\n if (!apiKey) {\n next();\n return;\n }\n const authHeader = req.headers.authorization;\n if (isSecureApiKey(apiKey) && validateBearerAuthorization(apiKey, authHeader)) {\n // Forward the validated caller to the SDK pipeline: toNodeHandler reads\n // req.auth and passes it as the handler's pass-through authInfo, which the\n // per-request factory receives and tool handlers read as ctx.http.authInfo.\n // `expiresAt` is deliberately omitted: a static API key has no expiry and\n // there is no `exp` claim or introspection response to read one from.\n // Populate it from the token if this ever moves to issued tokens.\n const presented = authHeader.slice('Bearer '.length).trim();\n (req as Request & { auth?: AuthInfo }).auth = {\n token: presented,\n clientId: 'api-key',\n scopes: [],\n };\n next();\n return;\n }\n sendJsonRpcError(res, 401, JSONRPC_SERVER_ERROR, 'Unauthorized', null, {\n 'WWW-Authenticate': buildAuthChallenge(\n req,\n req.headers.authorization !== undefined,\n hostValidated,\n publicUrl,\n ),\n });\n };\n}\n\n/**\n * Env-derived CORS origins (hostname-form, no scheme/port — matches the SDK\n * app's `allowedOrigins` consumer). Defaults to the SDK's loopback hostname set\n * so loopback browser clients keep working; operators set\n * `FS_ALLOWED_ORIGINS` to allow remote clients on non-loopback\n * binds. An empty value reads as unset, matching how parseAllowedHostsEnv\n * treats an all-empty list. The same set is consulted by corsPreflightHandler\n * so a remote origin is reflected end-to-end in Access-Control-Allow-Origin.\n */\nexport function computeAllowedOriginHostnames(originsEnv: string | undefined): string[] {\n // Truthiness, not list length: an all-blank value (\" , \") is a configured but\n // empty allow-list — deny every remote origin — while an unset or \"\" value\n // falls back to the loopback defaults.\n return originsEnv ? splitCsvList(originsEnv) : localhostAllowedHostnames();\n}\n\n/**\n * Reflect a present Origin on the response if it is allowed — localhost, or\n * in the env-derived `FS_ALLOWED_ORIGINS` set — and avoid\n * emitting a wildcard fallback. Shared by the OPTIONS preflight and the real\n * POST response: `createMcpExpressApp`'s `allowedOrigins` only gates which\n * Origins are accepted, it never sets `Access-Control-Allow-Origin` on the\n * actual response, so without this a browser client would pass preflight and\n * then have the POST response body blocked by CORS.\n */\nfunction reflectAllowedOrigin(\n req: Request,\n res: Response,\n allowedOriginHostnames: readonly string[],\n): void {\n const origin = req.headers.origin;\n if (origin && isOriginAllowed(origin, allowedOriginHostnames)) {\n res.header('Access-Control-Allow-Origin', origin);\n // Key the response by Origin so a CDN/proxy caching one origin's response\n // cannot replay it for a different origin (cache-poison).\n res.header('Vary', 'Origin');\n }\n}\n\n/** Mounted ahead of the `/mcp` handlers so every response — not just the\n * OPTIONS preflight — carries `Access-Control-Allow-Origin` for an allowed\n * Origin. */\nexport function corsOriginMiddleware(allowedOriginHostnames: readonly string[]): RequestHandler {\n return (req: Request, res: Response, next: NextFunction): void => {\n reflectAllowedOrigin(req, res, allowedOriginHostnames);\n next();\n };\n}\n\n/**\n * OPTIONS preflight for `/mcp`. The allow-list carries the SEP-2243 standard\n * headers (`mcp-protocol-version`, `mcp-method`, `mcp-name`): SDK clients send\n * all three on every modern request POST and `createMcpHandler` requires them\n * (400 / -32020), so omitting them here would fail every browser preflight —\n * and the HTTP leg is modern-only, so there is no legacy path to fall back to.\n * `mcp-session-id` is deliberately absent: it is 2025-era only and this\n * endpoint rejects legacy traffic.\n */\nexport function corsPreflightHandler(allowedOriginHostnames: readonly string[]): RequestHandler {\n return (req: Request, res: Response): void => {\n reflectAllowedOrigin(req, res, allowedOriginHostnames);\n res.header('Access-Control-Allow-Methods', 'POST, OPTIONS');\n res.header(\n 'Access-Control-Allow-Headers',\n 'Content-Type, Authorization, mcp-protocol-version, mcp-method, mcp-name',\n );\n res.status(204).end();\n };\n}\n\n// ─── Rate limiting (public bind only) ────────────────────────────────────────\n\ninterface RateBucket {\n windowStart: number;\n count: number;\n}\n\n// Fixed 60s window; never tuned, so it is a constant rather than config plumbing.\nconst RATE_LIMIT_WINDOW_MS = 60_000;\n\n/**\n * Fixed-window per-client-IP rate limiter.\n */\nexport function createRateLimiter(max: number): RequestHandler {\n const buckets = new Map<string, RateBucket>();\n const sweep = setInterval(() => {\n const cutoff = Date.now() - RATE_LIMIT_WINDOW_MS * 2;\n for (const [ip, bucket] of buckets) {\n if (bucket.windowStart < cutoff) buckets.delete(ip);\n }\n }, RATE_LIMIT_WINDOW_MS);\n sweep.unref();\n\n return (req: Request, res: Response, next: NextFunction): void => {\n const ip = req.ip ?? req.socket.remoteAddress ?? 'unknown';\n const now = Date.now();\n let bucket = buckets.get(ip);\n if (!bucket || now - bucket.windowStart >= RATE_LIMIT_WINDOW_MS) {\n bucket = { windowStart: now, count: 0 };\n buckets.set(ip, bucket);\n }\n bucket.count += 1;\n if (bucket.count <= max) {\n next();\n return;\n }\n const retryAfterSec = Math.max(\n 1,\n Math.ceil((bucket.windowStart + RATE_LIMIT_WINDOW_MS - now) / 1000),\n );\n sendJsonRpcError(res, 429, JSONRPC_SERVER_ERROR, 'Rate limit exceeded', null, {\n 'Retry-After': String(retryAfterSec),\n });\n };\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from 'node:process';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
3
|
+
import * as z from 'zod/v4';
|
|
4
|
+
import { CliExitError, parseArgs, runPrintConfig } from './cli.js';
|
|
5
|
+
import { logRuntimeFailure } from './core/observability.js';
|
|
6
|
+
import { NO_POSITIONAL_ROOTS_GUIDANCE } from './instructions.js';
|
|
7
|
+
import { startHttpServer, startServer } from './transport.js';
|
|
8
|
+
z.config(z.locales.en());
|
|
9
9
|
const SHUTDOWN_TIMEOUT_MS = 5000;
|
|
10
|
-
let
|
|
10
|
+
let activeStdioHandle;
|
|
11
11
|
let activeHttpServer;
|
|
12
12
|
let shutdownStarted = false;
|
|
13
13
|
function isStdinEvent(event) {
|
|
@@ -21,32 +21,49 @@ function registerShutdownTrigger(event) {
|
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
async function shutdown(reason, exitCode = 0) {
|
|
24
|
-
if (shutdownStarted)
|
|
24
|
+
if (shutdownStarted) {
|
|
25
|
+
if (reason === 'SIGINT' || reason === 'SIGTERM') {
|
|
26
|
+
process.exit(130);
|
|
27
|
+
}
|
|
25
28
|
return;
|
|
29
|
+
}
|
|
26
30
|
shutdownStarted = true;
|
|
27
31
|
process.exitCode = exitCode;
|
|
28
32
|
let keepForceExitTimer = true;
|
|
29
33
|
const timer = setTimeout(() => {
|
|
30
|
-
|
|
34
|
+
logRuntimeFailure('shutdown_timeout', 'process', 'shutdown', `Shutdown timed out (${reason}), forcing exit.`);
|
|
31
35
|
process.exit(exitCode);
|
|
32
36
|
}, SHUTDOWN_TIMEOUT_MS);
|
|
33
37
|
timer.unref();
|
|
34
38
|
try {
|
|
35
39
|
if (activeHttpServer) {
|
|
36
40
|
const server = activeHttpServer;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
try {
|
|
42
|
+
await new Promise((resolve, reject) => {
|
|
43
|
+
server.close((err) => {
|
|
44
|
+
if (err)
|
|
45
|
+
reject(err);
|
|
46
|
+
else
|
|
47
|
+
resolve();
|
|
48
|
+
});
|
|
40
49
|
});
|
|
41
|
-
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
logRuntimeFailure('shutdown_http_error', 'process', 'shutdown', error);
|
|
53
|
+
}
|
|
42
54
|
}
|
|
43
|
-
if (
|
|
44
|
-
|
|
55
|
+
if (activeStdioHandle) {
|
|
56
|
+
try {
|
|
57
|
+
await activeStdioHandle.close();
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
logRuntimeFailure('shutdown_mcp_error', 'process', 'shutdown', error);
|
|
61
|
+
}
|
|
45
62
|
}
|
|
46
63
|
keepForceExitTimer = false;
|
|
47
64
|
}
|
|
48
65
|
catch (error) {
|
|
49
|
-
|
|
66
|
+
logRuntimeFailure('shutdown_error', 'process', 'shutdown', error);
|
|
50
67
|
}
|
|
51
68
|
finally {
|
|
52
69
|
if (!keepForceExitTimer) {
|
|
@@ -58,64 +75,83 @@ async function main() {
|
|
|
58
75
|
let allowedDirs;
|
|
59
76
|
let allowCwd;
|
|
60
77
|
let port;
|
|
78
|
+
let readOnly;
|
|
79
|
+
let printConfig;
|
|
80
|
+
let json;
|
|
81
|
+
let httpHost;
|
|
82
|
+
let cliApiKey;
|
|
61
83
|
try {
|
|
62
84
|
const parsed = await parseArgs();
|
|
63
|
-
|
|
85
|
+
allowedDirs = parsed.allowedDirs;
|
|
86
|
+
allowCwd = parsed.allowCwd;
|
|
87
|
+
port = parsed.port;
|
|
88
|
+
readOnly = parsed.readOnly;
|
|
89
|
+
printConfig = parsed.printConfig;
|
|
90
|
+
json = parsed.json;
|
|
91
|
+
httpHost = parsed.httpHost;
|
|
92
|
+
cliApiKey = parsed.apiKey;
|
|
64
93
|
}
|
|
65
94
|
catch (error) {
|
|
66
95
|
if (error instanceof CliExitError) {
|
|
67
96
|
if (error.message.length > 0) {
|
|
68
|
-
|
|
97
|
+
logRuntimeFailure('cli_exit', 'startup', 'parse_args', error.message);
|
|
69
98
|
}
|
|
70
99
|
process.exitCode = error.exitCode;
|
|
71
100
|
return;
|
|
72
101
|
}
|
|
73
102
|
throw error;
|
|
74
103
|
}
|
|
104
|
+
if (printConfig) {
|
|
105
|
+
const apiKey = cliApiKey;
|
|
106
|
+
await runPrintConfig({
|
|
107
|
+
allowedDirs,
|
|
108
|
+
allowCwd,
|
|
109
|
+
readOnly,
|
|
110
|
+
json,
|
|
111
|
+
...(port !== undefined ? { port } : {}),
|
|
112
|
+
...(httpHost !== undefined ? { httpHost } : {}),
|
|
113
|
+
...(apiKey !== undefined ? { apiKey } : {}),
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
75
117
|
if (allowedDirs.length > 0) {
|
|
76
|
-
const { signal, cleanup } = createTimedAbortSignal(undefined, DEFAULT_SEARCH_TIMEOUT_MS);
|
|
77
|
-
try {
|
|
78
|
-
await setAllowedDirectoriesResolved(allowedDirs, signal);
|
|
79
|
-
}
|
|
80
|
-
finally {
|
|
81
|
-
cleanup();
|
|
82
|
-
}
|
|
83
118
|
console.error('Allowed directories (from CLI):');
|
|
84
119
|
for (const dir of allowedDirs) {
|
|
85
120
|
console.error(`- ${dir}`);
|
|
86
121
|
}
|
|
87
122
|
}
|
|
88
123
|
else {
|
|
89
|
-
console.error(
|
|
124
|
+
console.error(NO_POSITIONAL_ROOTS_GUIDANCE);
|
|
125
|
+
}
|
|
126
|
+
if (readOnly) {
|
|
127
|
+
console.error('Read-only mode: mutating tools disabled.');
|
|
90
128
|
}
|
|
129
|
+
const serverOptions = { allowCwd, cliAllowedDirs: allowedDirs, readOnly };
|
|
130
|
+
const runtimeConfig = {
|
|
131
|
+
...(httpHost !== undefined ? { httpHost } : {}),
|
|
132
|
+
...(cliApiKey !== undefined ? { apiKey: cliApiKey } : {}),
|
|
133
|
+
};
|
|
91
134
|
if (port !== undefined) {
|
|
92
|
-
activeHttpServer = await startHttpServer(port,
|
|
93
|
-
allowCwd,
|
|
94
|
-
cliAllowedDirs: allowedDirs,
|
|
95
|
-
});
|
|
135
|
+
activeHttpServer = await startHttpServer(port, serverOptions, runtimeConfig);
|
|
96
136
|
}
|
|
97
137
|
else {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
});
|
|
102
|
-
activeServer = server;
|
|
103
|
-
await startServer(server);
|
|
138
|
+
registerShutdownTrigger('end');
|
|
139
|
+
registerShutdownTrigger('close');
|
|
140
|
+
activeStdioHandle = startServer(serverOptions, runtimeConfig);
|
|
104
141
|
}
|
|
105
142
|
}
|
|
106
143
|
registerShutdownTrigger('SIGTERM');
|
|
107
144
|
registerShutdownTrigger('SIGINT');
|
|
108
|
-
registerShutdownTrigger('end');
|
|
109
|
-
registerShutdownTrigger('close');
|
|
110
145
|
process.once('unhandledRejection', (reason) => {
|
|
111
|
-
|
|
146
|
+
logRuntimeFailure('unhandled_rejection', 'process', 'unhandledRejection', reason);
|
|
112
147
|
void shutdown('unhandledRejection', 1);
|
|
113
148
|
});
|
|
114
149
|
process.once('uncaughtException', (error) => {
|
|
115
|
-
|
|
150
|
+
logRuntimeFailure('uncaught_exception', 'process', 'uncaughtException', error);
|
|
116
151
|
void shutdown('uncaughtException', 1);
|
|
117
152
|
});
|
|
118
153
|
main().catch((error) => {
|
|
119
|
-
|
|
154
|
+
logRuntimeFailure('fatal', 'startup', 'main', error);
|
|
120
155
|
void shutdown('fatal', 1);
|
|
121
156
|
});
|
|
157
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n// Type-only imports are erased at runtime — safe to keep static.\nimport type { StdioServerHandle } from '@modelcontextprotocol/server/stdio';\n\nimport type * as http from 'node:http';\nimport process from 'node:process';\n\nimport * as z from 'zod/v4';\n\nimport { CliExitError, parseArgs, runPrintConfig } from './cli.js';\nimport { logRuntimeFailure } from './core/observability.js';\nimport { NO_POSITIONAL_ROOTS_GUIDANCE } from './instructions.js';\nimport { startHttpServer, startServer } from './transport.js';\n\nz.config(z.locales.en());\n\nconst SHUTDOWN_TIMEOUT_MS = 5000;\nlet activeStdioHandle: StdioServerHandle | undefined;\nlet activeHttpServer: http.Server | undefined;\nlet shutdownStarted = false;\n\nfunction isStdinEvent(event: NodeJS.Signals | 'end' | 'close'): boolean {\n return event === 'end' || event === 'close';\n}\n\nfunction registerShutdownTrigger(event: NodeJS.Signals | 'end' | 'close'): void {\n const target = isStdinEvent(event) ? process.stdin : process;\n target.once(event, () => {\n const reason = isStdinEvent(event) ? `stdin ${event}` : event;\n void shutdown(reason, 0);\n });\n}\n\nasync function shutdown(reason: string, exitCode = 0): Promise<void> {\n if (shutdownStarted) {\n if (reason === 'SIGINT' || reason === 'SIGTERM') {\n process.exit(130);\n }\n return;\n }\n shutdownStarted = true;\n\n process.exitCode = exitCode;\n let keepForceExitTimer = true;\n\n const timer = setTimeout(() => {\n logRuntimeFailure(\n 'shutdown_timeout',\n 'process',\n 'shutdown',\n `Shutdown timed out (${reason}), forcing exit.`,\n );\n process.exit(exitCode);\n }, SHUTDOWN_TIMEOUT_MS);\n timer.unref();\n\n try {\n if (activeHttpServer) {\n const server = activeHttpServer;\n try {\n await new Promise<void>((resolve, reject) => {\n server.close((err) => {\n if (err) reject(err);\n else resolve();\n });\n });\n } catch (error: unknown) {\n logRuntimeFailure('shutdown_http_error', 'process', 'shutdown', error);\n }\n }\n if (activeStdioHandle) {\n try {\n await activeStdioHandle.close();\n } catch (error: unknown) {\n logRuntimeFailure('shutdown_mcp_error', 'process', 'shutdown', error);\n }\n }\n keepForceExitTimer = false;\n } catch (error: unknown) {\n logRuntimeFailure('shutdown_error', 'process', 'shutdown', error);\n } finally {\n if (!keepForceExitTimer) {\n clearTimeout(timer);\n }\n }\n}\n\nasync function main(): Promise<void> {\n let allowedDirs: string[];\n let allowCwd: boolean;\n let port: number | undefined;\n let readOnly: boolean;\n let printConfig: boolean;\n let json: boolean;\n let httpHost: string | undefined;\n let cliApiKey: string | undefined;\n\n try {\n const parsed = await parseArgs();\n allowedDirs = parsed.allowedDirs;\n allowCwd = parsed.allowCwd;\n port = parsed.port;\n readOnly = parsed.readOnly;\n printConfig = parsed.printConfig;\n json = parsed.json;\n httpHost = parsed.httpHost;\n cliApiKey = parsed.apiKey;\n } catch (error: unknown) {\n if (error instanceof CliExitError) {\n if (error.message.length > 0) {\n logRuntimeFailure('cli_exit', 'startup', 'parse_args', error.message);\n }\n process.exitCode = error.exitCode;\n return;\n }\n throw error;\n }\n\n if (printConfig) {\n const apiKey = cliApiKey;\n await runPrintConfig({\n allowedDirs,\n allowCwd,\n readOnly,\n json,\n ...(port !== undefined ? { port } : {}),\n ...(httpHost !== undefined ? { httpHost } : {}),\n ...(apiKey !== undefined ? { apiKey } : {}),\n });\n return;\n }\n\n if (allowedDirs.length > 0) {\n console.error('Allowed directories (from CLI):');\n for (const dir of allowedDirs) {\n console.error(`- ${dir}`);\n }\n } else {\n console.error(NO_POSITIONAL_ROOTS_GUIDANCE);\n }\n\n if (readOnly) {\n console.error('Read-only mode: mutating tools disabled.');\n }\n\n const serverOptions = { allowCwd, cliAllowedDirs: allowedDirs, readOnly };\n const runtimeConfig = {\n ...(httpHost !== undefined ? { httpHost } : {}),\n ...(cliApiKey !== undefined ? { apiKey: cliApiKey } : {}),\n };\n\n if (port !== undefined) {\n activeHttpServer = await startHttpServer(port, serverOptions, runtimeConfig);\n } else {\n registerShutdownTrigger('end');\n registerShutdownTrigger('close');\n activeStdioHandle = startServer(serverOptions, runtimeConfig);\n }\n}\n\nregisterShutdownTrigger('SIGTERM');\nregisterShutdownTrigger('SIGINT');\n\nprocess.once('unhandledRejection', (reason: unknown) => {\n logRuntimeFailure('unhandled_rejection', 'process', 'unhandledRejection', reason);\n void shutdown('unhandledRejection', 1);\n});\n\nprocess.once('uncaughtException', (error: Error) => {\n logRuntimeFailure('uncaught_exception', 'process', 'uncaughtException', error);\n void shutdown('uncaughtException', 1);\n});\n\nmain().catch((error: unknown) => {\n logRuntimeFailure('fatal', 'startup', 'main', error);\n void shutdown('fatal', 1);\n});\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const INSTRUCTIONS_URI = "internal://instructions";
|
|
2
|
+
export declare const NO_POSITIONAL_ROOTS_GUIDANCE = "No positional directories specified. Configure roots with directory arguments, FS_ALLOWED_DIRS, or --allow-cwd. Modern clients with elicitation can also call a tool with a concrete path and approve the requested grant.";
|
|
3
|
+
/**
|
|
4
|
+
* The one statement of what this server is and how to approach it. The server's
|
|
5
|
+
* `instructions`, the `get-help` prompt, and the instructions resource all
|
|
6
|
+
* describe the same document, and three hand-maintained paraphrases of it drift
|
|
7
|
+
* against each other and cost the client the same sentence three times.
|
|
8
|
+
*/
|
|
9
|
+
export declare const INSTRUCTIONS_SUMMARY: string;
|
|
10
|
+
export declare function buildSectionsRecord(readOnly: boolean): Record<string, string>;
|
|
11
|
+
export declare function renderSections(sections: Record<string, string>): string;
|
|
12
|
+
//# sourceMappingURL=instructions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../src/instructions.ts"],"names":[],"mappings":"AAiCA,eAAO,MAAM,gBAAgB,4BAA4B,CAAC;AAC1D,eAAO,MAAM,4BAA4B,+NACqL,CAAC;AAE/N;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,QAEmF,CAAC;AAErH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAuC7E;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAEvE"}
|