compendium-mcp 0.6.2 → 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,124 +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`**.
46
-
47
- That alone is enough: filter, compress, summarize, cache, and BM25 actions all work **without** a local model (fast heuristics).
28
+ Reload MCP. You should see a single tool named `compendium`. That's it.
48
29
 
49
- ### 2. (Optional) Smarter summaries with Ollama
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`.
50
31
 
51
- Want better `summarize_smart` / `filter_relevant`? Run a small model on your machine and point Compendium at it.
32
+ ### Optional: smarter summaries with Ollama
52
33
 
53
- 1. Install [Ollama](https://ollama.com/) and start it (default: `http://127.0.0.1:11434`).
54
- 2. Pull a chat model, for example:
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:
55
35
 
56
36
  ```bash
57
- ollama pull qwen:latest
58
- # or a smaller one: ollama pull qwen2.5:3b
59
- ```
60
-
61
- 3. Extend the MCP `env` block (URL must stay on **localhost** — Compendium blocks remote hosts on purpose):
62
-
63
- ```json
64
- {
65
- "mcpServers": {
66
- "compendium": {
67
- "command": "npx",
68
- "args": ["-y", "compendium-mcp"],
69
- "env": {
70
- "COMPENDIUM_LOCAL_LLM_URL": "http://127.0.0.1:11434/v1",
71
- "COMPENDIUM_LOCAL_LLM_MODEL": "qwen:latest"
72
- }
73
- }
74
- }
75
- }
37
+ npx -y compendium-mcp setup-ollama --write-mcp
76
38
  ```
77
39
 
78
- 4. Reload MCP, then ask the agent to call `compendium` with `action: "summarize_smart"`.
79
- In the result, `"backend": "local_llm"` means Ollama answered; `"heuristic"` means it fell back (Ollama down, wrong model name, or URL missing).
80
-
81
- **Notes**
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"`.
82
41
 
83
- - Package name on npm is **`compendium-mcp`** (`compendium` was already taken). The CLI binary name is still `compendium`.
84
- - First Ollama reply can be slow while the model loads; later calls are faster.
85
- - Other local OpenAI-compatible servers work the same way (e.g. Lemonade `http://127.0.0.1:13305/api/v1`). See [Environment](#environment).
42
+ Any OpenAI-compatible **loopback** server works (llama.cpp, Lemonade, …): set the two env vars yourself. Remote hosts are rejected on purpose.
86
43
 
87
- Smoke-check from a terminal (any folder **except** this git repo root is fine):
88
-
89
- ```bash
90
- npx -y compendium-mcp --help
91
- ```
44
+ ## Actions
92
45
 
93
- 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?` |
94
62
 
95
- ## 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`.
96
64
 
97
- - [Contributing](CONTRIBUTING.md)
98
- - [Changelog](CHANGELOG.md)
99
- - [Architecture](docs/architecture.md)
100
- - [Code of Conduct](CODE_OF_CONDUCT.md)
101
- - [Security policy](SECURITY.md)
102
- - [Support](SUPPORT.md)
103
-
104
- ## Transports
105
-
106
- | Mode | Command | Notes |
107
- |------|---------|-------|
108
- | **stdio** (default) | `compendium` / `compendium stdio` | Cursor / Claude Desktop — dual-compat (legacy initialize or modern connect) |
109
- | **Streamable HTTP** | `compendium http [BIND]` | Requires `--features http`. Endpoint: `http://{bind}/mcp`. Sessionless (`2026-07-28`); JSON preferred, SSE fallback |
110
-
111
- 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`.
112
-
113
- ## Tools
114
-
115
- 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.
116
-
117
- | `action` | Purpose | Main fields |
118
- |----------|---------|-------------|
119
- | `filter` | Strip ANSI, boilerplate, whitespace; densify JSON; keep/drop regexes | `text`, `filter` — not for cargo/npm dumps (`compress_output`) |
120
- | `compress` | Dense representation of text/code/logs | `text`, `compress` — soft inputs under ~1000 chars bypass unless `force` |
121
- | `compress_output` | Domain-aware stdout/stderr scrub (git, cargo, npm, docker, …) | `text`, `output` — prefer when CLI domain is known |
122
- | `summarize` | Hierarchical summary (conversation / file tree / outline) | `text`, `summarize` |
123
- | `summarize_smart` | Local-SLM dense summary (heuristic fallback if unset/fails) | `text`, `smart?`, `summarize?` |
124
- | `filter_relevant` | Query-aware keep of relevant lines (local SLM + heuristic fallback) | `text`, `query`, `smart?` |
125
- | `prune_history` | Drop filler / compress older chat turns | `text` or `messages`, `prune` |
126
- | `chunk` | Split into `cmp://` chunks (session-cached) | `text`, `chunk` |
127
- | `resolve` | Fetch chunk content by id | `id` (+ optional `map` / `text`) |
128
- | `count_tokens` | Measure tokens | `text` |
129
- | `stats` | Session savings + latency/bypass/backend telemetry | `reset?` — see playbook `stats-debug` |
130
- | `cache_store` | Park bulky payload outside the prompt | `text`, `cache` |
131
- | `cache_get` | Retrieve by key | `key` |
132
- | `cache_invalidate` | Drop one key or clear cache | `key?` |
133
- | `sanitize` | Redact secrets + neutralize IPI phrases | `text`, `sanitize?` — or `sanitize_input` |
134
- | `rerank` | BM25 (+ optional loopback embeddings + opt-in SLM cross-encoder) rank candidates / chunks | `query`, `items` or `text` or chunk `map`, `rerank?` |
135
- | `brief` | Scan a workspace; pack a structured starter briefing + cache key | `query`, `brief?` (`root`, caps), optional `text` hint |
136
- | `catalog` | Short action (+ playbook) ads; prefer before guessing | _(none)_ — call first when unsure |
137
- | `help` | Usage notes for one action (default **compressed**; `force: true` → full) | `id`, `force?` |
138
- | `playbooks` | List playbook ads | _(none)_ |
139
- | `playbook` | Load one playbook body | `id` |
140
- | `pack` | Zip text/files into a bounded archive | `text` or `items`, `pack?` |
141
- | `unpack` | Unpack zip with size caps into chunks (never runs scripts) | `text` or `key`, `pack?` |
142
- | `llm_status` | Probe configured local LLM (models; `force` = chat ping) | `force?` — when smart/hybrid unexpectedly heuristic |
143
-
144
- ### Progressive disclosure (skills)
145
-
146
- Tool description/instructions stay thin. Discover details on demand:
147
-
148
- - **Tool bridge:** `action=catalog` → `action=help` with `id`, or `playbooks` → `playbook`
149
- - **MCP resources:** `resources/list` / `resources/read` on:
150
- - `cmp://skill/index` — JSON index of actions + playbooks
151
- - `cmp://skill/action/{name}` — full action help (markdown)
152
- - `cmp://skill/playbook/{id}` — playbook body
153
-
154
- 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).
155
-
156
- 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`.
157
-
158
- `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`).
159
-
160
- `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.
161
-
162
- Example — noisy log (canonical first call after install):
65
+ Example:
163
66
 
164
67
  ```json
165
68
  {
@@ -169,254 +72,45 @@ Example — noisy log (canonical first call after install):
169
72
  }
170
73
  ```
171
74
 
172
- Discover more without reading this README: `{"action":"catalog"}` then `{"action":"help","id":"compress_output"}`. Sample payloads: [`examples/`](examples/).
173
-
174
- Response envelope: `{ "ok": true, "action": "filter", "result_json": "{...}" }`. Parse `result_json` as JSON for the action-specific payload.
175
-
176
- ## Project layout
177
-
178
- ```
179
- assets/ # brand mark (SVG/PNG); baked into MCP icons via data URI
180
- docs/ # architecture notes
181
- examples/ # sample MCP tool-call JSON payloads
182
- testdata/ # eval fixtures (logs, audit, PR JSON, untrusted paste, …)
183
- src/
184
- main.rs # CLI: stdio | http
185
- lib.rs
186
- brand.rs # SEP-973 icons for serverInfo + tool
187
- config.rs # COMPENDIUM_* env config
188
- server/ # MCP tool + resources + action handlers (rmcp)
189
- http.rs # Streamable HTTP, sessionless (feature = "http")
190
- pipeline/
191
- brief/ # workspace brief (walk / window / pack / synthesize)
192
- tokens.rs # heuristic or tiktoken BPE (feature = "real-tokens")
193
- filter.rs
194
- compress.rs
195
- summarize.rs
196
- smart.rs # summarize_smart + filter_relevant
197
- local_llm.rs # OpenAI-compatible local SLM client (+ embed cache)
198
- chunk.rs # chunk + resolve
199
- cache.rs # session key/value cache (+ optional disk / embed vectors)
200
- catalog.rs # action ads + help (progressive disclosure)
201
- playbook.rs # bundled / dir playbooks
202
- pack.rs # zip pack/unpack with size caps
203
- stats.rs # session savings counters
204
- prune.rs # conversation history pruning
205
- output.rs # domain-aware compress_output
206
- playbooks/ # embedded skill-md playbooks
207
- tests/
208
- integration.rs
209
- e2e_smoke.rs # spawns binary, MCP handshake, tools + resources
210
- eval_regression.rs # B1 heuristic quality + latency smoke
211
- CHANGELOG.md
212
- REPORT.md # design essay + Shipped (A–C) / Next ops / Deferred roadmap
213
- ```
214
-
215
- ## Build
216
-
217
- ```bash
218
- # Default: heuristic tokens + stdio only
219
- cargo build --release
220
-
221
- # Exact BPE token counts (tiktoken-rs)
222
- cargo build --release --features real-tokens
223
-
224
- # Streamable HTTP transport
225
- cargo build --release --features http
226
-
227
- # Everything
228
- cargo build --release --features real-tokens,http
229
- ```
230
-
231
- Binary: `target/release/compendium`
232
-
233
- ## Configure (advanced)
234
-
235
- The [Quick start](#quick-start-cursor) config is enough for most people. Extra options:
236
-
237
- ### Claude Desktop
238
-
239
- Same `command` / `args` / `env` as Cursor, in Claude’s MCP config file.
240
-
241
- ### Optional tuning env
242
-
243
- ```json
244
- "env": {
245
- "RUST_LOG": "compendium=info",
246
- "COMPENDIUM_DEFAULT_MAX_TOKENS": "2048",
247
- "COMPENDIUM_TOKENIZER": "cl100k_base",
248
- "COMPENDIUM_LOCAL_LLM_URL": "http://127.0.0.1:11434/v1",
249
- "COMPENDIUM_LOCAL_LLM_MODEL": "qwen:latest"
250
- }
251
- ```
252
-
253
- ### Local Cargo binary (developers)
254
-
255
- After code changes, rebuild and **reload MCP** so the live tool schema matches source (avoid stale `npx`/Release binaries during development):
256
-
257
- ```bash
258
- cargo build --release --features real-tokens,http
259
- ```
260
-
261
- ```json
262
- {
263
- "mcpServers": {
264
- "compendium": {
265
- "command": "/absolute/path/to/Compendium/target/release/compendium",
266
- "env": {
267
- "RUST_LOG": "compendium=info",
268
- "COMPENDIUM_DEFAULT_MAX_TOKENS": "2048"
269
- }
270
- }
271
- }
272
- }
273
- ```
274
-
275
- ### Remote / sidecar (HTTP)
276
-
277
- ```bash
278
- cargo run --features http -- http 127.0.0.1:8788
279
- # MCP endpoint: http://127.0.0.1:8788/mcp
280
- ```
281
-
282
- 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"}`.
283
76
 
284
77
  ## Environment
285
78
 
79
+ All optional.
80
+
286
81
  | Variable | Default | Meaning |
287
82
  |----------|---------|---------|
288
- | `COMPENDIUM_CHARS_PER_TOKEN` | `4.0` | Heuristic chars÷tokens (ignored with `real-tokens`) |
289
- | `COMPENDIUM_TOKENIZER` | `cl100k_base` | BPE encoding: `cl100k_base` or `o200k_base` (`real-tokens`) |
290
- | `COMPENDIUM_DEFAULT_MAX_TOKENS` | `2048` | Soft cap for compress |
291
- | `COMPENDIUM_MAX_BLANK_LINES` | `1` | Blank-line collapse limit |
292
- | `COMPENDIUM_SIMILARITY_THRESHOLD` | `0.85` | Jaccard line-dedupe threshold |
293
- | `COMPENDIUM_HTTP_BIND` | `127.0.0.1:8788` | Default HTTP listen address |
294
- | `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. |
295
- | `COMPENDIUM_LOCAL_LLM_MODEL` | `Qwen3-4B-GGUF` | Model id on that server (Ollama: e.g. `qwen:latest`) |
296
- | `COMPENDIUM_LOCAL_EMBED_MODEL` | _(same as chat)_ | Embeddings model for hybrid `rerank` / `brief` (e.g. `nomic-embed-text`) |
297
- | `COMPENDIUM_HYBRID_ALPHA` | `0.55` | BM25 weight in hybrid score (0–1); remainder is embedding cosine |
298
- | `COMPENDIUM_RERANK_CROSS_ENCODER` | _(off)_ | When `1`/`true`, `rerank` SLM-rescores top-N after BM25/hybrid |
299
- | `COMPENDIUM_CROSS_ENCODER_TOP_N` | `16` | Candidates passed to cross-encoder (clamped 4–64) |
300
- | `COMPENDIUM_AUDIT_PATH` | _(unset)_ | Append-only JSONL audit log (action metadata only; no payloads) |
301
- | `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 |
302
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 |
303
89
  | `COMPENDIUM_SIGNAL_MIN_CHARS` | `1000` | Bypass compress/summarize below this length (`0` disables) |
304
- | `COMPENDIUM_BRIEF_ROOT` | _(unset)_ | When set, `action=brief` may only scan roots under this canonical path |
305
- | `COMPENDIUM_PLAYBOOKS_DIR` | _(unset)_ | Extra/override playbook `*.md` directory (same `id` replaces embedded) |
306
- | `COMPENDIUM_ARCHIVE_MAX_BYTES` | `2097152` | Max compressed archive size for pack/unpack |
307
- | `COMPENDIUM_ARCHIVE_MAX_UNCOMPRESSED` | `4194304` | Max total uncompressed bytes for pack/unpack |
308
- | `COMPENDIUM_ARCHIVE_MAX_FILES` | `50` | Max files per archive |
309
- | `COMPENDIUM_SKILL_TTL_MS` | `300000` | Soft TTL (ms) on skill `resources/read` responses |
310
- | `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. |
311
- | `COMPENDIUM_CACHE_MAX_BYTES` | _(unset / 64MiB with dir)_ | Soft cap on total cached payload bytes |
312
- | `RUST_LOG` | `compendium=info` | Logs on **stderr** only |
313
-
314
- ## Example tool calls
315
-
316
- All calls use the single tool **`compendium`** with an `action` field.
317
-
318
- **Filter noisy terminal output**
319
-
320
- ```json
321
- {
322
- "action": "filter",
323
- "text": "\u001b[31mERROR\u001b[0m boom\n\n\nINFO ok",
324
- "filter": {
325
- "strip_ansi": true,
326
- "keep_patterns": ["ERROR|WARN"]
327
- }
328
- }
329
- ```
330
-
331
- **Compress a large log**
332
-
333
- ```json
334
- {
335
- "action": "compress",
336
- "text": "...",
337
- "compress": {
338
- "content_type": "log",
339
- "max_tokens": 512
340
- }
341
- }
342
- ```
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 |
343
94
 
344
- **Chunk a document into references**
345
-
346
- ```json
347
- {
348
- "action": "chunk",
349
- "text": "... huge file ...",
350
- "chunk": {
351
- "source": "file:///path/to/doc.md",
352
- "chunk_tokens": 400,
353
- "overlap_tokens": 40
354
- }
355
- }
356
- ```
357
-
358
- Prefer the returned `index_text` in the model context; pull individual chunk contents by id only when needed.
359
-
360
- **Query-aware filter (local SLM or heuristic fallback)**
361
-
362
- ```json
363
- {
364
- "action": "filter_relevant",
365
- "text": "... noisy cargo/test log ...",
366
- "query": "why did the auth tests fail",
367
- "smart": { "max_tokens": 512, "fallback": true }
368
- }
369
- ```
370
-
371
- Without `COMPENDIUM_LOCAL_LLM_URL`, `summarize_smart` / `filter_relevant` automatically use heuristics and set `backend: "heuristic"` plus `fallback_reason` in the result.
372
-
373
- **Pack a workspace briefing for a fresh agent turn**
374
-
375
- ```json
376
- {
377
- "action": "brief",
378
- "query": "fix the OAuth refresh token path",
379
- "brief": {
380
- "root": "/path/to/repo",
381
- "max_files": 40,
382
- "top_k_chunks": 12,
383
- "max_brief_tokens": 2048
384
- }
385
- }
386
- ```
387
-
388
- 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.
389
-
390
- ## Local small language model
391
-
392
- Follow [Quick start §2](#2-optional-smarter-summaries-with-ollama) for Ollama.
393
-
394
- Rules of thumb:
395
-
396
- - **Only loopback** URLs (`127.0.0.1`, `::1`, `localhost`) — no cloud endpoints.
397
- - Without `COMPENDIUM_LOCAL_LLM_URL`, smart actions use heuristics and set `backend: "heuristic"`.
398
- - Calls use `temperature=0` and `seed=0` for stable outputs.
399
- - Lemonade example: `COMPENDIUM_LOCAL_LLM_URL=http://127.0.0.1:13305/api/v1` and `COMPENDIUM_LOCAL_LLM_MODEL=Qwen3-4B-GGUF`.
400
- - llama.cpp OpenAI server: same pattern — set URL to its `/v1` base and the served model id.
401
-
402
- ## Develop / test
95
+ ## Build from source
403
96
 
404
97
  ```bash
98
+ cargo build --release # heuristic token counts
99
+ cargo build --release --features real-tokens # exact BPE counts via tiktoken
405
100
  cargo test
406
- cargo test --features real-tokens
407
- cargo test --features http --test http_smoke
408
- cargo test --test e2e_smoke
409
- cargo run --features http -- http 127.0.0.1:8788
410
101
  ```
411
102
 
412
- `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`.
413
104
 
414
- ## 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
415
112
 
416
- - **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.
417
- - **Token backends** — fast heuristic by default; opt into exact BPE with `real-tokens`.
418
- - **Zero stdout pollution** (stdio mode) — tracing goes to stderr so JSON-RPC framing stays clean.
419
- - **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)
420
114
 
421
115
  ## License
422
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.2",
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.2",
46
- "compendium-mcp-darwin-x64": "0.6.2",
47
- "compendium-mcp-linux-x64": "0.6.2",
48
- "compendium-mcp-linux-x64-musl": "0.6.2",
49
- "compendium-mcp-linux-arm64": "0.6.2",
50
- "compendium-mcp-win32-x64": "0.6.2",
51
- "compendium-mcp-win32-arm64": "0.6.2"
52
- },
53
- "preferUnplugged": true
37
+ "check-version": "node npm/scripts/check-version.js",
38
+ "prepack": "node npm/scripts/check-version.js"
39
+ }
54
40
  }