Git LFS for LLM context: a versioned, loss-aware context compression MCP server.
Project description
🗜️ SaveContext
Git LFS for your LLM context window.
Keep big documents outside the model — hand it a small searchable handle instead. The model reads a cheap summary to find what it needs, then pulls back the exact original words to quote, using a fraction of the tokens.
An MCP server for Claude Code, Claude Desktop, and any Model Context Protocol client — with a standalone CLI too.
ingest 150k-token contract ─▶ ctx://acme@v1
├─ ~700-token semantic brief
├─ block map (b0000 … b0042)
├─ protected atoms (money · dates · obligations …)
└─ raw source stored compressed, never auto-sent
ask 6 questions ─▶ lookup(...) ─▶ one call · top blocks · exact atoms
· verbatim quotes · confidence flag
Install
With uv, add SaveContext to Claude Code in one line — no
clone, no pip, no virtualenv:
claude mcp add savecontext -- uvx savecontext
Then open Claude Code, run /mcp to confirm the tools loaded, and point the model at
a big document and just ask — it ingests, looks up, and quotes on its own. To pin
where the database lives, add -e SAVECONTEXT_DB=/abs/path.db before the --.
Using a different MCP client (Claude Desktop, etc.)? Point its server config at the same
command: uvx savecontext. Prefer no MCP at all? The CLI does everything
standalone.
Why it's different
Most context tools are lossy summarizers — helpful, but they blur the exact numbers and can't tell you when they're guessing. SaveContext does the two things they can't:
- 🎯 Exact, with receipts. Facts come back as the original characters with their source location, never a paraphrase — every span reconstructs byte-for-byte. A summary is for finding; the quote is for trusting.
- 🙅 Says "not found" instead of inventing. When the answer isn't in the document, retrieval is flagged weak and the agent abstains — rather than hallucinating a plausible one.
- 📉 Flat cost. The payload to answer questions stays ~7,300 tokens whether the document is 15k or 150k. Pasting scales linearly; this doesn't.
- 🔁 One call, many questions.
lookupanswers a batch of queries in a single round trip, not one retrieve-cycle per question.
By the numbers
20.5×less contextat 150k tokens |
~7.3kflat token cost,any document size |
54/54live answers correct,incl. not found |
100%byte-exact quotes,0 hallucinations |
Measured with real agents on a contract set plus held-out documents (an RFC and a synthetic incident log the retriever was never tuned on), answering fact questions that have distractor values planted next to the true ones, graded against ground truth. Crossover is ≈ 5k tokens — below that, just paste; above it, the gap widens with size.
Honest note: live runs are single samples — treat small differences as noise. The durable findings are the flat-vs-scaling cost curve, byte-exact retrieval, and zero hallucination on absent facts. Dense technical prose is the current retrieval frontier. Reproduce it yourself:
examples/benchmark_suite/.
Reference
The 11 tools
All tools return clean JSON. Handles are stable and readable: ctx://label@vN for
contexts, out://label@vN for outputs.
| Tool | Purpose |
|---|---|
ingest |
Store long text; return handle, brief, block map, protected-atom summary, safety notes. Does not return the raw source. Optional agent_brief lets the model supply its own summary. |
lookup |
Batched retrieval — answer several queries in one call. Per query: the top block(s), the exact atoms inside them, a verbatim best-matching sentence, and a confidence flag (strong/weak) so the agent can abstain instead of guessing. |
brief |
Task-specific brief: relevant blocks + atoms for a given task, with uncertainty notes. |
expand |
Lazily expand a block / atom / section / search match at fidelity = summary | facts | quotes | full. |
quote |
Exact verbatim quote (by atom id or search query) with surrounding context and source ref. |
map |
Full (or paginated) block map + atom summary, fetched lazily. |
set_brief |
Replace a context's brief with an agent-authored one. Raw source and atoms unchanged. |
diff |
Diff new source against a stored context; report added/removed/changed atoms and meaning impact. Creates a new version. |
zip_output |
Store a large generated output; return section map + preview. |
expand_output |
Expand part of a stored output at fidelity = preview | section | full. |
audit |
Compression ratio, atom counts, per-category preservation estimate, safe_for / unsafe_for, warnings. |
expand fidelity levels: summary (extractive lead sentences, cheapest) · facts
(summary plus exact atoms, [money] $500,000) · quotes (verbatim spans you can
cite) · full (raw block text — the only path that returns source prose).
What a turn looks like
1. User pastes a 150,000-token contract.
2. Claude → ingest(source_text=…, label="acme")
← ctx://acme@v1, ~700-token brief, block map, atoms.
3. User: "Liability cap? Notice period? Governing law?"
4. Claude → lookup(ctx://acme@v1, ["liability cap", "termination notice", "governing law"])
← per query: the governing block, its atoms, a verbatim sentence, confidence.
5. Claude → quote(ctx://acme@v1, search_query="shall not exceed") # verify exact wording
← "…total liability shall not exceed $750,000…" + source offsets.
6. Claude answers using only the relevant, verbatim, cited context.
A runnable version is in examples/demo.py (python examples/demo.py).
Command line (savectx)
The package ships a CLI that shares the same store as the MCP server — ingest on one, query from the other:
savectx ingest report.md --label q4 --type auto # ingest a file
savectx ingest - --label pasted # or read stdin
savectx list # what's in the store
savectx lookup ctx://q4@v1 --query "liability cap" --query "fees" # batched
savectx brief ctx://q4@v1 --task "liability risks"
savectx expand ctx://q4@v1 --selector liability --fidelity facts
savectx quote ctx://q4@v1 --search "shall not exceed"
savectx audit ctx://q4@v1
savectx diff ctx://q4@v1 report_v2.md # version + change report
savectx serve # run the MCP server
Bare savecontext (and python -m savecontext) runs the MCP server, so MCP configs
keep working unchanged.
Configuration
Set via environment (or the MCP env block):
| Variable | Default | Purpose |
|---|---|---|
SAVECONTEXT_DB |
~/.savecontext/savecontext.db |
Store location. :memory: for ephemeral. Shared by CLI and server. |
SAVECONTEXT_TRANSPORT |
stdio |
stdio | http | sse |
SAVECONTEXT_HOST / SAVECONTEXT_PORT |
FastMCP defaults | For http/sse transport |
SAVECONTEXT_EMBEDDINGS |
off | 1 enables a local sentence-transformers retrieval backend (default is dependency-free BM25) |
SAVECONTEXT_LLM_SUMMARY |
off | 1 has a local Ollama model write an abstractive brief at ingest (fails closed to extractive) |
The ctx:// handle scheme is stable. SAVECONTEXT_* vars take precedence; the
pre-rename CONTEXTSAVER_* / CONTEXTVAULT_* names are still honored as fallbacks.
How it works (architecture + the loss-aware guarantee)
| Layer | Module | Behaviour |
|---|---|---|
| Tokenizer | tokenizer.py |
tiktoken cl100k_base, else chars/4 |
| Chunking | chunking.py |
deterministic: headings + paragraph packing to a token budget, exact offsets |
| Extraction | extraction.py |
regex atoms: dates, money, %, numbers, URLs, emails, entities, negations, obligations, code ids, file paths |
| Retrieval | retrieval.py |
BM25 + concept-group expansion + answer-type intents + saturation scoring; optional embedding backend behind the same rank interface |
| Summary | summarize.py |
extractive: ranked blocks + lead sentences + high-value atom lines; optional local-LLM |
| Diff | diffing.py |
atom-set diff + coarse block text diff + risk impact |
| Storage | storage.py |
SQLite metadata + zstd-compressed raw source; blocks/atoms as offsets |
| Service / Server | service.py, server.py |
the eleven tools; FastMCP over stdio / HTTP / SSE |
Loss-aware guarantee. Blocks and atoms are stored as offsets into the raw source,
so any span reconstructs byte-for-byte. The brief is explicitly flagged lossy; exact
wording always comes from quote() or expand(fidelity="quotes"). The full raw source
is only ever returned via expand(fidelity="full").
Who writes the brief?
Three summarizer options, best first — all keep the protected atoms exact; only the prose brief changes:
- The calling agent (recommended). Claude already has the document in context at
ingesttime, so it writes a faithful, dense brief for free — inline viaingest(agent_brief=…)or afterwards viaset_brief. (brief_mode='agent') - Local LLM (optional).
SAVECONTEXT_LLM_SUMMARY=1→ a local Ollama model writes an abstractive brief; fails closed to extractive. (brief_mode='abstractive(local-llm)') - Extractive (default). Rule-based real sentences from the source — free, offline,
deterministic, always available. (
brief_mode='extractive')
Development & tests
git clone https://github.com/AlexanderBoger/SaveContext && cd SaveContext
python -m pip install -e ".[tokenizer,dev]"
python -m pytest # 80 tests
Covers tokenizer / handles / chunking / extraction / BM25 retrieval units, an end-to-end
pass over all eleven tools (offset exactness, verbatim quotes, versioning, money-change
diff, audit preservation buckets, error handling), the batched-lookup retrieval +
confidence logic, and the CLI flow.
Roadmap
- ✅ BM25 + hybrid semantic ranking (concept groups, answer-type intents) — no model required
- ✅ Batched
lookupwith per-query confidence / abstention - ✅ Verbatim-integrity + extraction-recall reporting in
audit - ✅ PDF / DOCX / HTML loaders and recursive folder ingest
- ✅ HTTP / SSE transport in addition to stdio
- ⬜ Embedding backend benchmarked against the held-out suite (dense-prose retrieval)
- ⬜ Vector-store persistence so embeddings aren't recomputed per query
- ⬜ LLM-assisted atom extraction for fuzzy / multi-span facts
License
Apache License 2.0 — see also NOTICE. If you redistribute
SaveContext or build on it, retain the copyright and the NOTICE file, per the license
(that's how attribution is required).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file savecontext-0.1.1.tar.gz.
File metadata
- Download URL: savecontext-0.1.1.tar.gz
- Upload date:
- Size: 65.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
61d13dfb0116ec1e627c5beb4c78aa09949fa22ca83312652b1ec5585aaa3034
|
|
| MD5 |
d59662c1b4f8b8bc6ff51d67283019a8
|
|
| BLAKE2b-256 |
2321528f2652184d982effdfa1e2239d30ee7b0c3b3d749a3d681500773ac8b1
|
File details
Details for the file savecontext-0.1.1-py3-none-any.whl.
File metadata
- Download URL: savecontext-0.1.1-py3-none-any.whl
- Upload date:
- Size: 57.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1c39b8ecd0844aea669d7db2ed5b4df2c0a4ce523a8554f656b42b5a67be3ab0
|
|
| MD5 |
0d2540cb545347a421f209f2d46a6806
|
|
| BLAKE2b-256 |
f00e80b852478e7c8cb7ea935b5796ff9e27515bf3c50676dc5eb3395e0b1285
|