compendium-mcp 0.6.3 → 0.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/README.md CHANGED
@@ -4,32 +4,15 @@
4
4
  <img src="assets/logo.svg" alt="Compendium" width="420" />
5
5
  </p>
6
6
 
7
- MCP server that **minimizes LLM token usage** by compressing, summarizing, filtering, and chunk-referencing large context before it reaches the model.
7
+ MCP server that **shrinks noisy context before it reaches the model**: filter logs, compress bulky text/JSON, redact secrets, chunk huge files, and keep only what a question needs.
8
8
 
9
- Built in Rust with the official [`rmcp`](https://crates.io/crates/rmcp) SDK.
9
+ One tool (`compendium`), one `action` field, deterministic heuristics that need **no model**. An optional local Ollama model improves the two "smart" actions.
10
10
 
11
- ## Why / when to use Compendium
11
+ ## Install (Cursor / Claude Desktop)
12
12
 
13
- Use it when an agent is about to paste **large or noisy context** into the model (build logs, test dumps, API JSON, untrusted web/tool text, long chat, or a fresh workspace). The goal is a **smaller, safer, still-useful** prompt — not another planner or agent runtime.
13
+ Requires Node.js 18+. No Rust toolchain needed the launcher downloads a prebuilt binary once.
14
14
 
15
- | Situation | Call |
16
- |-----------|------|
17
- | Unsure which action | `catalog` → `help` + `id` (or read `cmp://skill/…`) |
18
- | New task in a repo | `brief` with a short `query` |
19
- | Noisy terminal / CLI dump | `filter` (generic) or `compress_output` (cargo/npm/docker/git/…) |
20
- | Bulky text/JSON to densify | `compress` (small inputs bypass unless `force`) |
21
- | Untrusted paste / secrets / IPI | `sanitize` (or `sanitize_input: true` on the next action) |
22
- | Guided recipe | `playbooks` → `playbook` |
23
-
24
- Heuristic paths work with **no local model**. Optional loopback LLM improves `summarize_smart` / hybrid `rerank` / smart `filter_relevant`.
25
-
26
- ## Quick start (Cursor)
27
-
28
- You need **Node.js 18+**. Compendium itself arrives via npm — no Rust install required.
29
-
30
- ### 1. Add the MCP server
31
-
32
- Open Cursor MCP settings (`~/.cursor/mcp.json` or the project `.cursor/mcp.json`) and add:
15
+ Add to `~/.cursor/mcp.json` (or the project's `.cursor/mcp.json`, or Claude Desktop's MCP config):
33
16
 
34
17
  ```json
35
18
  {
@@ -42,113 +25,44 @@ Open Cursor MCP settings (`~/.cursor/mcp.json` or the project `.cursor/mcp.json`
42
25
  }
43
26
  ```
44
27
 
45
- Restart MCP / reload Cursor. You should see one tool named **`compendium`**.
28
+ Reload MCP. You should see a single tool named `compendium`. That's it.
46
29
 
47
- That alone is enough: filter, compress, summarize, cache, and BM25 actions all work **without** a local model (fast heuristics).
30
+ Prebuilt binaries: macOS (arm64, x64), Linux (x64, arm64), Windows (x64). Other platforms: `cargo build --release` and set `COMPENDIUM_BINARY=/path/to/compendium` in the server `env`.
48
31
 
49
- ### 2. (Optional) Smarter summaries with Ollama
32
+ ### Optional: smarter summaries with Ollama
50
33
 
51
- Want better `summarize_smart` / `filter_relevant` / hybrid `rerank`? One command after the MCP server is added:
34
+ `summarize_smart` and `filter_relevant` use a small local model when one is configured; otherwise they fall back to heuristics. One command sets it up:
52
35
 
53
36
  ```bash
54
37
  npx -y compendium-mcp setup-ollama --write-mcp
55
38
  ```
56
39
 
57
- That detects (or `--install`s) [Ollama](https://ollama.com/), pulls a small chat + embed model (`qwen2.5:3b` + `nomic-embed-text`), probes loopback `http://127.0.0.1:11434/v1`, and merges `COMPENDIUM_LOCAL_LLM_*` into `~/.cursor/mcp.json`. Use `--project` for `.cursor/mcp.json`. Reload MCP, then `action: "llm_status"` should show `reachable: true`. `"backend": "local_llm"` on smart actions means Ollama answered; `"heuristic"` means it fell back.
40
+ It detects (or `--install`s) [Ollama](https://ollama.com/), pulls `qwen2.5:3b`, probes `http://127.0.0.1:11434/v1`, and writes `COMPENDIUM_LOCAL_LLM_URL` / `COMPENDIUM_LOCAL_LLM_MODEL` into `~/.cursor/mcp.json` (`--project` for `.cursor/mcp.json`). Reload MCP and call `{"action":"llm_status"}` `reachable: true` means smart actions will report `backend: "local_llm"`.
58
41
 
59
- ```bash
60
- npx -y compendium-mcp setup-ollama --help
61
- npx -y compendium-mcp setup-ollama --install --write-mcp --project
62
- npx -y compendium-mcp setup-ollama --dry-run --json
63
- ```
42
+ Any OpenAI-compatible **loopback** server works (llama.cpp, Lemonade, …): set the two env vars yourself. Remote hosts are rejected on purpose.
64
43
 
65
- Agent recipe: playbook `setup-ollama` (`cmp://skill/playbook/setup-ollama`).
66
-
67
- Manual / Lemonade / custom models: set `COMPENDIUM_LOCAL_LLM_URL` yourself (must stay on **localhost** — Compendium blocks remote hosts on purpose). See [Environment](#environment).
68
-
69
- **Notes**
70
-
71
- - Package name on npm is **`compendium-mcp`** (`compendium` was already taken). The CLI binary name is still `compendium`.
72
- - First Ollama reply can be slow while the model loads; later calls are faster.
73
- - Other local OpenAI-compatible servers work the same way (e.g. Lemonade `http://127.0.0.1:13305/api/v1`). See [Environment](#environment).
74
-
75
- Smoke-check from a terminal (any folder **except** this git repo root is fine):
76
-
77
- ```bash
78
- npx -y compendium-mcp --help
79
- ```
44
+ ## Actions
80
45
 
81
- Binary packaging details for maintainers: [npm/DISTRIBUTION.md](npm/DISTRIBUTION.md).
46
+ | `action` | Use it for | Main fields |
47
+ |----------|------------|-------------|
48
+ | `filter` | Strip ANSI/boilerplate, collapse whitespace, keep/drop regex lines | `text`, `filter?`, `query?` |
49
+ | `compress_output` | Domain-aware scrub of cargo / npm / docker / git / kubectl output | `text`, `output?` |
50
+ | `compress` | Dense rewrite of bulky text, code, or JSON | `text`, `compress?` |
51
+ | `summarize` | Outline / conversation / file-tree summary | `text`, `summarize?` |
52
+ | `summarize_smart` | Local-model summary (heuristic fallback) | `text`, `smart?` |
53
+ | `filter_relevant` | Keep only lines relevant to a question (local model or BM25) | `text`, `query` |
54
+ | `sanitize` | Redact secrets, neutralize prompt-injection phrases | `text`, `sanitize?` |
55
+ | `rerank` | BM25-rank candidates for a query | `query`, `items` / `text` / chunk `map` |
56
+ | `chunk` / `resolve` | Split a huge corpus into `cmp://` chunks; fetch one by id | `text`, `chunk?` / `id` |
57
+ | `cache_store` / `cache_get` / `cache_invalidate` | Park a blob outside the prompt under a short key | `text`, `cache?` / `key` |
58
+ | `count_tokens` | Measure size | `text` |
59
+ | `stats` | Session savings per action | `reset?` |
60
+ | `llm_status` | Is the local model configured and reachable? | `force?` |
61
+ | `help` | List actions, or fields + example for one action | `id?` |
82
62
 
83
- ## Community
63
+ Every response is `{ "ok": true, "action": "filter", "result_json": "{…}" }` — parse `result_json` for the action payload. Add `"sanitize_input": true` to any text action to scrub secrets first. `compress` / `summarize` leave inputs under 1000 characters untouched unless `force: true`.
84
64
 
85
- - [Contributing](CONTRIBUTING.md)
86
- - [Changelog](CHANGELOG.md)
87
- - [Architecture](docs/architecture.md)
88
- - [Code of Conduct](CODE_OF_CONDUCT.md)
89
- - [Security policy](SECURITY.md)
90
- - [Support](SUPPORT.md)
91
-
92
- ## Transports
93
-
94
- | Mode | Command | Notes |
95
- |------|---------|-------|
96
- | **stdio** (default) | `compendium` / `compendium stdio` | Cursor / Claude Desktop — dual-compat (legacy initialize or modern connect) |
97
- | **Streamable HTTP** | `compendium http [BIND]` | Requires `--features http`. Endpoint: `http://{bind}/mcp`. Sessionless (`2026-07-28`); JSON preferred, SSE fallback |
98
- | **Ollama setup** | `compendium setup-ollama` | Not an MCP transport — CLI helper (`--write-mcp`, `--dry-run`, `--json`). Alias: `compendium ollama` |
99
-
100
- Default HTTP bind: `127.0.0.1:8788` (override with arg or `COMPENDIUM_HTTP_BIND`). App cache (`COMPENDIUM_CACHE_DIR`) is not an MCP session — set it for multi-request HTTP. See playbook `http-transport`.
101
-
102
- ## Tools
103
-
104
- Single MCP tool: **`compendium`**. Choose the operation with `action`. Prefer the [Why / when](#why--when-to-use-compendium) table for the first call; use the full list below only when you need a specific field.
105
-
106
- | `action` | Purpose | Main fields |
107
- |----------|---------|-------------|
108
- | `filter` | Strip ANSI, boilerplate, whitespace; densify JSON; keep/drop regexes | `text`, `filter` — not for cargo/npm dumps (`compress_output`) |
109
- | `compress` | Dense representation of text/code/logs | `text`, `compress` — soft inputs under ~1000 chars bypass unless `force` |
110
- | `compress_output` | Domain-aware stdout/stderr scrub (git, cargo, npm, docker, …) | `text`, `output` — prefer when CLI domain is known |
111
- | `summarize` | Hierarchical summary (conversation / file tree / outline) | `text`, `summarize` |
112
- | `summarize_smart` | Local-SLM dense summary (heuristic fallback if unset/fails) | `text`, `smart?`, `summarize?` |
113
- | `filter_relevant` | Query-aware keep of relevant lines (local SLM + heuristic fallback) | `text`, `query`, `smart?` |
114
- | `prune_history` | Drop filler / compress older chat turns | `text` or `messages`, `prune` |
115
- | `chunk` | Split into `cmp://` chunks (session-cached) | `text`, `chunk` |
116
- | `resolve` | Fetch chunk content by id | `id` (+ optional `map` / `text`) |
117
- | `count_tokens` | Measure tokens | `text` |
118
- | `stats` | Session savings + latency/bypass/backend telemetry | `reset?` — see playbook `stats-debug` |
119
- | `cache_store` | Park bulky payload outside the prompt | `text`, `cache` |
120
- | `cache_get` | Retrieve by key | `key` |
121
- | `cache_invalidate` | Drop one key or clear cache | `key?` |
122
- | `sanitize` | Redact secrets + neutralize IPI phrases | `text`, `sanitize?` — or `sanitize_input` |
123
- | `rerank` | BM25 (+ optional loopback embeddings + opt-in SLM cross-encoder) rank candidates / chunks | `query`, `items` or `text` or chunk `map`, `rerank?` |
124
- | `brief` | Scan a workspace; pack a structured starter briefing + cache key | `query`, `brief?` (`root`, caps), optional `text` hint |
125
- | `catalog` | Short action (+ playbook) ads; prefer before guessing | _(none)_ — call first when unsure |
126
- | `help` | Usage notes for one action (default **compressed**; `force: true` → full) | `id`, `force?` |
127
- | `playbooks` | List playbook ads | _(none)_ |
128
- | `playbook` | Load one playbook body | `id` |
129
- | `pack` | Zip text/files into a bounded archive | `text` or `items`, `pack?` |
130
- | `unpack` | Unpack zip with size caps into chunks (never runs scripts) | `text` or `key`, `pack?` |
131
- | `llm_status` | Probe configured local LLM (models; `force` = chat ping) | `force?` — when smart/hybrid unexpectedly heuristic |
132
-
133
- ### Progressive disclosure (skills)
134
-
135
- Tool description/instructions stay thin. Discover details on demand:
136
-
137
- - **Tool bridge:** `action=catalog` → `action=help` with `id`, or `playbooks` → `playbook`
138
- - **MCP resources:** `resources/list` / `resources/read` on:
139
- - `cmp://skill/index` — JSON index of actions + playbooks
140
- - `cmp://skill/action/{name}` — full action help (markdown)
141
- - `cmp://skill/playbook/{id}` — playbook body
142
-
143
- Bundled playbooks live under [`playbooks/`](playbooks/). Override/extend with `COMPENDIUM_PLAYBOOKS_DIR` (same `id` wins). Archives honor `COMPENDIUM_ARCHIVE_MAX_BYTES` / `_UNCOMPRESSED` / `_FILES` (defaults 2 MiB / 4 MiB / 50).
144
-
145
- Optional on most text actions: `sanitize_input: true` scrubs before processing. Soft payloads under `COMPENDIUM_SIGNAL_MIN_CHARS` (default 1000) bypass `compress` / `summarize` / `summarize_smart` unless `force: true`.
146
-
147
- `filter` accepts optional `query` (top-level or `filter.query`) for BM25 line keep. `prune_history` supports `prune.strategy: "afm"` (Critical / Thematic / Distant tiers; distant blob cached for `cache_get`).
148
-
149
- `brief` walks `brief.root` (default: process cwd) with `.gitignore` / `.ignore`, BM25-ranks paths/chunks, window-reads oversized files (not head-truncate), and returns a structured `briefing`: **Task / Status / Evidence / Caveats / Sources / Read next**, plus `cache_key`. Status uses a local SLM when `COMPENDIUM_LOCAL_LLM_URL` is set (`backend: local_llm`); otherwise heuristic bullets. Caveats flag truncated files and docs older than selected code. **Read next** includes source paths plus suggested `cmp://skill/playbook/…` / action URIs. Optional `COMPENDIUM_BRIEF_ROOT` restricts allowed roots. Briefings are sanitized by default.
150
-
151
- Example — noisy log (canonical first call after install):
65
+ Example:
152
66
 
153
67
  ```json
154
68
  {
@@ -158,256 +72,45 @@ Example — noisy log (canonical first call after install):
158
72
  }
159
73
  ```
160
74
 
161
- Discover more without reading this README: `{"action":"catalog"}` then `{"action":"help","id":"compress_output"}`. Sample payloads: [`examples/`](examples/).
162
-
163
- Response envelope: `{ "ok": true, "action": "filter", "result_json": "{...}" }`. Parse `result_json` as JSON for the action-specific payload.
164
-
165
- ## Project layout
166
-
167
- ```
168
- assets/ # brand mark (SVG/PNG); baked into MCP icons via data URI
169
- docs/ # architecture notes
170
- examples/ # sample MCP tool-call JSON payloads
171
- testdata/ # eval fixtures (logs, audit, PR JSON, untrusted paste, …)
172
- src/
173
- main.rs # CLI: stdio | http | setup-ollama
174
- setup_ollama.rs # `compendium setup-ollama` (detect/pull/probe/write MCP env)
175
- lib.rs
176
- brand.rs # SEP-973 icons for serverInfo + tool
177
- config.rs # COMPENDIUM_* env config
178
- server/ # MCP tool + resources + action handlers (rmcp)
179
- http.rs # Streamable HTTP, sessionless (feature = "http")
180
- pipeline/
181
- brief/ # workspace brief (walk / window / pack / synthesize)
182
- tokens.rs # heuristic or tiktoken BPE (feature = "real-tokens")
183
- filter.rs
184
- compress.rs
185
- summarize.rs
186
- smart.rs # summarize_smart + filter_relevant
187
- local_llm.rs # OpenAI-compatible local SLM client (+ embed cache)
188
- chunk.rs # chunk + resolve
189
- cache.rs # session key/value cache (+ optional disk / embed vectors)
190
- catalog.rs # action ads + help (progressive disclosure)
191
- playbook.rs # bundled / dir playbooks
192
- pack.rs # zip pack/unpack with size caps
193
- stats.rs # session savings counters
194
- prune.rs # conversation history pruning
195
- output.rs # domain-aware compress_output
196
- playbooks/ # embedded skill-md playbooks
197
- tests/
198
- integration.rs
199
- e2e_smoke.rs # spawns binary, MCP handshake, tools + resources
200
- eval_regression.rs # B1 heuristic quality + latency smoke
201
- CHANGELOG.md
202
- REPORT.md # design essay + Shipped (A–C) / Next ops / Deferred roadmap
203
- ```
204
-
205
- ## Build
206
-
207
- ```bash
208
- # Default: heuristic tokens + stdio only
209
- cargo build --release
210
-
211
- # Exact BPE token counts (tiktoken-rs)
212
- cargo build --release --features real-tokens
213
-
214
- # Streamable HTTP transport
215
- cargo build --release --features http
216
-
217
- # Everything
218
- cargo build --release --features real-tokens,http
219
- ```
220
-
221
- Binary: `target/release/compendium`
222
-
223
- ## Configure (advanced)
224
-
225
- The [Quick start](#quick-start-cursor) config is enough for most people. Extra options:
226
-
227
- ### Claude Desktop
228
-
229
- Same `command` / `args` / `env` as Cursor, in Claude’s MCP config file.
230
-
231
- ### Optional tuning env
232
-
233
- ```json
234
- "env": {
235
- "RUST_LOG": "compendium=info",
236
- "COMPENDIUM_DEFAULT_MAX_TOKENS": "2048",
237
- "COMPENDIUM_TOKENIZER": "cl100k_base",
238
- "COMPENDIUM_LOCAL_LLM_URL": "http://127.0.0.1:11434/v1",
239
- "COMPENDIUM_LOCAL_LLM_MODEL": "qwen2.5:3b",
240
- "COMPENDIUM_LOCAL_EMBED_MODEL": "nomic-embed-text"
241
- }
242
- ```
243
-
244
- ### Local Cargo binary (developers)
245
-
246
- After code changes, rebuild and **reload MCP** so the live tool schema matches source (avoid stale `npx`/Release binaries during development):
247
-
248
- ```bash
249
- cargo build --release --features real-tokens,http
250
- ```
251
-
252
- ```json
253
- {
254
- "mcpServers": {
255
- "compendium": {
256
- "command": "/absolute/path/to/Compendium/target/release/compendium",
257
- "env": {
258
- "RUST_LOG": "compendium=info",
259
- "COMPENDIUM_DEFAULT_MAX_TOKENS": "2048"
260
- }
261
- }
262
- }
263
- }
264
- ```
265
-
266
- ### Remote / sidecar (HTTP)
267
-
268
- ```bash
269
- cargo run --features http -- http 127.0.0.1:8788
270
- # MCP endpoint: http://127.0.0.1:8788/mcp
271
- ```
272
-
273
- Point an MCP streamable-HTTP client at that URL (e.g. `StreamableHttpClientTransport::from_uri`).
75
+ More samples in [`examples/`](examples/). Unsure which action fits? Call `{"action":"help"}`.
274
76
 
275
77
  ## Environment
276
78
 
79
+ All optional.
80
+
277
81
  | Variable | Default | Meaning |
278
82
  |----------|---------|---------|
279
- | `COMPENDIUM_CHARS_PER_TOKEN` | `4.0` | Heuristic chars÷tokens (ignored with `real-tokens`) |
280
- | `COMPENDIUM_TOKENIZER` | `cl100k_base` | BPE encoding: `cl100k_base` or `o200k_base` (`real-tokens`) |
281
- | `COMPENDIUM_DEFAULT_MAX_TOKENS` | `2048` | Soft cap for compress |
282
- | `COMPENDIUM_MAX_BLANK_LINES` | `1` | Blank-line collapse limit |
283
- | `COMPENDIUM_SIMILARITY_THRESHOLD` | `0.85` | Jaccard line-dedupe threshold |
284
- | `COMPENDIUM_HTTP_BIND` | `127.0.0.1:8788` | Default HTTP listen address |
285
- | `COMPENDIUM_LOCAL_LLM_URL` | _(unset)_ | OpenAI-compatible base URL (e.g. `http://127.0.0.1:11434/v1` or `http://127.0.0.1:13305/api/v1`). Enables smart actions. |
286
- | `COMPENDIUM_LOCAL_LLM_MODEL` | `Qwen3-4B-GGUF` | Model id on that server (Ollama setup default: `qwen2.5:3b`) |
287
- | `COMPENDIUM_LOCAL_EMBED_MODEL` | _(same as chat)_ | Embeddings model for hybrid `rerank` / `brief` (e.g. `nomic-embed-text`) |
288
- | `COMPENDIUM_HYBRID_ALPHA` | `0.55` | BM25 weight in hybrid score (0–1); remainder is embedding cosine |
289
- | `COMPENDIUM_RERANK_CROSS_ENCODER` | _(off)_ | When `1`/`true`, `rerank` SLM-rescores top-N after BM25/hybrid |
290
- | `COMPENDIUM_CROSS_ENCODER_TOP_N` | `16` | Candidates passed to cross-encoder (clamped 4–64) |
291
- | `COMPENDIUM_AUDIT_PATH` | _(unset)_ | Append-only JSONL audit log (action metadata only; no payloads) |
292
- | `COMPENDIUM_LOCAL_LLM_API_KEY` | _(unset)_ | Optional bearer token for locked loopback servers |
83
+ | `COMPENDIUM_LOCAL_LLM_URL` | _(unset)_ | OpenAI-compatible loopback base URL; enables smart actions |
84
+ | `COMPENDIUM_LOCAL_LLM_MODEL` | `qwen2.5:3b` | Model id on that server |
85
+ | `COMPENDIUM_LOCAL_LLM_API_KEY` | _(unset)_ | Bearer token for locked loopback servers |
293
86
  | `COMPENDIUM_LOCAL_LLM_TIMEOUT_SECS` | `120` | HTTP timeout (first model load can be slow) |
87
+ | `COMPENDIUM_LOCAL_LLM_REASONING_EFFORT` | `low` | `low`/`medium`/`high`/`off`; `omit` drops the field for strict servers |
88
+ | `COMPENDIUM_DEFAULT_MAX_TOKENS` | `2048` | Soft output cap for compress/summarize |
294
89
  | `COMPENDIUM_SIGNAL_MIN_CHARS` | `1000` | Bypass compress/summarize below this length (`0` disables) |
295
- | `COMPENDIUM_BRIEF_ROOT` | _(unset)_ | When set, `action=brief` may only scan roots under this canonical path |
296
- | `COMPENDIUM_PLAYBOOKS_DIR` | _(unset)_ | Extra/override playbook `*.md` directory (same `id` replaces embedded) |
297
- | `COMPENDIUM_ARCHIVE_MAX_BYTES` | `2097152` | Max compressed archive size for pack/unpack |
298
- | `COMPENDIUM_ARCHIVE_MAX_UNCOMPRESSED` | `4194304` | Max total uncompressed bytes for pack/unpack |
299
- | `COMPENDIUM_ARCHIVE_MAX_FILES` | `50` | Max files per archive |
300
- | `COMPENDIUM_SKILL_TTL_MS` | `300000` | Soft TTL (ms) on skill `resources/read` responses |
301
- | `COMPENDIUM_CACHE_DIR` | _(unset)_ | Persist session cache (chunks/cache keys) across restarts; default size cap 64 MiB. Multiple MCP processes may share one dir — **no** cross-process lock; TTL/eviction are best-effort. Prefer a dedicated dir per user/host. |
302
- | `COMPENDIUM_CACHE_MAX_BYTES` | _(unset / 64MiB with dir)_ | Soft cap on total cached payload bytes |
303
- | `RUST_LOG` | `compendium=info` | Logs on **stderr** only |
304
-
305
- ## Example tool calls
306
-
307
- All calls use the single tool **`compendium`** with an `action` field.
308
-
309
- **Filter noisy terminal output**
310
-
311
- ```json
312
- {
313
- "action": "filter",
314
- "text": "\u001b[31mERROR\u001b[0m boom\n\n\nINFO ok",
315
- "filter": {
316
- "strip_ansi": true,
317
- "keep_patterns": ["ERROR|WARN"]
318
- }
319
- }
320
- ```
321
-
322
- **Compress a large log**
323
-
324
- ```json
325
- {
326
- "action": "compress",
327
- "text": "...",
328
- "compress": {
329
- "content_type": "log",
330
- "max_tokens": 512
331
- }
332
- }
333
- ```
334
-
335
- **Chunk a document into references**
336
-
337
- ```json
338
- {
339
- "action": "chunk",
340
- "text": "... huge file ...",
341
- "chunk": {
342
- "source": "file:///path/to/doc.md",
343
- "chunk_tokens": 400,
344
- "overlap_tokens": 40
345
- }
346
- }
347
- ```
348
-
349
- Prefer the returned `index_text` in the model context; pull individual chunk contents by id only when needed.
350
-
351
- **Query-aware filter (local SLM or heuristic fallback)**
352
-
353
- ```json
354
- {
355
- "action": "filter_relevant",
356
- "text": "... noisy cargo/test log ...",
357
- "query": "why did the auth tests fail",
358
- "smart": { "max_tokens": 512, "fallback": true }
359
- }
360
- ```
361
-
362
- Without `COMPENDIUM_LOCAL_LLM_URL`, `summarize_smart` / `filter_relevant` automatically use heuristics and set `backend: "heuristic"` plus `fallback_reason` in the result.
363
-
364
- **Pack a workspace briefing for a fresh agent turn**
365
-
366
- ```json
367
- {
368
- "action": "brief",
369
- "query": "fix the OAuth refresh token path",
370
- "brief": {
371
- "root": "/path/to/repo",
372
- "max_files": 40,
373
- "top_k_chunks": 12,
374
- "max_brief_tokens": 2048
375
- }
376
- }
377
- ```
378
-
379
- Start the new turn with the returned `briefing` (or `cache_get` the `cache_key`). The host should not paste the whole tree into the prompt first. Treat Status as a starter synthesis — verify Caveats and Read next before large edits.
380
-
381
- ## Local small language model
90
+ | `COMPENDIUM_CHARS_PER_TOKEN` | `4.0` | Heuristic token estimate |
91
+ | `COMPENDIUM_TOKENIZER` | `cl100k_base` | BPE encoding when built with `--features real-tokens` |
92
+ | `COMPENDIUM_BINARY` | _(unset)_ | npm launcher: use this binary instead of downloading |
93
+ | `RUST_LOG` | `compendium=info` | Logs go to stderr only |
382
94
 
383
- Follow [Quick start §2](#2-optional-smarter-summaries-with-ollama) (`npx -y compendium-mcp setup-ollama`).
384
-
385
- Rules of thumb:
386
-
387
- - **Only loopback** URLs (`127.0.0.1`, `::1`, `localhost`) — no cloud endpoints.
388
- - Without `COMPENDIUM_LOCAL_LLM_URL`, smart actions use heuristics and set `backend: "heuristic"`.
389
- - Calls use `temperature=0` and `seed=0` for stable outputs.
390
- - Lemonade example: `COMPENDIUM_LOCAL_LLM_URL=http://127.0.0.1:13305/api/v1` and `COMPENDIUM_LOCAL_LLM_MODEL=Qwen3-4B-GGUF`.
391
- - llama.cpp OpenAI server: same pattern — set URL to its `/v1` base and the served model id.
392
-
393
- ## Develop / test
95
+ ## Build from source
394
96
 
395
97
  ```bash
98
+ cargo build --release # heuristic token counts
99
+ cargo build --release --features real-tokens # exact BPE counts via tiktoken
396
100
  cargo test
397
- cargo test --features real-tokens
398
- cargo test --features http --test http_smoke
399
- cargo test --test e2e_smoke
400
- cargo run --features http -- http 127.0.0.1:8788
401
101
  ```
402
102
 
403
- `e2e_smoke` spawns `CARGO_BIN_EXE_compendium`, completes MCP connect (legacy initialize) over stdio, lists tools, then calls gateway actions. `http_smoke` (requires `--features http`) exercises sessionless streamable HTTP in-process.
103
+ Point your MCP config at `target/release/compendium` directly, or keep `npx` and set `COMPENDIUM_BINARY`.
404
104
 
405
- ## Design notes
105
+ ## Design
106
+
107
+ - **Deterministic by default.** Heuristic paths need no network and produce byte-identical output, which keeps prompt prefix caches warm.
108
+ - **Local-first.** Smart actions only ever call a loopback URL; there is no cloud path.
109
+ - **stdout is JSON-RPC.** All logging goes to stderr.
110
+
111
+ ## Community
406
112
 
407
- - **Deterministic by default** heuristic pipeline needs no network; smart actions only call a configured **local** OpenAI-compatible URL and fall back to heuristics when unset or failing.
408
- - **Token backends** — fast heuristic by default; opt into exact BPE with `real-tokens`.
409
- - **Zero stdout pollution** (stdio mode) — tracing goes to stderr so JSON-RPC framing stays clean.
410
- - **Release profile** — LTO + stripped binary for low footprint.
113
+ [Contributing](CONTRIBUTING.md) · [Changelog](CHANGELOG.md) · [Architecture](docs/architecture.md) · [Security](SECURITY.md) · [Support](SUPPORT.md) · [Code of Conduct](CODE_OF_CONDUCT.md)
411
114
 
412
115
  ## License
413
116
 
package/bin/run.js CHANGED
@@ -2,25 +2,17 @@
2
2
  'use strict';
3
3
 
4
4
  /**
5
- * Compendium MCP dispatcher.
5
+ * Compendium MCP launcher.
6
6
  *
7
- * Resolves the native binary and exec's it with inherited stdio so Cursor /
8
- * Claude Desktop / `npx compendium-mcp` talk JSON-RPC over stdin/stdout.
9
- *
10
- * Resolution order:
11
- * 1. COMPENDIUM_BINARY env
12
- * 2. Dev build: <repo>/target/release|debug/compendium(.exe)
13
- * 3. optionalDependency platform package (compendium-mcp-<platform>)
14
- * 4. Cached GitHub Release download (~/.cache/compendium-mcp/<version>/)
7
+ * Finds (or downloads once) the native `compendium` binary for this platform
8
+ * and runs it with inherited stdio, so MCP clients talk JSON-RPC over stdin/stdout.
15
9
  */
16
10
 
17
11
  const { spawn } = require('child_process');
18
12
  const fs = require('fs');
19
13
  const https = require('https');
20
- const http = require('http');
21
14
  const path = require('path');
22
15
  const { pipeline } = require('stream/promises');
23
- const { createWriteStream } = require('fs');
24
16
 
25
17
  const {
26
18
  packageVersion,
@@ -31,46 +23,28 @@ const {
31
23
  platformKey,
32
24
  } = require('../npm/lib/platform');
33
25
 
34
- function existsExecutable(file) {
26
+ function isExecutable(file) {
35
27
  try {
36
28
  fs.accessSync(file, fs.constants.F_OK);
37
- // On Windows, X_OK is unreliable; presence is enough.
38
- if (process.platform !== 'win32') {
39
- fs.accessSync(file, fs.constants.X_OK);
40
- }
29
+ if (process.platform !== 'win32') fs.accessSync(file, fs.constants.X_OK);
41
30
  return true;
42
31
  } catch {
43
32
  return false;
44
33
  }
45
34
  }
46
35
 
47
- function tryRequirePlatformBinary() {
48
- const spec = currentPlatform();
49
- try {
50
- // optionalDependency installs next to this package when available.
51
- const pkgRoot = path.dirname(require.resolve(`${spec.pkg}/package.json`));
52
- const candidate = path.join(pkgRoot, 'bin', binaryName(spec));
53
- if (existsExecutable(candidate)) return candidate;
54
- } catch {
55
- // optional dep missing — fall through
56
- }
57
- return null;
58
- }
59
-
60
- function tryDevBinary() {
36
+ function devBinary() {
61
37
  const repoRoot = path.resolve(__dirname, '..');
62
- const name = process.platform === 'win32' ? 'compendium.exe' : 'compendium';
63
- const release = path.join(repoRoot, 'target', 'release', name);
64
- const debug = path.join(repoRoot, 'target', 'debug', name);
65
- if (existsExecutable(release)) return release;
66
- if (existsExecutable(debug)) return debug;
38
+ for (const profile of ['release', 'debug']) {
39
+ const candidate = path.join(repoRoot, 'target', profile, binaryName());
40
+ if (isExecutable(candidate)) return candidate;
41
+ }
67
42
  return null;
68
43
  }
69
44
 
70
- function download(url, dest) {
45
+ function download(url, dest, hops = 0) {
71
46
  return new Promise((resolve, reject) => {
72
- const client = url.startsWith('https:') ? https : http;
73
- const req = client.get(
47
+ const req = https.get(
74
48
  url,
75
49
  {
76
50
  headers: {
@@ -79,14 +53,10 @@ function download(url, dest) {
79
53
  },
80
54
  },
81
55
  (res) => {
82
- // Follow one redirect hop (GitHub release assets).
83
- if (
84
- res.statusCode >= 300 &&
85
- res.statusCode < 400 &&
86
- res.headers.location
87
- ) {
56
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
88
57
  res.resume();
89
- download(res.headers.location, dest).then(resolve, reject);
58
+ if (hops >= 5) return reject(new Error('too many redirects'));
59
+ download(res.headers.location, dest, hops + 1).then(resolve, reject);
90
60
  return;
91
61
  }
92
62
  if (res.statusCode !== 200) {
@@ -94,19 +64,16 @@ function download(url, dest) {
94
64
  reject(
95
65
  new Error(
96
66
  `Download failed (${res.statusCode}) for ${url}. ` +
97
- `Ensure release assets exist for ${platformKey()}.`
67
+ `No release asset for ${platformKey()}? Build from source and set COMPENDIUM_BINARY.`
98
68
  )
99
69
  );
100
70
  return;
101
71
  }
102
72
  const tmp = `${dest}.partial`;
103
- const out = createWriteStream(tmp);
104
- pipeline(res, out)
73
+ pipeline(res, fs.createWriteStream(tmp))
105
74
  .then(() => {
106
75
  fs.renameSync(tmp, dest);
107
- if (process.platform !== 'win32') {
108
- fs.chmodSync(dest, 0o755);
109
- }
76
+ if (process.platform !== 'win32') fs.chmodSync(dest, 0o755);
110
77
  resolve(dest);
111
78
  })
112
79
  .catch((err) => {
@@ -123,44 +90,29 @@ function download(url, dest) {
123
90
  });
124
91
  }
125
92
 
126
- async function ensureDownloadedBinary() {
93
+ async function downloadedBinary() {
127
94
  const spec = currentPlatform();
128
95
  const version = packageVersion();
129
96
  const dir = cacheDir(version);
130
97
  fs.mkdirSync(dir, { recursive: true });
131
- const dest = path.join(dir, binaryName(spec));
132
- if (existsExecutable(dest)) return dest;
133
-
134
- const repo = githubRepo();
135
- const tag = `v${version}`;
136
- const url = `https://github.com/${repo}/releases/download/${tag}/${spec.asset}`;
98
+ const dest = path.join(dir, binaryName());
99
+ if (isExecutable(dest)) return dest;
137
100
 
138
- process.stderr.write(
139
- `[compendium-mcp] Downloading ${spec.asset} (${tag})…\n`
140
- );
101
+ const url = `https://github.com/${githubRepo()}/releases/download/v${version}/${spec.asset}`;
102
+ process.stderr.write(`[compendium-mcp] downloading ${spec.asset} v${version}…\n`);
141
103
  await download(url, dest);
142
- process.stderr.write(`[compendium-mcp] Cached at ${dest}\n`);
143
104
  return dest;
144
105
  }
145
106
 
146
107
  async function resolveBinary() {
147
108
  if (process.env.COMPENDIUM_BINARY) {
148
109
  const forced = path.resolve(process.env.COMPENDIUM_BINARY);
149
- if (!existsExecutable(forced)) {
110
+ if (!isExecutable(forced)) {
150
111
  throw new Error(`COMPENDIUM_BINARY not executable: ${forced}`);
151
112
  }
152
113
  return forced;
153
114
  }
154
-
155
- // Prefer a local Cargo build when developing from this repo so `cargo build
156
- // --release` is picked up immediately (optional platform packages can lag).
157
- const fromDev = tryDevBinary();
158
- if (fromDev) return fromDev;
159
-
160
- const fromOptional = tryRequirePlatformBinary();
161
- if (fromOptional) return fromOptional;
162
-
163
- return ensureDownloadedBinary();
115
+ return devBinary() || downloadedBinary();
164
116
  }
165
117
 
166
118
  async function main() {
@@ -172,28 +124,24 @@ async function main() {
172
124
  process.exit(1);
173
125
  }
174
126
 
175
- process.stderr.write(`[compendium-mcp] using ${binary}\n`);
176
-
177
127
  const child = spawn(binary, process.argv.slice(2), {
178
128
  stdio: 'inherit',
179
129
  windowsHide: true,
180
130
  });
181
131
 
182
132
  child.on('error', (err) => {
183
- process.stderr.write(`[compendium-mcp] failed to spawn ${binary}: ${err.message}\n`);
133
+ process.stderr.write(`[compendium-mcp] failed to start ${binary}: ${err.message}\n`);
184
134
  process.exit(1);
185
135
  });
186
136
 
187
- const forward = (signal) => {
188
- if (!child.killed) child.kill(signal);
189
- };
190
- process.on('SIGINT', () => forward('SIGINT'));
191
- process.on('SIGTERM', () => forward('SIGTERM'));
137
+ for (const signal of ['SIGINT', 'SIGTERM']) {
138
+ process.on(signal, () => {
139
+ if (!child.killed) child.kill(signal);
140
+ });
141
+ }
192
142
 
193
143
  child.on('exit', (code, signal) => {
194
- if (signal) {
195
- process.exit(signal === 'SIGINT' ? 130 : 1);
196
- }
144
+ if (signal) process.exit(signal === 'SIGINT' ? 130 : 1);
197
145
  process.exit(code ?? 1);
198
146
  });
199
147
  }
@@ -1,157 +1,59 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Shared platform package / release-asset mapping for Compendium npm distribution.
4
+ * PlatformGitHub Release asset mapping for the `compendium-mcp` npm wrapper.
5
5
  *
6
- * Distribution strategy (hybrid):
7
- * 1. Prefer optionalDependency platform packages (esbuild-style) — offline, fast.
8
- * 2. Fall back to downloading the matching GitHub Release asset into a local cache.
9
- * 3. Dev override: COMPENDIUM_BINARY or ./target/release/compendium.
10
- * 4. Force key: COMPENDIUM_PLATFORM (e.g. linux-x64-musl).
6
+ * Binary resolution (see bin/run.js):
7
+ * 1. COMPENDIUM_BINARY env override
8
+ * 2. Dev build: <repo>/target/release|debug/compendium
9
+ * 3. Cached download from GitHub Releases (~/.cache/compendium-mcp/<version>/)
11
10
  */
12
11
 
13
- const fs = require('fs');
14
12
  const os = require('os');
15
13
  const path = require('path');
16
- const { execSync } = require('child_process');
17
14
 
18
- /** @typedef {{ pkg: string, asset: string, rustTarget: string }} PlatformSpec */
19
-
20
- /** @type {Record<string, PlatformSpec>} */
15
+ /** @type {Record<string, { asset: string }>} */
21
16
  const PLATFORMS = {
22
- 'darwin-arm64': {
23
- pkg: 'compendium-mcp-darwin-arm64',
24
- asset: 'compendium-darwin-arm64',
25
- rustTarget: 'aarch64-apple-darwin',
26
- },
27
- 'darwin-x64': {
28
- pkg: 'compendium-mcp-darwin-x64',
29
- asset: 'compendium-darwin-x64',
30
- rustTarget: 'x86_64-apple-darwin',
31
- },
32
- 'linux-x64': {
33
- pkg: 'compendium-mcp-linux-x64',
34
- asset: 'compendium-linux-x64',
35
- rustTarget: 'x86_64-unknown-linux-gnu',
36
- },
37
- 'linux-x64-musl': {
38
- pkg: 'compendium-mcp-linux-x64-musl',
39
- asset: 'compendium-linux-x64-musl',
40
- rustTarget: 'x86_64-unknown-linux-musl',
41
- },
42
- 'linux-arm64': {
43
- pkg: 'compendium-mcp-linux-arm64',
44
- asset: 'compendium-linux-arm64',
45
- rustTarget: 'aarch64-unknown-linux-gnu',
46
- },
47
- 'win32-x64': {
48
- pkg: 'compendium-mcp-win32-x64',
49
- asset: 'compendium-win32-x64.exe',
50
- rustTarget: 'x86_64-pc-windows-msvc',
51
- },
52
- 'win32-arm64': {
53
- pkg: 'compendium-mcp-win32-arm64',
54
- asset: 'compendium-win32-arm64.exe',
55
- rustTarget: 'aarch64-pc-windows-msvc',
56
- },
17
+ 'darwin-arm64': { asset: 'compendium-darwin-arm64' },
18
+ 'darwin-x64': { asset: 'compendium-darwin-x64' },
19
+ 'linux-x64': { asset: 'compendium-linux-x64' },
20
+ 'linux-arm64': { asset: 'compendium-linux-arm64' },
21
+ 'win32-x64': { asset: 'compendium-win32-x64.exe' },
57
22
  };
58
23
 
59
24
  function packageVersion() {
60
- // Keep in sync with root package.json / Cargo.toml at publish time.
61
25
  return require('../../package.json').version;
62
26
  }
63
27
 
64
28
  function githubRepo() {
65
- return (
66
- process.env.COMPENDIUM_GITHUB_REPO ||
67
- process.env.GITHUB_REPOSITORY ||
68
- 'hocestnonsatis/Compendium'
69
- );
29
+ return process.env.COMPENDIUM_GITHUB_REPO || 'hocestnonsatis/Compendium';
70
30
  }
71
31
 
72
- /** Best-effort musl detection (Alpine / static libc). Default is glibc. */
73
- function isLinuxMusl() {
74
- if (process.platform !== 'linux') return false;
75
- try {
76
- if (typeof process.report?.getReport === 'function') {
77
- const report = process.report.getReport();
78
- if (report?.header?.glibcVersionRuntime) return false;
79
- }
80
- } catch (_) {
81
- /* ignore */
82
- }
83
- try {
84
- if (fs.existsSync('/etc/alpine-release')) return true;
85
- } catch (_) {
86
- /* ignore */
87
- }
88
- try {
89
- const out = execSync('ldd --version 2>&1 || true', {
90
- encoding: 'utf8',
91
- stdio: ['ignore', 'pipe', 'pipe'],
92
- });
93
- if (/musl/i.test(out)) return true;
94
- } catch (_) {
95
- /* ignore */
96
- }
97
- return false;
98
- }
99
-
100
- /**
101
- * Normalize Node's process.platform + process.arch into our key.
102
- * Override with COMPENDIUM_PLATFORM when needed (e.g. linux-x64-musl).
103
- * @returns {string}
104
- */
105
32
  function platformKey() {
106
33
  const forced = (process.env.COMPENDIUM_PLATFORM || '').trim();
107
34
  if (forced) return forced;
108
-
109
- const platform = process.platform;
110
- let arch = process.arch;
111
- // Rosetta / rare aliases
112
- if (arch === 'ia32') arch = 'x64';
113
-
114
- if (platform === 'linux' && arch === 'x64' && isLinuxMusl()) {
115
- return 'linux-x64-musl';
116
- }
117
- return `${platform}-${arch}`;
35
+ const arch = process.arch === 'ia32' ? 'x64' : process.arch;
36
+ return `${process.platform}-${arch}`;
118
37
  }
119
38
 
120
- /**
121
- * @returns {PlatformSpec}
122
- */
123
39
  function currentPlatform() {
124
40
  const key = platformKey();
125
41
  const spec = PLATFORMS[key];
126
42
  if (!spec) {
127
- const supported = Object.keys(PLATFORMS).join(', ');
128
43
  throw new Error(
129
- `Unsupported platform "${key}". Supported: ${supported}.\n` +
130
- `Set COMPENDIUM_BINARY to a local build, COMPENDIUM_PLATFORM to a known key, or open an issue for this target.`
44
+ `Unsupported platform "${key}". Supported: ${Object.keys(PLATFORMS).join(', ')}.\n` +
45
+ `Build from source (cargo build --release) and set COMPENDIUM_BINARY to the binary.`
131
46
  );
132
47
  }
133
48
  return spec;
134
49
  }
135
50
 
136
- /**
137
- * Binary filename inside a platform package / cache.
138
- * @param {PlatformSpec} [spec]
139
- */
140
- function binaryName(spec = currentPlatform()) {
141
- return process.platform === 'win32' || spec.asset.endsWith('.exe')
142
- ? 'compendium.exe'
143
- : 'compendium';
51
+ function binaryName() {
52
+ return process.platform === 'win32' ? 'compendium.exe' : 'compendium';
144
53
  }
145
54
 
146
- /**
147
- * Cache directory for lazily downloaded binaries.
148
- * @param {string} version
149
- */
150
55
  function cacheDir(version = packageVersion()) {
151
- const base =
152
- process.env.COMPENDIUM_CACHE_DIR ||
153
- process.env.XDG_CACHE_HOME ||
154
- path.join(os.homedir(), '.cache');
56
+ const base = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
155
57
  return path.join(base, 'compendium-mcp', version);
156
58
  }
157
59
 
@@ -163,5 +65,4 @@ module.exports = {
163
65
  currentPlatform,
164
66
  binaryName,
165
67
  cacheDir,
166
- isLinuxMusl,
167
68
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compendium-mcp",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "MCP server that compresses, summarizes, and filters context to minimize LLM token usage",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -34,21 +34,7 @@
34
34
  "LICENSE"
35
35
  ],
36
36
  "scripts": {
37
- "check-npm-gates": "node npm/scripts/check-versions-selftest.js && node npm/scripts/check-versions.js && node npm/scripts/check-residual-npm.js",
38
- "check-versions": "node npm/scripts/check-versions.js",
39
- "check-versions-selftest": "node npm/scripts/check-versions-selftest.js",
40
- "check-residual-npm": "node npm/scripts/check-residual-npm.js",
41
- "prepack": "node npm/scripts/check-wrapper.js && node npm/scripts/check-versions.js",
42
- "prepare": "node npm/scripts/link-bins.js"
43
- },
44
- "optionalDependencies": {
45
- "compendium-mcp-darwin-arm64": "0.6.3",
46
- "compendium-mcp-darwin-x64": "0.6.3",
47
- "compendium-mcp-linux-x64": "0.6.3",
48
- "compendium-mcp-linux-x64-musl": "0.6.3",
49
- "compendium-mcp-linux-arm64": "0.6.3",
50
- "compendium-mcp-win32-x64": "0.6.3",
51
- "compendium-mcp-win32-arm64": "0.6.3"
52
- },
53
- "preferUnplugged": true
37
+ "check-version": "node npm/scripts/check-version.js",
38
+ "prepack": "node npm/scripts/check-version.js"
39
+ }
54
40
  }