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 — so it reads a cheap summary to find what it needs, then pulls back the exact original words to quote, using a fraction of the tokens.
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
LLM workflows waste tokens re-reading the same long documents. SaveContext loads the source once, returns a tiny handle plus a compact brief, and lets the model pull only the spans it needs — while guaranteeing the meaning-critical facts (dollar amounts, dates, obligations, identifiers) come back verbatim, with exact source offsets. It's not gzip-style byte compression; it's token reduction.
Quick start — one command, zero install
With uv installed, add SaveContext to Claude Code in a
single line. No clone, no pip, no virtualenv — uvx fetches and runs it in an
ephemeral, auto-updating environment:
# straight from GitHub (no PyPI needed):
claude mcp add savecontext -- uvx --from git+https://github.com/AlexanderBoger/SaveContext savecontext
# once published to PyPI:
claude mcp add savecontext -- uvx savecontext
Open Claude Code, run /mcp, and you'll see the 11 savecontext tools. Point the
model at a big document and just ask — it ingests, looks up, and quotes on its own.
To pin the database location, add -e SAVECONTEXT_DB=/abs/path.db before the --.
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. A summary is for finding; the
quoteis for trusting. Every span reconstructs byte-for-byte. - 🙅 Says "not found" instead of inventing. When the answer isn't in the document, retrieval is flagged weak and the agent abstains. In testing, agents returned not found for a missing database engine and on-call name rather than hallucinating plausible ones.
- 📉 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 onebrief/expand/quotecycle 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 on a contract set plus held-out documents (an RFC and a synthetic incident log) the retriever was never tuned on — real agents answering fact questions with distractor values planted next to the true ones, graded against ground truth. Crossover is ≈ 5k tokens: below that, just paste; above it, SaveContext wins and the gap widens with size.
Reproduce: examples/benchmark_suite/ —
eval_paraphrase.py (phrasing robustness) and eval_holdout.py (RFC + logs,
abstention grading).
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. Retrieval on dense technical prose is the current frontier (see the roadmap's embedding backend).
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 calling 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 (brief_mode='agent'). 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, protected-atom counts, per-category preservation estimate, safe_for / unsafe_for, warnings. |
Fidelity levels (expand): summary (extractive lead sentences, cheapest) ·
facts (summary plus exact atoms, [money] $500,000) · quotes (verbatim spans
you can cite) · full (raw text of the matched block — the only path that returns
source prose).
How a turn flows
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).
Configuration
Set via environment (or the MCP env block):
| Variable | Default | Purpose |
|---|---|---|
SAVECONTEXT_DB |
~/.savecontext/savecontext.db |
Store location. :memory: for ephemeral. Shared by the CLI and the MCP server, so anything ingested on one is queryable from the other. |
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_* variables take precedence; the
pre-rename CONTEXTSAVER_* / CONTEXTVAULT_* names are still honored as fallbacks.
CLI
The package ships a CLI (savecontext, short alias savectx) that shares the same
store as the MCP server:
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.
How it works
| 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 can write 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
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 (this is 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.0.tar.gz.
File metadata
- Download URL: savecontext-0.1.0.tar.gz
- Upload date:
- Size: 65.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
937dee0e296ddd0d01940d27115dae7a1596873d5b7da71cee5b6a56f39745c5
|
|
| MD5 |
081326127e88c14568ee666071b885b4
|
|
| BLAKE2b-256 |
cac0b837cbb592d7a55745dab027fd2a2a0a91f1cbdfafed8ff3c8c318cc83b8
|
File details
Details for the file savecontext-0.1.0-py3-none-any.whl.
File metadata
- Download URL: savecontext-0.1.0-py3-none-any.whl
- Upload date:
- Size: 57.5 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 |
86543a38cdd67e5e4eeb79eb67cf4f6a72b7c024bac256214489f5d6dc485d7f
|
|
| MD5 |
d55f4eba3e475e37939bcd737b5c873c
|
|
| BLAKE2b-256 |
1f4c2c42dbf01a38f8f5d2e0e1f05d62102804d789476c9afea50ff6ad37c9fb
|