@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.
- package/CHANGELOG.md +105 -0
- package/README.md +27 -6
- package/SECURITY.md +34 -0
- 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 +191 -45
- package/dist/index.js.map +1 -1
- package/dist/pdf.d.ts +51 -0
- package/dist/pdf.d.ts.map +1 -0
- package/dist/pdf.js +162 -0
- package/dist/pdf.js.map +1 -0
- package/dist/tools.d.ts +52 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +86 -0
- package/dist/tools.js.map +1 -1
- package/docs/api.md +3 -1
- package/docs/http-transport.md +305 -0
- package/package.json +3 -2
|
@@ -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.
|
|
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
|
-
"
|
|
94
|
+
"pdfjs-dist": "^4.10.38"
|
|
94
95
|
}
|
|
95
96
|
}
|