@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 +62 -0
- package/README.md +22 -3
- package/dist/http-transport.d.ts +92 -0
- package/dist/http-transport.d.ts.map +1 -0
- package/dist/http-transport.js +384 -0
- package/dist/http-transport.js.map +1 -0
- package/dist/index.d.ts +45 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +162 -44
- package/dist/index.js.map +1 -1
- package/docs/api.md +2 -0
- package/docs/http-transport.md +305 -0
- package/package.json +1 -1
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 | ✅
|
|
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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"}
|