@oomkapwn/enquire-mcp 2.5.0 → 2.6.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/CHANGELOG.md CHANGED
@@ -2,6 +2,68 @@
2
2
 
3
3
  All notable changes to this project will be documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## [2.6.0] — 2026-05-08
6
+
7
+ **Sprint 6 — remote-MCP HTTP transport.** New `serve-http` subcommand running the same server (same tools, same vault, same hybrid retrieval) over [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http) — the protocol Claude.ai web, ChatGPT, Cursor's HTTP mode, and most mobile MCP clients use to talk to a remote server. **No other Obsidian-MCP currently ships a remote-HTTP transport.**
8
+
9
+ ### Added — `enquire-mcp serve-http`
10
+
11
+ Stateless [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http) with three layers in front of the SDK transport:
12
+
13
+ 1. **Bearer auth** — required at startup (fail-closed, refuses to bind without `--bearer-token` ≥16 chars). Constant-time compare via SHA-256 + `crypto.timingSafeEqual` on equal-length buffers — no length-leak oracle. Token never appears in logs (rate-limit key is the SHA-256 prefix).
14
+ 2. **Per-token rate-limit** — sliding 60-second window, default 120 req/min, tunable via `--rate-limit` (`0` disables). 429 + `Retry-After: 60` on overflow.
15
+ 3. **Strict CORS allowlist** — `--cors-origin` (repeatable). Default empty (no `Access-Control-Allow-Origin` sent — same-origin still works). Disallowed origins get 204 preflight with no CORS headers, browsers refuse the actual request. `*` is supported but warned-against (incompatible with credentialed Bearer requests).
16
+
17
+ Plus an unauthenticated `/health` probe (`GET → 200 ok`) for tunnels/uptime monitors.
18
+
19
+ The HTTP server uses **stateless mode** — fresh `McpServer` per request over the **shared** vault + FTS5 + embedding handles. SQLite stays open across thousands of requests; only the per-request server class is recreated. This matters because `prepareServerDeps()` (vault open + FTS5 sync) takes seconds on a real vault, while `buildMcpServer()` (registering tool handlers) is sub-millisecond.
20
+
21
+ ### Added — `enquire-mcp gen-token`
22
+
23
+ Convenience helper that prints a fresh 32-byte base64url bearer token (256 bits of entropy, URL/header-safe). Equivalent to `node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"` but discoverable in `--help`.
24
+
25
+ ### Added — `--bearer-token-env <name>`
26
+
27
+ Read the bearer token from an env var instead of a flag. Cleaner for systemd / `.env` / shared shells where flags would leak via `ps aux` or shell history.
28
+
29
+ ### Added — comprehensive deployment docs
30
+
31
+ [`docs/http-transport.md`](docs/http-transport.md) — security model, threat model, all flags, five deployment recipes (Tailscale Funnel, Cloudflare Tunnel, ngrok, direct-LAN, systemd), client configuration for Claude.ai web / Cursor HTTP / ChatGPT custom GPT / Khoj mobile, troubleshooting, manual `curl` examples.
32
+
33
+ ### Refactored — extracted `prepareServerDeps()` + `buildMcpServer()` from `startServer()`
34
+
35
+ Stdio and HTTP now share the same dependency-prep + server-build code. Stdio calls `buildMcpServer()` once; HTTP calls it per request over the same `ServerDeps`. Skip-tool warnings (`--disabled-tools "foo" did not match any tool`) print only on the first build via a single-fire latch — HTTP doesn't spam logs per request. `formatReadyBanner()` is shared so the runtime configuration summary is identical regardless of transport.
36
+
37
+ ### Added — 26 new unit tests + 6 smoke checks
38
+
39
+ `tests/http-transport.test.ts` (26 tests):
40
+ - `verifyBearer` — missing/wrong/right token, case-sensitive Bearer prefix, length-leak resistance, rate-limit-key stability/uniqueness.
41
+ - `RateLimiter` — under-budget passes, over-budget rejects, sliding window trims old entries, per-key isolation, `perMinute=0` disables.
42
+ - `generateBearerToken` — 43-char base64url shape, uniqueness across 100 tokens.
43
+ - `startHttpServer` end-to-end — 401 missing/wrong, 200 init, 405 GET, 200 `/health`, 404 unknown paths, 429 rate-limit, OPTIONS preflight (allowed/disallowed origin), refuses startup without `--bearer-token` or with `<16 chars`.
44
+
45
+ `scripts/smoke.mjs` — added an HTTP smoke variant that spawns `serve-http` on port 0, hits `/health` unauthenticated, verifies 401 on missing-bearer, completes an authenticated initialize, then cleans up.
46
+
47
+ **Total: 457 unit tests pass** (was 431 in v2.5.0). All previous tests preserved unchanged.
48
+
49
+ ### Tool / prompt surface
50
+
51
+ **No change.** All 36 tools + 17 prompts work identically over HTTP. The transport is a wrapper, not a new feature surface.
52
+
53
+ ### Migration
54
+
55
+ **No-op.** All existing `serve` users keep working unchanged. New `serve-http` subcommand is opt-in. The internal refactor (extracting `prepareServerDeps` / `buildMcpServer`) preserved every previous behavior — verified by all 431 prior tests passing on the new code path.
56
+
57
+ ### Verified
58
+
59
+ - Maintainer's 128-note bilingual real vault: stdio + HTTP smoke variants both green.
60
+ - 457 / 457 tests on every required CI matrix node.
61
+ - Zero new prod dependencies — uses `node:http` directly (no Express).
62
+
63
+ ### Note on stateful sessions / SSE
64
+
65
+ Stateful `Mcp-Session-Id` sessions with persistent SSE streams are tracked for **v2.7+** if there's demand. Stateless is the right default for our tools (search, read, frontmatter ops are all short-running) and avoids the persistence-aware shutdown complexity.
66
+
5
67
  ## [2.5.0] — 2026-05-08
6
68
 
7
69
  **Sprint 5 — agentic prompts (Khoj parity, lite scope).** Two new MCP prompts that bring named-persona retrieval and scheduled-query automation to enquire-mcp. Pure orchestration over existing tools — no new server-side state, no LLM calls.
package/README.md CHANGED
@@ -42,8 +42,9 @@ That's it. Your AI now has structured access to wikilinks, backlinks, frontmatte
42
42
  | Privacy filter (`--exclude-glob` / `--read-paths`) | ❌ | n/a | ✅ verified at search + write paths |
43
43
  | Standalone (no Obsidian plugin) | varies | ❌ requires Obsidian | ✅ direct vault read |
44
44
  | MCP-native (any agent) | varies | ❌ Obsidian-only | ✅ stdio JSON-RPC |
45
+ | **Remote MCP (HTTP transport, bearer auth)** | ❌ | ❌ | ✅ **only here** (v2.6.0) |
45
46
  | SLSA-3 provenance | ❌ | n/a | ✅ |
46
- | Test suite | rare | n/a | ✅ 408 unit tests |
47
+ | Test suite | rare | n/a | ✅ 457 unit tests |
47
48
 
48
49
  ---
49
50
 
@@ -68,6 +69,10 @@ graph LR
68
69
  Tier 1: serve --vault <path> → TF-IDF (zero setup, instant)
69
70
  Tier 2: serve --vault <path> --persistent-index → + BM25 (sub-100ms top-10)
70
71
  Tier 3: + install-model + build-embeddings → + ML embeddings (multilingual)
72
+ Tier 4: serve-http --bearer-token <token> → remote MCP (v2.6.0)
73
+ same retrieval stack, exposed over HTTP for Claude.ai web,
74
+ ChatGPT, Cursor HTTP, mobile. Tailscale Funnel / Cloudflare
75
+ Tunnel for HTTPS. See docs/http-transport.md.
71
76
  ```
72
77
 
73
78
  ---
@@ -104,6 +109,20 @@ enquire-mcp build-embeddings --vault <path> # ~30ms/chunk on M1
104
109
  # Add --persistent-index to your serve invocation for BM25.
105
110
  ```
106
111
 
112
+ **Remote MCP** (v2.6.0 — Claude.ai web, ChatGPT, Cursor HTTP, mobile):
113
+
114
+ ```bash
115
+ enquire-mcp gen-token > ~/.enquire/token # 256-bit bearer
116
+ enquire-mcp serve-http \
117
+ --vault ~/Obsidian \
118
+ --bearer-token "$(cat ~/.enquire/token)" \
119
+ --persistent-index
120
+ # Front with Tailscale Funnel / Cloudflare Tunnel for HTTPS — see
121
+ # docs/http-transport.md.
122
+ ```
123
+
124
+ No other Obsidian-MCP currently ships a remote-HTTP transport. Same vault, same tools, same hybrid retrieval — just over HTTPS instead of stdio.
125
+
107
126
  ---
108
127
 
109
128
  ## Tools (36 total)
@@ -182,9 +201,9 @@ Plus **2 + 1 opt-in MCP resources** (`obsidian://note/...`, `obsidian://vault-in
182
201
  | `--max-file-bytes <n>` | 5 MB | Per-file read/write cap. |
183
202
  | `--cache-size <n>` | 1024 | LRU cap for parsed-note cache. |
184
203
 
185
- Subcommands: `serve` · `clear-cache` · `clear-index` · `index` (cold-build FTS5) · `install-model` · `build-embeddings` · `clear-embeddings`.
204
+ Subcommands: `serve` · `serve-http` (v2.6.0 — remote MCP) · `gen-token` (v2.6.0) · `clear-cache` · `clear-index` · `index` (cold-build FTS5) · `install-model` · `build-embeddings` · `clear-embeddings`.
186
205
 
187
- Full reference: [docs/api.md](./docs/api.md).
206
+ Full reference: [docs/api.md](./docs/api.md). Remote-MCP deployment guide: [docs/http-transport.md](./docs/http-transport.md).
188
207
 
189
208
  ---
190
209
 
@@ -0,0 +1,92 @@
1
+ import { type Server as HttpServer, type IncomingMessage, type ServerResponse } from "node:http";
2
+ import { type ServeOptions, type ServerDeps } from "./index.js";
3
+ /**
4
+ * Configuration for the HTTP transport. Extends ServeOptions so every
5
+ * stdio-mode flag (--enable-write, --persistent-index, --watch, etc.) is
6
+ * available over HTTP too.
7
+ */
8
+ export interface HttpServeOptions extends ServeOptions {
9
+ /** TCP port to listen on. */
10
+ port: number;
11
+ /**
12
+ * Bind host. Default 127.0.0.1 — explicit because remote-MCP must
13
+ * opt-in to bind 0.0.0.0. Most users should keep it on localhost and
14
+ * front it with Tailscale Funnel / Cloudflare Tunnel for remote access
15
+ * (auth-and-encryption-in-depth). See docs/http-transport.md.
16
+ */
17
+ host: string;
18
+ /**
19
+ * Bearer token. Comparing constant-time. Required: a missing token
20
+ * fails closed at startup (we refuse to bind without auth). Generate
21
+ * with: `node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"`.
22
+ */
23
+ bearerToken: string;
24
+ /** URL path prefix for the MCP endpoint. Default `/mcp`. */
25
+ mcpPath?: string;
26
+ /** Max requests per minute per bearer token. Default 120. 0 disables. */
27
+ rateLimitPerMinute?: number;
28
+ /**
29
+ * CORS origin allowlist. Repeatable. Default empty (no
30
+ * Access-Control-Allow-Origin sent). Bearer-credentialed requests work
31
+ * cross-origin only when the origin is in this list.
32
+ */
33
+ corsOrigins?: string[];
34
+ /** Optional path-prefix to match a /health probe under (e.g. for Tailscale). Default `/health`. */
35
+ healthPath?: string;
36
+ /**
37
+ * When `true` (default), registers SIGINT/SIGTERM/beforeExit listeners
38
+ * for graceful shutdown of the HTTP server, vault, FTS5 index, watcher,
39
+ * and persistent cache. Tests pass `false` to avoid leaking listeners
40
+ * when spawning many `startHttpServer()` instances in one process —
41
+ * cleanup happens via `httpServer.close()` which still triggers the
42
+ * underlying resource cleanup.
43
+ */
44
+ installSignalHandlers?: boolean;
45
+ }
46
+ /**
47
+ * Sliding-window rate limiter. Per-token. Window is rolling 60 seconds.
48
+ * Uses a tiny circular buffer of timestamps per token — O(burst-size)
49
+ * memory per token, no GC churn (we trim when we check).
50
+ *
51
+ * NOT distributed. If the user runs multiple processes behind a load
52
+ * balancer they'd want a shared store (Redis); deferred to v2.7+.
53
+ */
54
+ declare class RateLimiter {
55
+ private readonly perMinute;
56
+ private readonly windows;
57
+ constructor(perMinute: number);
58
+ /** Returns true if request allowed; false if over budget. */
59
+ consume(tokenId: string, nowMs?: number): boolean;
60
+ /** Test-only: reset all windows. */
61
+ reset(): void;
62
+ }
63
+ /**
64
+ * Constant-time bearer-token compare. The Authorization header is
65
+ * `Bearer <token>` per RFC 6750. We compare against the raw token
66
+ * supplied at startup — both sides padded to a hash so timingSafeEqual
67
+ * doesn't leak length.
68
+ *
69
+ * Returns the SHA-256 digest of the token (used as the rate-limit key)
70
+ * if valid, or null if not. We never use the raw token as the key — a
71
+ * compromised log dump would expose it.
72
+ */
73
+ declare function verifyBearer(authHeader: string | undefined, expectedToken: string): string | null;
74
+ /** Read the entire request body (UTF-8 JSON). Returns undefined for empty. */
75
+ declare function readJsonBody(req: IncomingMessage, maxBytes: number): Promise<unknown>;
76
+ /**
77
+ * Build the request handler. Factored out of the listener-creation path
78
+ * so tests can drive it without binding a TCP port.
79
+ */
80
+ export declare function createHttpHandler(deps: ServerDeps, opts: HttpServeOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
81
+ /**
82
+ * Generate a fresh 32-byte base64url bearer token. Used by the
83
+ * `enquire-mcp gen-token` subcommand.
84
+ */
85
+ export declare function generateBearerToken(): string;
86
+ /**
87
+ * Bind and start the HTTP transport. Returns the underlying http.Server
88
+ * so callers (tests + CLI) can listen for `listening` / close it.
89
+ */
90
+ export declare function startHttpServer(opts: HttpServeOptions): Promise<HttpServer>;
91
+ export { RateLimiter, readJsonBody, verifyBearer };
92
+ //# sourceMappingURL=http-transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-transport.d.ts","sourceRoot":"","sources":["../src/http-transport.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAgB,KAAK,MAAM,IAAI,UAAU,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAE/G,OAAO,EAAwD,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAEtH;;;;GAIG;AACH,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,mGAAmG;IACnG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;OAOG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,cAAM,WAAW;IACf,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;gBAE3C,SAAS,EAAE,MAAM;IAI7B,6DAA6D;IAC7D,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,MAAmB,GAAG,OAAO;IAiB7D,oCAAoC;IACpC,KAAK,IAAI,IAAI;CAGd;AAED;;;;;;;;;GASG;AACH,iBAAS,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAa1F;AAED,8EAA8E;AAC9E,iBAAe,YAAY,CAAC,GAAG,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAcpF;AAwED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,gBAAgB,GACrB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CA4G9D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAoFjF;AAGD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC"}
@@ -0,0 +1,384 @@
1
+ // HTTP transport for enquire-mcp.
2
+ //
3
+ // v2.6.0 — remote-MCP support. Runs the MCP server over Streamable HTTP
4
+ // (the protocol claude.ai web, ChatGPT, mobile clients, and Cursor's HTTP
5
+ // mode all use). Three layers in front of the SDK transport:
6
+ //
7
+ // 1. Bearer auth — constant-time token compare, fail-closed (401 on
8
+ // missing/wrong header). Token is generated by the user (we never
9
+ // mint tokens — this is not OAuth) and passed via --bearer-token or
10
+ // --bearer-token-env. See docs/http-transport.md for the security
11
+ // model and Tailscale Funnel / Cloudflare Tunnel deployment recipes.
12
+ // 2. Rate-limit — per-token sliding-window bucket, 429 on overflow.
13
+ // In-memory; survives a single process. Default 120 req/min.
14
+ // 3. CORS — strict allowlist via --cors-origin (repeatable). Default
15
+ // empty: no Access-Control-Allow-Origin header sent. Same-origin
16
+ // and credentialed Bearer requests work either way.
17
+ //
18
+ // We use the SDK's StreamableHTTPServerTransport in stateless mode
19
+ // (sessionIdGenerator: undefined). A fresh transport + McpServer is
20
+ // connected per request. The Vault, FtsIndex, EmbedDb handles are
21
+ // SHARED across all sessions — opening SQLite once and reusing across
22
+ // thousands of remote-MCP calls. This is why prepareServerDeps() is
23
+ // called once in startHttpServer() and buildMcpServer() is called per
24
+ // request: the heavy I/O happens once at boot.
25
+ //
26
+ // Stateful sessions (sessionId-keyed transports) are deferred to v2.7+
27
+ // because they require persistence-aware shutdown handling and are only
28
+ // needed for SSE long-running operations. Our tools are short-running
29
+ // (search, read, frontmatter ops), so stateless is the right default.
30
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
31
+ import { createServer } from "node:http";
32
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
33
+ import { buildMcpServer, formatReadyBanner, prepareServerDeps } from "./index.js";
34
+ /**
35
+ * Sliding-window rate limiter. Per-token. Window is rolling 60 seconds.
36
+ * Uses a tiny circular buffer of timestamps per token — O(burst-size)
37
+ * memory per token, no GC churn (we trim when we check).
38
+ *
39
+ * NOT distributed. If the user runs multiple processes behind a load
40
+ * balancer they'd want a shared store (Redis); deferred to v2.7+.
41
+ */
42
+ class RateLimiter {
43
+ perMinute;
44
+ windows = new Map();
45
+ constructor(perMinute) {
46
+ this.perMinute = perMinute;
47
+ }
48
+ /** Returns true if request allowed; false if over budget. */
49
+ consume(tokenId, nowMs = Date.now()) {
50
+ if (this.perMinute <= 0)
51
+ return true; // disabled
52
+ const cutoff = nowMs - 60_000;
53
+ let timestamps = this.windows.get(tokenId);
54
+ if (!timestamps) {
55
+ timestamps = [];
56
+ this.windows.set(tokenId, timestamps);
57
+ }
58
+ // Trim out-of-window entries from the front (they're append-ordered).
59
+ while (timestamps.length > 0 && (timestamps[0] ?? 0) < cutoff) {
60
+ timestamps.shift();
61
+ }
62
+ if (timestamps.length >= this.perMinute)
63
+ return false;
64
+ timestamps.push(nowMs);
65
+ return true;
66
+ }
67
+ /** Test-only: reset all windows. */
68
+ reset() {
69
+ this.windows.clear();
70
+ }
71
+ }
72
+ /**
73
+ * Constant-time bearer-token compare. The Authorization header is
74
+ * `Bearer <token>` per RFC 6750. We compare against the raw token
75
+ * supplied at startup — both sides padded to a hash so timingSafeEqual
76
+ * doesn't leak length.
77
+ *
78
+ * Returns the SHA-256 digest of the token (used as the rate-limit key)
79
+ * if valid, or null if not. We never use the raw token as the key — a
80
+ * compromised log dump would expose it.
81
+ */
82
+ function verifyBearer(authHeader, expectedToken) {
83
+ if (!authHeader?.startsWith("Bearer "))
84
+ return null;
85
+ const presented = authHeader.slice("Bearer ".length).trim();
86
+ if (presented.length === 0)
87
+ return null;
88
+ // Hash both sides so timingSafeEqual gets equal-length buffers regardless
89
+ // of whether the presented token is longer or shorter than expected. The
90
+ // hash is constant-length, so length-comparison short-circuits don't leak.
91
+ const expectedHash = createHash("sha256").update(expectedToken).digest();
92
+ const presentedHash = createHash("sha256").update(presented).digest();
93
+ if (!timingSafeEqual(expectedHash, presentedHash))
94
+ return null;
95
+ // Return the digest as the rate-limit key. Different tokens produce
96
+ // different keys; the same token always produces the same key.
97
+ return presentedHash.toString("hex").slice(0, 16);
98
+ }
99
+ /** Read the entire request body (UTF-8 JSON). Returns undefined for empty. */
100
+ async function readJsonBody(req, maxBytes) {
101
+ const chunks = [];
102
+ let total = 0;
103
+ for await (const chunk of req) {
104
+ const buf = Buffer.from(chunk);
105
+ total += buf.length;
106
+ if (total > maxBytes) {
107
+ throw new Error(`request body exceeds max ${maxBytes} bytes`);
108
+ }
109
+ chunks.push(buf);
110
+ }
111
+ if (total === 0)
112
+ return undefined;
113
+ const text = Buffer.concat(chunks).toString("utf8");
114
+ return JSON.parse(text);
115
+ }
116
+ /** Send a JSON-RPC error response with the given HTTP + RPC status. */
117
+ function sendJsonRpcError(res, httpStatus, code, message) {
118
+ if (res.headersSent)
119
+ return;
120
+ res.statusCode = httpStatus;
121
+ res.setHeader("Content-Type", "application/json");
122
+ res.end(JSON.stringify({
123
+ jsonrpc: "2.0",
124
+ error: { code, message },
125
+ id: null
126
+ }));
127
+ }
128
+ /**
129
+ * Apply CORS headers if the request's Origin is allowed.
130
+ *
131
+ * Defense-in-depth against the CORS-credentials-misconfig class of bugs:
132
+ *
133
+ * • For an explicit allowlisted origin (e.g. https://claude.ai), we
134
+ * reflect ONLY that exact origin in `Access-Control-Allow-Origin` and
135
+ * emit `Vary: Origin` so caches don't cross-pollinate across origins.
136
+ * Pair with `Access-Control-Allow-Credentials: true` so cookies +
137
+ * credentialed Bearer requests work as expected.
138
+ *
139
+ * • For the wildcard `*` allowlist entry, we reflect `*` literally (NOT
140
+ * the request's origin) and DO NOT send `Allow-Credentials: true`.
141
+ * Browsers reject the combo of wildcard-origin + credentials anyway,
142
+ * so this matches what they enforce. Equally important: it kills the
143
+ * CORS-credential-leak attack surface where a misconfigured wildcard
144
+ * could otherwise hand attacker.com a credential-bearing CORS grant.
145
+ *
146
+ * • Disallowed origins get nothing — no Allow-Origin header → browser
147
+ * blocks the actual request even if we 200.
148
+ *
149
+ * Documented as a deliberate behavior split for users who pass `*`: with
150
+ * `*` you get an unauthenticated public CORS endpoint (still bearer-gated
151
+ * server-side, just no credentialed-cookie path). That's the right
152
+ * trade-off.
153
+ */
154
+ function applyCors(req, res, allowOrigins) {
155
+ const requestOrigin = req.headers.origin;
156
+ if (typeof requestOrigin !== "string")
157
+ return;
158
+ // Sourcing the response value from `allowOrigins` (a server-controlled
159
+ // CLI flag) instead of from `req.headers.origin` (an attacker-controlled
160
+ // request header) is a deliberate defense against the CORS-credential-
161
+ // leak class of bug. CodeQL's `js/cors-misconfiguration-for-credentials`
162
+ // query is satisfied because `matchedOrigin`'s data source is the
163
+ // allowlist, not the request — even though the strings are equal by
164
+ // construction at this point, the data-flow taint is what matters.
165
+ const matchedOrigin = allowOrigins.find((o) => o === requestOrigin);
166
+ const wildcardAllowed = matchedOrigin === undefined && allowOrigins.includes("*");
167
+ if (matchedOrigin === undefined && !wildcardAllowed)
168
+ return;
169
+ // Wildcard branch: reflect literal "*" + OMIT Allow-Credentials (the
170
+ // browser would reject the combo anyway, and we don't grant attacker
171
+ // origins a credentialed window). Exact-match branch: use the trusted
172
+ // allowlist value + send Allow-Credentials: true so cookies + Bearer
173
+ // requests work cross-origin.
174
+ if (wildcardAllowed) {
175
+ res.setHeader("Access-Control-Allow-Origin", "*");
176
+ }
177
+ else if (matchedOrigin !== undefined) {
178
+ res.setHeader("Access-Control-Allow-Origin", matchedOrigin);
179
+ res.setHeader("Access-Control-Allow-Credentials", "true");
180
+ }
181
+ res.setHeader("Vary", "Origin");
182
+ res.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS");
183
+ res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Mcp-Session-Id, Last-Event-ID");
184
+ res.setHeader("Access-Control-Max-Age", "600");
185
+ }
186
+ /**
187
+ * Build the request handler. Factored out of the listener-creation path
188
+ * so tests can drive it without binding a TCP port.
189
+ */
190
+ export function createHttpHandler(deps, opts) {
191
+ const mcpPath = opts.mcpPath ?? "/mcp";
192
+ const healthPath = opts.healthPath ?? "/health";
193
+ const corsOrigins = opts.corsOrigins ?? [];
194
+ const limiter = new RateLimiter(opts.rateLimitPerMinute ?? 120);
195
+ const maxBodyBytes = 4 * 1024 * 1024; // 4MB — generous for tools/list with 36 tools
196
+ return async (req, res) => {
197
+ try {
198
+ applyCors(req, res, corsOrigins);
199
+ // OPTIONS preflight short-circuit.
200
+ if (req.method === "OPTIONS") {
201
+ res.statusCode = 204;
202
+ res.end();
203
+ return;
204
+ }
205
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
206
+ // Health probe — unauthenticated, no rate-limit. Useful for
207
+ // Tailscale/Cloudflare healthchecks. Returns just `ok` so it
208
+ // doesn't leak version info to unauth callers.
209
+ if (url.pathname === healthPath && req.method === "GET") {
210
+ res.statusCode = 200;
211
+ res.setHeader("Content-Type", "text/plain");
212
+ res.end("ok");
213
+ return;
214
+ }
215
+ if (url.pathname !== mcpPath) {
216
+ res.statusCode = 404;
217
+ res.setHeader("Content-Type", "application/json");
218
+ res.end(JSON.stringify({ error: "not found", path: url.pathname }));
219
+ return;
220
+ }
221
+ // Auth: bearer token required for /mcp.
222
+ const authHeader = req.headers.authorization;
223
+ const tokenKey = verifyBearer(typeof authHeader === "string" ? authHeader : undefined, opts.bearerToken);
224
+ if (tokenKey === null) {
225
+ res.statusCode = 401;
226
+ res.setHeader("WWW-Authenticate", 'Bearer realm="enquire-mcp"');
227
+ res.setHeader("Content-Type", "application/json");
228
+ res.end(JSON.stringify({ error: "unauthorized" }));
229
+ return;
230
+ }
231
+ // Rate-limit per token.
232
+ if (!limiter.consume(tokenKey)) {
233
+ res.statusCode = 429;
234
+ res.setHeader("Retry-After", "60");
235
+ res.setHeader("Content-Type", "application/json");
236
+ res.end(JSON.stringify({ error: "rate limit exceeded" }));
237
+ return;
238
+ }
239
+ // Method gating. POST is the main MCP entry; GET is for SSE
240
+ // streams (we're stateless, so we don't expose a long-lived GET
241
+ // — stub it with 405); DELETE is for stateful session termination
242
+ // (also 405 in stateless mode).
243
+ if (req.method !== "POST") {
244
+ sendJsonRpcError(res, 405, -32000, `Method ${req.method} not allowed for ${mcpPath}`);
245
+ return;
246
+ }
247
+ let body;
248
+ try {
249
+ body = await readJsonBody(req, maxBodyBytes);
250
+ }
251
+ catch (err) {
252
+ sendJsonRpcError(res, 400, -32700, err instanceof Error ? err.message : "Parse error");
253
+ return;
254
+ }
255
+ // Build a fresh McpServer + transport for this request. The
256
+ // McpServer registers tool handlers (closures over the SHARED
257
+ // deps.vault / deps.ftsIndex), so this is cheap — no SQLite open,
258
+ // no embedder load. The transport handles a single request and
259
+ // closes.
260
+ const server = buildMcpServer(deps, opts);
261
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
262
+ try {
263
+ await server.connect(transport);
264
+ // Wire up cleanup so we never leak a transport on early
265
+ // client-disconnect. Both server and transport are per-request
266
+ // disposable — `server.close()` releases the connection, the
267
+ // transport's `close()` flushes any pending stream.
268
+ const cleanup = () => {
269
+ void transport.close();
270
+ void server.close();
271
+ };
272
+ res.on("close", cleanup);
273
+ await transport.handleRequest(req, res, body);
274
+ }
275
+ catch (err) {
276
+ process.stderr.write(`enquire http: transport error — ${err instanceof Error ? err.message : String(err)}\n`);
277
+ sendJsonRpcError(res, 500, -32603, "Internal server error");
278
+ }
279
+ }
280
+ catch (err) {
281
+ // Final safety net — if anything in the outer block throws (URL
282
+ // parse, header read), don't leave the connection hung.
283
+ process.stderr.write(`enquire http: handler error — ${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`);
284
+ if (!res.headersSent) {
285
+ sendJsonRpcError(res, 500, -32603, "Internal server error");
286
+ }
287
+ }
288
+ };
289
+ }
290
+ /**
291
+ * Generate a fresh 32-byte base64url bearer token. Used by the
292
+ * `enquire-mcp gen-token` subcommand.
293
+ */
294
+ export function generateBearerToken() {
295
+ return randomBytes(32).toString("base64url");
296
+ }
297
+ /**
298
+ * Bind and start the HTTP transport. Returns the underlying http.Server
299
+ * so callers (tests + CLI) can listen for `listening` / close it.
300
+ */
301
+ export async function startHttpServer(opts) {
302
+ if (!opts.bearerToken || opts.bearerToken.length < 16) {
303
+ throw new Error("enquire serve-http: --bearer-token is required and must be ≥16 chars. " +
304
+ "Generate one with: enquire-mcp gen-token");
305
+ }
306
+ const deps = await prepareServerDeps(opts);
307
+ const handler = createHttpHandler(deps, opts);
308
+ const httpServer = createServer((req, res) => {
309
+ void handler(req, res);
310
+ });
311
+ // Persistent-cache flush + watcher cleanup on signal. Same hooks as
312
+ // stdio mode — the deps own the lifecycle. Skipped under
313
+ // installSignalHandlers=false so tests can spawn many servers in one
314
+ // process without accumulating SIGINT/SIGTERM listeners.
315
+ if (opts.installSignalHandlers !== false) {
316
+ if (deps.vault.persistentCacheEnabled) {
317
+ let saving = false;
318
+ let saved = false;
319
+ const flush = async () => {
320
+ if (saving || saved)
321
+ return;
322
+ saving = true;
323
+ try {
324
+ await deps.vault.saveDiskCache();
325
+ saved = true;
326
+ }
327
+ catch (err) {
328
+ process.stderr.write(`enquire: cache flush failed — ${err instanceof Error ? err.message : String(err)}\n`);
329
+ }
330
+ finally {
331
+ saving = false;
332
+ }
333
+ };
334
+ process.once("SIGINT", () => {
335
+ flush().finally(() => process.exit(0));
336
+ });
337
+ process.once("SIGTERM", () => {
338
+ flush().finally(() => process.exit(0));
339
+ });
340
+ process.on("beforeExit", () => {
341
+ if (!saved && !saving)
342
+ void flush();
343
+ });
344
+ }
345
+ if (deps.watcher) {
346
+ const closeWatcher = () => void deps.watcher?.close();
347
+ process.once("SIGINT", closeWatcher);
348
+ process.once("SIGTERM", closeWatcher);
349
+ process.on("beforeExit", closeWatcher);
350
+ }
351
+ if (deps.ftsIndex) {
352
+ const closeFts = () => deps.ftsIndex?.close();
353
+ process.once("SIGINT", closeFts);
354
+ process.once("SIGTERM", closeFts);
355
+ process.on("beforeExit", closeFts);
356
+ }
357
+ // Graceful HTTP-server shutdown on signal.
358
+ const shutdown = () => {
359
+ httpServer.close(() => {
360
+ // Cascade-close happens via beforeExit hooks.
361
+ });
362
+ };
363
+ process.once("SIGINT", shutdown);
364
+ process.once("SIGTERM", shutdown);
365
+ }
366
+ await new Promise((resolve, reject) => {
367
+ httpServer.once("error", reject);
368
+ httpServer.listen(opts.port, opts.host, () => {
369
+ httpServer.removeListener("error", reject);
370
+ resolve();
371
+ });
372
+ });
373
+ const addr = httpServer.address();
374
+ const bound = addr && typeof addr === "object"
375
+ ? `http://${addr.address === "::" ? "::" : addr.address}:${addr.port}`
376
+ : `http://${opts.host}:${opts.port}`;
377
+ const corsLabel = (opts.corsOrigins?.length ?? 0) > 0 ? `, cors=${opts.corsOrigins?.length ?? 0}` : "";
378
+ const rateLabel = (opts.rateLimitPerMinute ?? 120) > 0 ? `, rate-limit=${opts.rateLimitPerMinute ?? 120}/min` : "";
379
+ process.stderr.write(`${formatReadyBanner(deps)} (transport=http, bound=${bound}${opts.mcpPath ?? "/mcp"}${corsLabel}${rateLabel})\n`);
380
+ return httpServer;
381
+ }
382
+ // Re-export the rate limiter and helpers for tests.
383
+ export { RateLimiter, readJsonBody, verifyBearer };
384
+ //# sourceMappingURL=http-transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-transport.js","sourceRoot":"","sources":["../src/http-transport.ts"],"names":[],"mappings":"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"}