@oomkapwn/enquire-mcp 2.5.0 → 2.7.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.
@@ -0,0 +1,305 @@
1
+ # HTTP transport (remote MCP) — `enquire-mcp serve-http`
2
+
3
+ > Available since v2.6.0. Stateless [Streamable HTTP transport](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.
4
+
5
+ The default `serve` subcommand runs over **stdio** — fast, secure, but local-only. `serve-http` runs the same server (same tools, same vault, same indexes) over **HTTP**, so an agent can reach it from a browser tab, a phone, or another machine.
6
+
7
+ ## TL;DR
8
+
9
+ ```bash
10
+ # 1. Generate a bearer token (one-time, store in a password manager)
11
+ enquire-mcp gen-token > ~/.enquire/token # base64url, 43 chars
12
+
13
+ # 2. Start the HTTP server (binds 127.0.0.1:3000 by default)
14
+ enquire-mcp serve-http \
15
+ --vault ~/Obsidian/MyVault \
16
+ --bearer-token "$(cat ~/.enquire/token)" \
17
+ --persistent-index
18
+
19
+ # 3. Verify it's up
20
+ curl http://127.0.0.1:3000/health
21
+ # → ok
22
+
23
+ # 4. Configure your client (claude.ai, ChatGPT, Cursor, etc.) with:
24
+ # URL: http://127.0.0.1:3000/mcp (or your tunnel URL)
25
+ # Auth header: Authorization: Bearer <your-token>
26
+ ```
27
+
28
+ ## When to use HTTP vs stdio
29
+
30
+ | Use case | Transport |
31
+ |---|---|
32
+ | Claude Code / Cursor / Codex on the same machine as your vault | **stdio** (`serve`) — faster, no network setup |
33
+ | Claude.ai web (browser) reaching your local vault | **HTTP** + tunnel |
34
+ | ChatGPT custom GPT with MCP integration | **HTTP** + public tunnel |
35
+ | Phone agents (Claude mobile, Khoj mobile) | **HTTP** + tunnel |
36
+ | Shared vault for a small team | **HTTP** on a small VM (one process, multiple bearer tokens via reverse proxy) |
37
+ | Long-lived background agent that wakes up daily | **HTTP** + cron + tunnel |
38
+
39
+ ## All flags
40
+
41
+ | Flag | Default | Purpose |
42
+ |---|---|---|
43
+ | `--vault <path>` | (required) | Vault root — same semantics as `serve`. |
44
+ | `--bearer-token <token>` | (required, ≥16 chars) | Token clients must present in the `Authorization: Bearer …` header. Generate with `enquire-mcp gen-token`. |
45
+ | `--bearer-token-env <name>` | — | Read the token from this env var instead of the flag. Cleaner for systemd / `.env` files / shared shells where flags are visible in `ps`. Either flag is required. |
46
+ | `--port <n>` | `3000` | TCP port. Pass `0` for kernel-assigned (useful in tests). |
47
+ | `--host <host>` | `127.0.0.1` | Bind host. **Keep on `127.0.0.1`** unless you've thought hard about exposing the server directly — `0.0.0.0` is opt-in because remote-MCP must front a tunnel. |
48
+ | `--mcp-path <path>` | `/mcp` | URL path for the MCP endpoint. |
49
+ | `--rate-limit <n>` | `120` | Max requests per minute per bearer token. `0` disables. Sliding 60-second window, in-memory (single process). |
50
+ | `--cors-origin <origin...>` | (empty) | CORS allowlist. Repeatable. Required when a browser-based agent (claude.ai, ChatGPT) hits the endpoint cross-origin. With explicit origins (`https://claude.ai https://chatgpt.com`) we send `Access-Control-Allow-Credentials: true` so cookies + credentialed Bearer requests work cross-origin. The single-entry wildcard `*` is also supported but **deliberately omits** `Allow-Credentials: true` (because browsers reject that combo anyway, and reflecting credentialed CORS to arbitrary origins is the [CodeQL-flagged cors-credential-leak class of bug](https://codeql.github.com/codeql-query-help/javascript/js-cors-misconfiguration-for-credentials/)). With `*` the endpoint is still bearer-gated server-side; you just lose the cookie path. |
51
+ | `--health-path <path>` | `/health` | Unauthenticated probe path that returns `ok`. Useful for tunnel/uptime monitors. |
52
+ | `--enable-write` | off | Same as `serve` — gates the write tools. |
53
+ | `--persistent-index`, `--watch`, `--exclude-glob`, `--read-paths`, `--disabled-tools`, `--enabled-tools`, `--diagnostic-search-tools`, `--max-file-bytes`, `--cache-size`, `--persistent-cache`, `--cache-file` | — | Identical semantics to `serve`. |
54
+
55
+ Health probe and OPTIONS preflight are unauthenticated. Everything else under `--mcp-path` requires a valid Bearer.
56
+
57
+ ## Security model
58
+
59
+ This is the **opinionated** part of the design. Read it before exposing the endpoint.
60
+
61
+ ### Threat model
62
+
63
+ We assume:
64
+ - The bearer token is a long random secret you treat like a password.
65
+ - The transport between client and server is encrypted (HTTPS) — **we don't terminate TLS ourselves**, you put a reverse proxy or tunnel in front.
66
+ - The host running `enquire-mcp` is your machine (or a small VM you own). We don't sandbox the server itself.
67
+
68
+ We protect against:
69
+ - **Unauthenticated read.** Wrong/missing token → 401, fail-closed.
70
+ - **Token timing leaks.** Bearer compare hashes both sides with SHA-256 first, then `crypto.timingSafeEqual` on equal-length buffers. No length oracle.
71
+ - **Token logging.** Logs use the SHA-256 prefix as the rate-limit key — the raw token never appears in stderr or rate-limit state.
72
+ - **Rate-limit abuse.** Default 120 req/min per token; tunable via `--rate-limit`.
73
+ - **CORS-based origin spoofing.** No `Access-Control-Allow-Origin` header is sent unless the origin matches `--cors-origin`. Default deny.
74
+ - **Body bombs.** 4 MB request-body cap per request.
75
+
76
+ We do **not** protect against:
77
+ - TLS downgrade — that's the tunnel's job. Always front with HTTPS.
78
+ - A compromised client. Treat the token like a password.
79
+ - Denial of service from a single token — a malicious client can fire rate-limit-budget requests forever; we just answer 429 once it's over budget. Use the tunnel's WAF for upstream DoS protection.
80
+ - Sophisticated cross-tenant attacks across multiple tokens — this is a single-tenant tool. A small team should run one process per user (e.g. systemd template unit) and not share tokens.
81
+ - Bypassing the privacy filter (`--exclude-glob`, `--read-paths`) via crafted requests — the same audit-tested filter applies on every search/read path; there are no HTTP-specific shortcuts.
82
+
83
+ ### Bearer token generation
84
+
85
+ ```bash
86
+ # Recommended:
87
+ enquire-mcp gen-token
88
+ # → t7Q1nLkYQrfbXrI9w1Tj2kZ4u_FZCgC5RT8HNqkR1PA
89
+
90
+ # Equivalent ad-hoc:
91
+ node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
92
+
93
+ # Save it once:
94
+ enquire-mcp gen-token > ~/.enquire/token
95
+ chmod 600 ~/.enquire/token
96
+ ```
97
+
98
+ Tokens are 32 random bytes encoded as base64url (43 chars, no padding, URL/header-safe). 256 bits is sufficient — far beyond brute-force at any rate limit.
99
+
100
+ ### Reading from env (recommended for systemd)
101
+
102
+ ```bash
103
+ # .env or systemd EnvironmentFile=
104
+ ENQUIRE_TOKEN=t7Q1nLkYQrfbXrI9w1Tj2kZ4u_FZCgC5RT8HNqkR1PA
105
+ ```
106
+
107
+ ```bash
108
+ enquire-mcp serve-http --vault ~/Obsidian --bearer-token-env ENQUIRE_TOKEN
109
+ ```
110
+
111
+ The token doesn't appear in `ps aux`, shell history, or arg-trace logs.
112
+
113
+ ## Deployment recipes
114
+
115
+ ### Recipe 1 — Tailscale Funnel (zero-config, recommended)
116
+
117
+ [Tailscale Funnel](https://tailscale.com/kb/1223/funnel) gives you a public HTTPS URL routed through Tailscale's MagicDNS, with TLS handled for you. Free for personal use.
118
+
119
+ ```bash
120
+ # One-time setup:
121
+ tailscale up
122
+ tailscale funnel 3000
123
+
124
+ # Run enquire-mcp on localhost:3000
125
+ enquire-mcp serve-http \
126
+ --vault ~/Obsidian \
127
+ --bearer-token-env ENQUIRE_TOKEN \
128
+ --port 3000 \
129
+ --cors-origin https://claude.ai
130
+ ```
131
+
132
+ Tailscale Funnel proxies `https://<machine>.<tailnet>.ts.net/` → `localhost:3000`. Configure your client with:
133
+
134
+ - URL: `https://<machine>.<tailnet>.ts.net/mcp`
135
+ - Authorization: `Bearer <your-token>`
136
+
137
+ You **don't** need to bind to `0.0.0.0` — Funnel reaches localhost via Tailscale's userspace.
138
+
139
+ ### Recipe 2 — Cloudflare Tunnel (no Tailscale account)
140
+
141
+ ```bash
142
+ # One-time:
143
+ brew install cloudflared
144
+ cloudflared tunnel login # opens browser, auths to your zone
145
+ cloudflared tunnel create enquire
146
+ # → outputs a UUID; save it as ~/.cloudflared/<uuid>.json
147
+
148
+ # Route a hostname:
149
+ cloudflared tunnel route dns enquire vault.yourdomain.com
150
+
151
+ # Run the tunnel + the server (in two terminals or two systemd units):
152
+ cloudflared tunnel run --url http://localhost:3000 enquire
153
+ enquire-mcp serve-http \
154
+ --vault ~/Obsidian \
155
+ --bearer-token-env ENQUIRE_TOKEN \
156
+ --cors-origin https://claude.ai https://chatgpt.com
157
+ ```
158
+
159
+ Client hits `https://vault.yourdomain.com/mcp` — Cloudflare terminates TLS, validates the host, forwards to your machine over the tunnel.
160
+
161
+ ### Recipe 3 — ngrok (quick demo / dev)
162
+
163
+ ```bash
164
+ ngrok http 3000
165
+
166
+ # In another terminal:
167
+ enquire-mcp serve-http --vault ~/Obsidian --bearer-token-env ENQUIRE_TOKEN
168
+ ```
169
+
170
+ ngrok prints `https://abc123.ngrok-free.app` — your endpoint is `https://abc123.ngrok-free.app/mcp`. Free tier rotates the URL on every restart; paid plans get a static domain.
171
+
172
+ ### Recipe 4 — direct LAN (no tunnel — local network only)
173
+
174
+ ```bash
175
+ # DANGEROUS without TLS — only on a trusted private network you control.
176
+ enquire-mcp serve-http \
177
+ --vault ~/Obsidian \
178
+ --bearer-token-env ENQUIRE_TOKEN \
179
+ --host 0.0.0.0 \
180
+ --port 3000
181
+ ```
182
+
183
+ Then on another machine on the same LAN: `http://<your-ip>:3000/mcp`. The bearer token is sent in plaintext — only acceptable on a private network behind a real firewall. **Don't do this on coffee-shop WiFi or any network you don't fully control.**
184
+
185
+ ### Recipe 5 — systemd service (production)
186
+
187
+ `/etc/systemd/system/enquire.service`:
188
+
189
+ ```ini
190
+ [Unit]
191
+ Description=enquire-mcp HTTP transport
192
+ After=network-online.target
193
+
194
+ [Service]
195
+ Type=simple
196
+ User=enquire
197
+ EnvironmentFile=/etc/enquire/env
198
+ ExecStart=/usr/local/bin/enquire-mcp serve-http \
199
+ --vault /home/enquire/vault \
200
+ --bearer-token-env ENQUIRE_TOKEN \
201
+ --persistent-index \
202
+ --watch \
203
+ --port 3000 \
204
+ --rate-limit 240
205
+ Restart=on-failure
206
+ RestartSec=5
207
+ PrivateTmp=true
208
+ NoNewPrivileges=true
209
+ ProtectSystem=strict
210
+ ProtectHome=read-only
211
+ ReadWritePaths=/home/enquire/.cache/enquire
212
+
213
+ [Install]
214
+ WantedBy=multi-user.target
215
+ ```
216
+
217
+ `/etc/enquire/env` (mode 600, owner `enquire`):
218
+
219
+ ```
220
+ ENQUIRE_TOKEN=<your-token>
221
+ ```
222
+
223
+ Pair with a Cloudflare tunnel (Recipe 2) or nginx + Let's Encrypt for TLS termination.
224
+
225
+ ## Client configuration
226
+
227
+ ### Claude.ai web (Pro/Team/Enterprise)
228
+
229
+ Settings → Integrations → Add custom MCP:
230
+ - **Name:** Obsidian Vault
231
+ - **URL:** `https://<your-tunnel>/mcp`
232
+ - **Auth:** Bearer
233
+ - **Token:** your token
234
+
235
+ ### Cursor (HTTP mode)
236
+
237
+ `~/.cursor/mcp.json`:
238
+
239
+ ```json
240
+ {
241
+ "mcpServers": {
242
+ "enquire": {
243
+ "url": "https://<your-tunnel>/mcp",
244
+ "headers": { "Authorization": "Bearer <your-token>" }
245
+ }
246
+ }
247
+ }
248
+ ```
249
+
250
+ ### ChatGPT (custom GPT)
251
+
252
+ In the GPT Builder → Configure → Actions → Authentication → API key → "Bearer" → paste token. Schema URL points at `/mcp`. (ChatGPT's MCP support is rolling out — check OpenAI docs for the exact wiring.)
253
+
254
+ ### Khoj mobile
255
+
256
+ Settings → MCP servers → Add. URL + bearer token.
257
+
258
+ ### Manual (curl)
259
+
260
+ ```bash
261
+ TOKEN="$(cat ~/.enquire/token)"
262
+ URL="http://127.0.0.1:3000/mcp"
263
+
264
+ # Initialize
265
+ curl -sX POST "$URL" \
266
+ -H "Content-Type: application/json" \
267
+ -H "Accept: application/json, text/event-stream" \
268
+ -H "Authorization: Bearer $TOKEN" \
269
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0.0.0"}}}'
270
+
271
+ # tools/list
272
+ curl -sX POST "$URL" \
273
+ -H "Content-Type: application/json" \
274
+ -H "Accept: application/json, text/event-stream" \
275
+ -H "Authorization: Bearer $TOKEN" \
276
+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
277
+ ```
278
+
279
+ ## Operational notes
280
+
281
+ - **Stateless mode.** Every request creates a fresh `McpServer` instance over the **shared** vault + index handles. SQLite stays open; only the per-request server class is recreated. This means session-scoped state (e.g. a paginating cursor) doesn't carry across requests — each request is independent. Stateful sessions (with `Mcp-Session-Id` + persistent SSE streams) are tracked for v2.7+ if there's demand.
282
+ - **Cold start.** First request after server start does the FTS5 sync; subsequent requests hit the warm index. `--watch` keeps it warm across vault edits without reboots.
283
+ - **Rate limit is per-process.** If you run multiple processes (e.g. team-tier with one process per user behind a reverse proxy), each enforces its own bucket. For shared limits use the reverse proxy's rate-limit module.
284
+ - **Logs go to stderr.** The ready banner, skip-tool warnings, and transport errors all go to stderr — keep it captured by systemd / your tunnel.
285
+
286
+ ## Comparison vs other Obsidian-MCPs
287
+
288
+ No other Obsidian-MCP currently ships a remote-HTTP transport. With v2.6.0, enquire-mcp is the only one you can wire up to claude.ai web, ChatGPT, or a phone — same vault, same tools, same hybrid retrieval, just over HTTPS instead of stdio.
289
+
290
+ ## Troubleshooting
291
+
292
+ **`enquire serve-http: --bearer-token is required and must be ≥16 chars.`**
293
+ You either forgot the token or it's too short. Generate one with `enquire-mcp gen-token`.
294
+
295
+ **`enquire fatal: --port must be an integer in [0, 65535]`**
296
+ You passed something that's not a non-negative integer. Use `0` for ephemeral, `3000` for default, or any port your firewall lets through.
297
+
298
+ **Client gets 401 with the right token**
299
+ Double-check there's no leading/trailing whitespace in your env var. `--bearer-token "$(cat token)"` includes a trailing newline — use `--bearer-token-env` and `printf` (no trailing `\n`) instead, or trim with `tr -d '\n'`.
300
+
301
+ **Browser client gets CORS errors**
302
+ Add the origin explicitly with `--cors-origin https://claude.ai` (or whichever domain). `*` doesn't work with credentialed Bearer requests.
303
+
304
+ **Initialize succeeds but `tools/list` returns nothing**
305
+ You probably set `--enabled-tools` to a name that doesn't match. Check stderr — the warning `--enabled-tools "<name>" did not match any tool` lists every registered tool name.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oomkapwn/enquire-mcp",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "Drop-in MCP server for Obsidian vaults. Hybrid retrieval (BM25 + TF-IDF + ML embeddings via RRF), wikilinks resolved with aliases and sections, backlinks ranked with snippets, frontmatter typed, Dataview-style queries first-class. Read-only by default; opt-in writes. Works with Claude Code, Cursor, OpenClaw, Codex, Devin, and any MCP-compatible client.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -89,7 +89,8 @@
89
89
  "vitest": "^4.1.5"
90
90
  },
91
91
  "optionalDependencies": {
92
+ "@huggingface/transformers": "^4.2.0",
92
93
  "better-sqlite3": "^12.9.0",
93
- "@huggingface/transformers": "^4.2.0"
94
+ "pdfjs-dist": "^4.10.38"
94
95
  }
95
96
  }