@quantakrypto/mcp 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/HOSTING.md CHANGED
@@ -27,9 +27,24 @@ hosted endpoint is reachable by untrusted peers. Out of the box `node dist/http.
27
27
  gate. The knowledge tools (`explain_finding`, `suggest_hybrid`, `list_rules`)
28
28
  are pure and always exposed. The gating is a pure function (`gateHttpTools`),
29
29
  unit-tested in `test/http.test.ts`.
30
- - **Bounds each request.** A 1 MiB request-body cap (always), a per-request tool
31
- timeout (`QUANTAKRYPTO_MCP_TIMEOUT_MS`, default 30 s `504`) and a response-size cap
32
- (`QUANTAKRYPTO_MCP_MAX_RESPONSE_BYTES`, default 4 MiB `500`).
30
+ - **Confines the filesystem tools to a root allow-list.** When the FS tools are
31
+ enabled, every scanned path must resolve inside `QUANTAKRYPTO_MCP_ROOT`
32
+ (`:`-separated; defaults to the process CWD). `..` traversal and out-of-root
33
+ absolute paths are rejected (`resolveScanPath` in `src/fsconfig.ts`), so the
34
+ endpoint cannot be turned into an `/etc/passwd` read oracle.
35
+ - **Validates the request `Origin`.** `POST /mcp` rejects a browser request whose
36
+ `Origin` host is not loopback (or in `QUANTAKRYPTO_MCP_ALLOW_ORIGIN`) with `403`,
37
+ blocking DNS-rebinding / localhost-CSRF against the default no-token config.
38
+ Requests with no `Origin` (CLI / native MCP clients) are unaffected.
39
+ - **Bounds each request.** A 1 MiB request-body cap (`413` only on the cap; a
40
+ transport read error is `400`), a per-request tool timeout that **aborts the
41
+ in-flight scan** (`QUANTAKRYPTO_MCP_TIMEOUT_MS`, default 30 s → `504`; no
42
+ background work leaks past the response), a response-size cap
43
+ (`QUANTAKRYPTO_MCP_MAX_RESPONSE_BYTES`, default 4 MiB → `500`), and per-scan work
44
+ budgets (`QUANTAKRYPTO_MCP_MAX_FILES` / `QUANTAKRYPTO_MCP_MAX_BYTES`, each capped).
45
+ - **Sanitizes error messages.** Internal failures (an `ENOENT` carrying a server
46
+ path, a stack trace) are logged to stderr and replaced with a generic message
47
+ in the wire response, so a remote caller never learns server internals.
33
48
 
34
49
  | Env var | Default | Purpose |
35
50
  | --- | --- | --- |
@@ -37,8 +52,12 @@ hosted endpoint is reachable by untrusted peers. Out of the box `node dist/http.
37
52
  | `PORT` | `3000` | Listen port. |
38
53
  | `QUANTAKRYPTO_MCP_TOKEN` | _(unset)_ | When set, requires `Authorization: Bearer <token>`. |
39
54
  | `QUANTAKRYPTO_MCP_ALLOW_FS` | _(off)_ | `1`/`true` exposes the filesystem tools over HTTP. |
40
- | `QUANTAKRYPTO_MCP_TIMEOUT_MS` | `30000` | Per-request tool-execution deadline. |
55
+ | `QUANTAKRYPTO_MCP_ROOT` | _(cwd)_ | `:`-separated allow-list of directories the FS tools may scan. |
56
+ | `QUANTAKRYPTO_MCP_ALLOW_ORIGIN` | _(loopback)_ | Comma-separated extra `Origin` hosts allowed on `/mcp`. |
57
+ | `QUANTAKRYPTO_MCP_TIMEOUT_MS` | `30000` | Per-request deadline; aborts the in-flight scan on timeout. |
41
58
  | `QUANTAKRYPTO_MCP_MAX_RESPONSE_BYTES` | `4194304` | Response-body size cap. |
59
+ | `QUANTAKRYPTO_MCP_MAX_FILES` | `25000` (cap `250000`) | Max files a single scan may read. |
60
+ | `QUANTAKRYPTO_MCP_MAX_BYTES` | `268435456` (cap 2 GiB) | Max cumulative bytes a single scan may read. |
42
61
 
43
62
  **Design choice — refuse vs. warn on a wide-open bind.** Binding to a
44
63
  non-loopback host with `QUANTAKRYPTO_MCP_TOKEN` unset is **refused** (startup fails)
package/README.md CHANGED
@@ -160,9 +160,21 @@ is reachable by untrusted peers:
160
160
  only when `QUANTAKRYPTO_MCP_ALLOW_FS=1`. The knowledge tools (`explain_finding`,
161
161
  `suggest_hybrid`, `list_rules`) are always available. `tools/list` and
162
162
  `tools/call` both reflect the gating.
163
- - **Limits.** A 1 MiB request-body cap (always), a per-request tool timeout
164
- (`QUANTAKRYPTO_MCP_TIMEOUT_MS`, default 30000 `504` on timeout) and a response-size
165
- cap (`QUANTAKRYPTO_MCP_MAX_RESPONSE_BYTES`, default 4 MiB).
163
+ - **Filesystem tools are root-confined.** Even with `QUANTAKRYPTO_MCP_ALLOW_FS=1`,
164
+ every scanned path must resolve inside the `QUANTAKRYPTO_MCP_ROOT` allow-list
165
+ (`:`-separated; the process CWD by default). `..` traversal and out-of-root
166
+ absolute paths (e.g. `/etc/passwd`) are rejected — the server is not an
167
+ arbitrary-file-read oracle.
168
+ - **Origin validation.** `POST /mcp` rejects a browser request whose `Origin`
169
+ host is not loopback (or allow-listed via `QUANTAKRYPTO_MCP_ALLOW_ORIGIN`),
170
+ defending the default no-token loopback config against DNS-rebinding /
171
+ localhost-CSRF. Non-browser clients (no `Origin`) are unaffected.
172
+ - **Limits + work budgets.** A 1 MiB request-body cap (`413` only on the cap,
173
+ `400` on a transport error), a per-request tool timeout that **aborts the
174
+ underlying scan** (`QUANTAKRYPTO_MCP_TIMEOUT_MS`, default 30000 → `504`), a
175
+ response-size cap (`QUANTAKRYPTO_MCP_MAX_RESPONSE_BYTES`, default 4 MiB), and
176
+ per-scan work budgets (`QUANTAKRYPTO_MCP_MAX_FILES` / `QUANTAKRYPTO_MCP_MAX_BYTES`)
177
+ so a single call cannot exhaust host resources.
166
178
 
167
179
  | Env var | Default | Purpose |
168
180
  | --- | --- | --- |
@@ -170,8 +182,12 @@ is reachable by untrusted peers:
170
182
  | `PORT` | `3000` | Listen port. |
171
183
  | `QUANTAKRYPTO_MCP_TOKEN` | _(unset)_ | When set, requires `Authorization: Bearer <token>`. |
172
184
  | `QUANTAKRYPTO_MCP_ALLOW_FS` | _(off)_ | `1`/`true` exposes the filesystem tools over HTTP. |
173
- | `QUANTAKRYPTO_MCP_TIMEOUT_MS` | `30000` | Per-request tool-execution deadline. |
185
+ | `QUANTAKRYPTO_MCP_ROOT` | _(cwd)_ | `:`-separated allow-list of directories the FS tools may scan. |
186
+ | `QUANTAKRYPTO_MCP_ALLOW_ORIGIN` | _(loopback)_ | Comma-separated extra `Origin` hosts allowed on `/mcp`. |
187
+ | `QUANTAKRYPTO_MCP_TIMEOUT_MS` | `30000` | Per-request deadline; aborts the in-flight scan on timeout. |
174
188
  | `QUANTAKRYPTO_MCP_MAX_RESPONSE_BYTES` | `4194304` | Response-body size cap. |
189
+ | `QUANTAKRYPTO_MCP_MAX_FILES` | `25000` (cap `250000`) | Max files a single scan may read. |
190
+ | `QUANTAKRYPTO_MCP_MAX_BYTES` | `268435456` (cap 2 GiB) | Max cumulative bytes a single scan may read. |
175
191
 
176
192
  ```bash
177
193
  # Local, knowledge tools only (default safe posture)
@@ -225,3 +241,9 @@ streams) — no process spawning.
225
241
  ## License
226
242
 
227
243
  Apache-2.0
244
+
245
+ ## Support & training
246
+
247
+ Questions, commercial support, or post-quantum readiness training for your team —
248
+ visit **[quantakrypto.com](https://quantakrypto.com)** or email
249
+ **[hello@quantakrypto.com](mailto:hello@quantakrypto.com)**.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Filesystem-tool safety policy for the quantakrypto MCP (P0 — FS confinement
3
+ * and work budgets).
4
+ *
5
+ * The FS-backed tools (`scan_path`, `inventory_crypto`, `generate_cbom`) pass a
6
+ * caller-supplied `path` straight into `@quantakrypto/core`'s `scan()`. With the
7
+ * filesystem tools enabled (`QUANTAKRYPTO_MCP_ALLOW_FS=1`) that turns the server
8
+ * into an arbitrary-file-read oracle: `path` could be `/etc/passwd` or any tree
9
+ * on the host (SECURITY.md warns about exactly this). This module turns that
10
+ * warning into enforced policy:
11
+ *
12
+ * - **Root allow-list** (`QUANTAKRYPTO_MCP_ROOT`, `:`-separated): every scanned
13
+ * path must resolve inside one of the configured roots. When unset, the
14
+ * process CWD is the single implicit root.
15
+ * - **`..` traversal rejection**: a resolved path that escapes every root is
16
+ * refused, even if the literal string contained no `..`.
17
+ * - **Work budgets** (`maxFiles` / `maxBytes`): bounded by default and capped,
18
+ * so a single `scan_path` cannot exhaust host resources.
19
+ *
20
+ * Everything here is a pure function of an env snapshot + the requested path so
21
+ * it is fully unit-testable; only {@link resolveFsConfig} reads `process.env`.
22
+ */
23
+ /** Minimal env shape so the resolver stays pure and testable. */
24
+ export type FsEnv = Record<string, string | undefined>;
25
+ /** Default file-count budget for a single FS tool call. */
26
+ export declare const DEFAULT_MAX_FILES = 25000;
27
+ /** Hard cap on the file-count budget, even when raised via env. */
28
+ export declare const MAX_MAX_FILES = 250000;
29
+ /** Default cumulative-bytes budget for a single FS tool call (256 MiB). */
30
+ export declare const DEFAULT_MAX_BYTES: number;
31
+ /** Hard cap on the cumulative-bytes budget, even when raised via env (2 GiB). */
32
+ export declare const MAX_MAX_BYTES: number;
33
+ /** Resolved FS-tool policy: where scans may read and how much work they may do. */
34
+ export interface FsConfig {
35
+ /**
36
+ * Absolute, normalized roots a scan may read inside. Always non-empty; defaults
37
+ * to `[process.cwd()]` when `QUANTAKRYPTO_MCP_ROOT` is unset.
38
+ */
39
+ roots: string[];
40
+ /** Max files a single scan may read before {@link BudgetExceededError}. */
41
+ maxFiles: number;
42
+ /** Max cumulative bytes a single scan may read before {@link BudgetExceededError}. */
43
+ maxBytes: number;
44
+ }
45
+ /**
46
+ * Resolve the FS-tool policy from an env snapshot. Pure aside from the supplied
47
+ * `cwd` default (so callers/tests can pin it).
48
+ */
49
+ export declare function resolveFsConfig(env: FsEnv, cwd?: string): FsConfig;
50
+ /** Outcome of validating a requested path against the root allow-list. */
51
+ export type PathDecision = {
52
+ ok: true;
53
+ path: string;
54
+ } | {
55
+ ok: false;
56
+ reason: string;
57
+ };
58
+ /**
59
+ * Validate and resolve a caller-supplied scan path against the allow-list.
60
+ *
61
+ * Relative paths are resolved against the FIRST configured root (the primary
62
+ * scan root), not the process CWD, so a relative request can never reach outside
63
+ * the allow-list. Absolute paths must already sit inside a configured root. Any
64
+ * path that resolves outside every root — whether via an absolute path or a
65
+ * `..` traversal — is rejected. Pure: does no filesystem I/O (it does not follow
66
+ * symlinks; the allow-list is the trust boundary).
67
+ */
68
+ export declare function resolveScanPath(config: FsConfig, requested: string): PathDecision;
69
+ //# sourceMappingURL=fsconfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fsconfig.d.ts","sourceRoot":"","sources":["../src/fsconfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH,iEAAiE;AACjE,MAAM,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEvD,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,QAAS,CAAC;AAExC,mEAAmE;AACnE,eAAO,MAAM,aAAa,SAAU,CAAC;AAErC,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,QAAoB,CAAC;AAEnD,iFAAiF;AACjF,eAAO,MAAM,aAAa,QAAyB,CAAC;AAEpD,mFAAmF;AACnF,MAAM,WAAW,QAAQ;IACvB;;;OAGG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,QAAQ,EAAE,MAAM,CAAC;CAClB;AA8BD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,GAAE,MAAsB,GAAG,QAAQ,CAMjF;AAED,0EAA0E;AAC1E,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAUtF;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CAsBjF"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Filesystem-tool safety policy for the quantakrypto MCP (P0 — FS confinement
3
+ * and work budgets).
4
+ *
5
+ * The FS-backed tools (`scan_path`, `inventory_crypto`, `generate_cbom`) pass a
6
+ * caller-supplied `path` straight into `@quantakrypto/core`'s `scan()`. With the
7
+ * filesystem tools enabled (`QUANTAKRYPTO_MCP_ALLOW_FS=1`) that turns the server
8
+ * into an arbitrary-file-read oracle: `path` could be `/etc/passwd` or any tree
9
+ * on the host (SECURITY.md warns about exactly this). This module turns that
10
+ * warning into enforced policy:
11
+ *
12
+ * - **Root allow-list** (`QUANTAKRYPTO_MCP_ROOT`, `:`-separated): every scanned
13
+ * path must resolve inside one of the configured roots. When unset, the
14
+ * process CWD is the single implicit root.
15
+ * - **`..` traversal rejection**: a resolved path that escapes every root is
16
+ * refused, even if the literal string contained no `..`.
17
+ * - **Work budgets** (`maxFiles` / `maxBytes`): bounded by default and capped,
18
+ * so a single `scan_path` cannot exhaust host resources.
19
+ *
20
+ * Everything here is a pure function of an env snapshot + the requested path so
21
+ * it is fully unit-testable; only {@link resolveFsConfig} reads `process.env`.
22
+ */
23
+ import * as path from "node:path";
24
+ import process from "node:process";
25
+ /** Default file-count budget for a single FS tool call. */
26
+ export const DEFAULT_MAX_FILES = 25_000;
27
+ /** Hard cap on the file-count budget, even when raised via env. */
28
+ export const MAX_MAX_FILES = 250_000;
29
+ /** Default cumulative-bytes budget for a single FS tool call (256 MiB). */
30
+ export const DEFAULT_MAX_BYTES = 256 * 1024 * 1024;
31
+ /** Hard cap on the cumulative-bytes budget, even when raised via env (2 GiB). */
32
+ export const MAX_MAX_BYTES = 2 * 1024 * 1024 * 1024;
33
+ /** Parse a positive integer from an env string, clamped to `[1, cap]`. */
34
+ function toBudget(value, fallback, cap) {
35
+ if (value === undefined)
36
+ return Math.min(fallback, cap);
37
+ const n = Number(value);
38
+ if (!Number.isFinite(n) || n <= 0)
39
+ return Math.min(fallback, cap);
40
+ return Math.min(Math.floor(n), cap);
41
+ }
42
+ /** Split the `:`-separated root allow-list, dropping empty segments. */
43
+ function parseRoots(value, cwd) {
44
+ const raw = (value ?? "")
45
+ .split(path.delimiter)
46
+ .map((s) => s.trim())
47
+ .filter((s) => s.length > 0);
48
+ const list = raw.length > 0 ? raw : [cwd];
49
+ // Normalize to absolute, de-duplicated roots.
50
+ const seen = new Set();
51
+ const roots = [];
52
+ for (const r of list) {
53
+ const abs = path.resolve(cwd, r);
54
+ if (!seen.has(abs)) {
55
+ seen.add(abs);
56
+ roots.push(abs);
57
+ }
58
+ }
59
+ return roots;
60
+ }
61
+ /**
62
+ * Resolve the FS-tool policy from an env snapshot. Pure aside from the supplied
63
+ * `cwd` default (so callers/tests can pin it).
64
+ */
65
+ export function resolveFsConfig(env, cwd = process.cwd()) {
66
+ return {
67
+ roots: parseRoots(env.QUANTAKRYPTO_MCP_ROOT, cwd),
68
+ maxFiles: toBudget(env.QUANTAKRYPTO_MCP_MAX_FILES, DEFAULT_MAX_FILES, MAX_MAX_FILES),
69
+ maxBytes: toBudget(env.QUANTAKRYPTO_MCP_MAX_BYTES, DEFAULT_MAX_BYTES, MAX_MAX_BYTES),
70
+ };
71
+ }
72
+ /** True when `child` is `root` itself or lives beneath it (no `..` escape). */
73
+ function isInsideRoot(child, root) {
74
+ if (child === root)
75
+ return true;
76
+ const rel = path.relative(root, child);
77
+ // `rel` starting with ".." (or being absolute) means `child` escapes `root`.
78
+ return rel.length > 0 && !rel.startsWith("..") && !path.isAbsolute(rel);
79
+ }
80
+ /**
81
+ * Validate and resolve a caller-supplied scan path against the allow-list.
82
+ *
83
+ * Relative paths are resolved against the FIRST configured root (the primary
84
+ * scan root), not the process CWD, so a relative request can never reach outside
85
+ * the allow-list. Absolute paths must already sit inside a configured root. Any
86
+ * path that resolves outside every root — whether via an absolute path or a
87
+ * `..` traversal — is rejected. Pure: does no filesystem I/O (it does not follow
88
+ * symlinks; the allow-list is the trust boundary).
89
+ */
90
+ export function resolveScanPath(config, requested) {
91
+ const trimmed = requested.trim();
92
+ if (trimmed.length === 0) {
93
+ return { ok: false, reason: "path must be a non-empty string" };
94
+ }
95
+ const primaryRoot = config.roots[0];
96
+ const resolved = path.isAbsolute(trimmed)
97
+ ? path.resolve(trimmed)
98
+ : path.resolve(primaryRoot, trimmed);
99
+ for (const root of config.roots) {
100
+ if (isInsideRoot(resolved, root)) {
101
+ return { ok: true, path: resolved };
102
+ }
103
+ }
104
+ return {
105
+ ok: false,
106
+ reason: "path is outside the configured scan root(s). " +
107
+ "Set QUANTAKRYPTO_MCP_ROOT to allow-list the directories the MCP may scan.",
108
+ };
109
+ }
110
+ //# sourceMappingURL=fsconfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fsconfig.js","sourceRoot":"","sources":["../src/fsconfig.ts"],"names":[],"mappings":"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"}
package/dist/http.d.ts CHANGED
@@ -67,6 +67,11 @@ export interface HttpConfig {
67
67
  maxResponseBytes: number;
68
68
  /** True when the host is a loopback interface (safe without auth). */
69
69
  loopback: boolean;
70
+ /**
71
+ * Extra origin hosts to allow on `POST /mcp` (lower-cased hostnames), beyond
72
+ * the implicit loopback set and the bind host. From QUANTAKRYPTO_MCP_ALLOW_ORIGIN.
73
+ */
74
+ allowedOrigins: string[];
70
75
  }
71
76
  /**
72
77
  * Resolve the HTTP transport config from env + explicit overrides. Pure: does
@@ -84,6 +89,27 @@ export declare function startupDecision(config: HttpConfig): {
84
89
  ok: boolean;
85
90
  reason?: string;
86
91
  };
92
+ /**
93
+ * Decide whether a request's `Origin` / `Host` is acceptable, to defend the
94
+ * default no-token loopback config against DNS-rebinding and localhost-CSRF: a
95
+ * malicious web page can POST to `http://127.0.0.1:<port>/mcp`, but the browser
96
+ * stamps a foreign `Origin` we can reject. Pure and testable.
97
+ *
98
+ * Policy:
99
+ * - No `Origin` header (curl, a native MCP client, same-origin GET) → allow;
100
+ * the header is a browser artifact and absence is not an attack signal here.
101
+ * - An `Origin` present → its host must be a configured loopback host (or a
102
+ * host explicitly added to the allow-list). A foreign origin is rejected.
103
+ * - When the server itself binds a non-loopback interface, a token is already
104
+ * mandatory (see {@link startupDecision}); the loopback allow-list still
105
+ * applies but auth is the primary control there.
106
+ */
107
+ export declare function originDecision(allowedHosts: ReadonlySet<string>, originHeader: string | undefined): {
108
+ ok: boolean;
109
+ reason?: string;
110
+ };
111
+ /** The hosts an `Origin` may name. Loopback by default, plus the bind host. */
112
+ export declare function allowedOriginHosts(config: HttpConfig): Set<string>;
87
113
  /** A request-authorization outcome. */
88
114
  export interface AuthDecision {
89
115
  authorized: boolean;
@@ -97,6 +123,15 @@ export interface AuthDecision {
97
123
  * allowed (the loopback / trusted-edge case). Pure and testable.
98
124
  */
99
125
  export declare function authorizeRequest(token: string, authorizationHeader: string | undefined): AuthDecision;
126
+ /**
127
+ * Constant-time string compare. Both inputs are hashed with the same algorithm
128
+ * to fixed-length digests before {@link timingSafeEqual}, so the comparison runs
129
+ * over equal-length buffers and the early length-mismatch return (which leaked
130
+ * the configured token's length) is gone. The hashing is a domain-separation /
131
+ * length-equalization step, not a secrecy measure: `timingSafeEqual` still does
132
+ * the constant-time work and rejects unequal digests.
133
+ */
134
+ export declare function timingSafeEqualStr(a: string, b: string): boolean;
100
135
  /**
101
136
  * Select the tools to expose over HTTP for a given policy. The knowledge tools
102
137
  * are always returned; the filesystem tools are included only when
@@ -104,6 +139,20 @@ export declare function authorizeRequest(token: string, authorizationHeader: str
104
139
  * for HTTP tool gating, so tools/list and tools/call stay consistent.
105
140
  */
106
141
  export declare function gateHttpTools(tools: readonly ToolDefinition[], allowFs: boolean): ToolDefinition[];
142
+ /**
143
+ * Error thrown by {@link readBody} when the request body exceeds the size cap.
144
+ * Distinguished from a transport/I/O error so the caller can map it to HTTP 413
145
+ * specifically, while genuine read failures map to 400/500 (M1).
146
+ */
147
+ export declare class BodyTooLargeError extends Error {
148
+ readonly name = "BodyTooLargeError";
149
+ constructor(message?: string);
150
+ }
151
+ /** Error raised by {@link withTimeout} when the request deadline elapses. */
152
+ export declare class RequestTimeoutError extends Error {
153
+ readonly name = "RequestTimeoutError";
154
+ constructor(message?: string);
155
+ }
107
156
  /**
108
157
  * Build (but do not start) the HTTP server wrapping an {@link McpServer}.
109
158
  * Exposed for testing and for embedding in a larger process. The `config`
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAGH,OAAO,KAAK,EAAmB,MAAM,EAAkB,MAAM,WAAW,CAAC;AAQzE,OAAO,KAAK,EAAmB,cAAc,EAAE,MAAM,eAAe,CAAC;AAErE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,8EAA8E;AAC9E,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAc/C,MAAM,WAAW,iBAAiB;IAChC,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uFAAuF;IACvF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sFAAsF;IACtF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wFAAwF;IACxF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uFAAuF;IACvF,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAMD,uEAAuE;AACvE,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEzD,wDAAwD;AACxD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,iBAAsB,GAAG,UAAU,CAoB3F;AASD,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG;IACnD,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAUA;AAED,uCAAuC;AACvC,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,mBAAmB,EAAE,MAAM,GAAG,SAAS,GACtC,YAAY,CAYd;AAUD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,SAAS,cAAc,EAAE,EAChC,OAAO,EAAE,OAAO,GACf,cAAc,EAAE,CAGlB;AA4DD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,MAAM,CAW9E;AAyGD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAEjE;AAED,6DAA6D;AAC7D,wBAAgB,eAAe,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CA+BhF"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}