Sarup
Thai-first context compression; shrinks long Thai and English text, originals retrievable.
ماذا يمكنك أن تفعل مع Sarup؟
- 🇹🇭 Real Thai compression — PyThaiNLP
newmmword segmentation, not whitespace. - ♻️ Lossless by guarantee — every compress caches the original;
verified: trueproves a byte-for-byte round-trip. - 🎚️ Five modes: from offline 1 ms TF-IDF to a 78%-savings cascade.
- 🧠 Optional local LLM — embeddings + rewrite via Ollama, with automatic offline fallback.
- 📏 Honest metrics: a real tokenizer (tiktoken) at runtime; Claude's own tokenizer and a
- 🕳️ Visible gaps:
[…]marks every place sentences were dropped, so Claude retrieves - 🔌 Content-aware — JSON compaction, log dedup, and verbatim code-fence preservation built in.
- 🛟 Can't break Claude — it's an MCP tool, not an API proxy; if the server is down the tools just go away and Claude keeps working.
التثبيت
أضف هذا إلى إعدادات MCP في عميلك (Claude Desktop: claude_desktop_config.json، وCursor: .cursor/mcp.json، ويستخدم VS Code وغيره الصيغة نفسها «mcpServers»).
sarupclaude mcp add sarup -- uvx sarup==0.2.0
{
"mcpServers": {
"sarup": {
"command": "uvx",
"args": [
"sarup==0.2.0"
]
}
}
}
التوثيق
مأخوذ من ملف README للمشروع على GitHub ويُحدَّث أسبوعيًا. تفتح الروابط على GitHub.
المحتويات
สรุป · Sarup
Thai-first context compression for Claude Code. An MCP server that actually shrinks Thai (50–78% fewer tokens) and caches every original so nothing is ever lost. Measured for what matters too: whether Claude still answers correctly.
สรุป means "to summarize." Headroom routes Thai through
noop(0% savings) because its whitespace tokenizer can't find Thai word boundaries. Sarup uses PyThaiNLP segmentation, so it compresses Thai as well as English — and caches every original so nothing is ever lost.
Contents
- Highlights
- Why it's safe — the two-tier guarantee
- How it works
- Tools
- Compression modes
- Measured results
- Example
- Install
- Register with Claude Code
- Auto-compression hook
- Privacy & data
- Configuration
- Project structure
- Tech stack & techniques
- Testing
- Roadmap
- License
Highlights
- 🇹🇭 Real Thai compression — PyThaiNLP
newmmword segmentation, not whitespace. - ♻️ Lossless by guarantee — every compress caches the original;
verified: trueproves a byte-for-byte round-trip. - 🎚️ Five modes: from offline 1 ms TF-IDF to a 78%-savings cascade.
- 🧠 Optional local LLM — embeddings + rewrite via Ollama, with automatic offline fallback.
- 📏 Honest metrics: a real tokenizer (tiktoken) at runtime; Claude's own tokenizer and a Thai QA eval for the published numbers (Does Claude still get it right?).
- 🕳️ Visible gaps:
[…]marks every place sentences were dropped, so Claude retrieves instead of mistaking a nearby fact for the answer. - 🔌 Content-aware — JSON compaction, log dedup, and verbatim code-fence preservation built in.
- 🛟 Can't break Claude — it's an MCP tool, not an API proxy; if the server is down the tools just go away and Claude keeps working.
Why it's safe — the two-tier guarantee
| Tier | What | Guarantee |
|---|---|---|
| Compressed view | the shrunk text the model works on | lossy · small · cheap |
| Retrieval store | the original, keyed by a stable hash | lossless · recoverable |
Aggressive lossy compression is safe because the original is always one sarup_retrieve(hash)
away. This is how "maximum savings" and "100% accuracy" coexist — they live in different tiers.
How it works
Two entry points feed one engine: a cheap compressed view the model reads, and a lossless retrieval store that can restore the original byte-for-byte.
flowchart TD
M["🧑 Manual<br/>sarup_compress()"]:::entry --> R
A["⚙️ Automatic<br/>PostToolUse hook<br/>(Read · Bash · Grep)"]:::entry --> R
R{"Sarup compress<br/>extractive · semantic · abstractive · pipeline"}:::engine
R -- "compressed view<br/>50–78% fewer tokens" --> V["📄 Model context"]:::lossy
R -. "cache original" .-> S[("🗄️ Retrieval store<br/>hash → original")]:::lossless
V -. "need full detail?" .-> RET["🔑 sarup_retrieve(hash)"]:::lossless
RET --> S
S == "byte-for-byte ✓" ==> V
classDef entry fill:#e0e7ff,stroke:#6366f1,color:#111
classDef engine fill:#fde68a,stroke:#d97706,color:#111
classDef lossy fill:#fef3c7,stroke:#f59e0b,color:#111
classDef lossless fill:#bbf7d0,stroke:#16a34a,color:#111
- Manual — the model calls
sarup_compress/sarup_retrieveitself. - Automatic — the hook intercepts large tool outputs, caches the original to
SARUP_DB_PATH, and substitutes the compressed view + a retrieval hash. Source code is skipped; small outputs pass through untouched.
Tools
| Tool | Purpose |
|---|---|
sarup_compress(content, target_ratio?, lossless?, query?, mode?) | Compress; returns compressed text, hash, token metrics,verified, token_method. |
sarup_retrieve(hash) | Recover the original content byte-for-byte. |
sarup_stats() | Cumulative session savings. |
sarup_compress arguments
| Arg | Type | Default | Meaning |
|---|---|---|---|
content | string | — | Text to compress (required). |
target_ratio | number | 0.5 | Fraction of prose to keep (0.1–0.9). |
lossless | boolean | false | Only apply lossless transforms (whitespace / JSON compact). |
query | string | "" | What you need from the text; sentences matching it are kept. Pass it whenever you know: it took the QA eval from 69% to 100% at the same size. |
mode | string | extractive | See modes below. |
Compression modes
| Mode | How | Needs Ollama | Savings¹ | Speed¹ | Output |
|---|---|---|---|---|---|
extractive (default) | TF-IDF scoring + fact boost + n-gram dedup | no | 52.7% | ~1 ms | verbatim subset |
semantic | Embedding centrality + cosine dedup | yes | 63.8% | ~2 s | verbatim subset |
abstractive | Local-LLM rewrite | yes | 44.4% | ~8 s | paraphrased |
pipeline | Cascade: semantic → abstractive | yes | 78.2% | ~6 s | paraphrased |
auto | semantic if Ollama is up, else extractive | optional | 63.8% | ~2 s | subset |
¹ Measured on a 10-sentence Thai paragraph (522 tokens), rewrite model gemma4:12b. Every mode
stays 100% recoverable via the store; Ollama modes degrade gracefully to extractive when the
backend is down. (gemma4:e4b: pipeline 82.2%; June 2026, gemma3:12b: 88.1%.)
Extractive and semantic output is a verbatim subset of the input sentences, with […] on its
own line wherever sentences were dropped. Sentences carrying specifics (numbers, Thai digits,
identifiers like X-Api-Key) score higher than general statements, since those are what
questions ask about.
Measured results
$ .\.venv\Scripts\python.exe bench\benchmark.py
sample before after savings verify
Thai prose 522 247 52.7% OK
Thai prose (aggressive) 522 189 63.8% OK
English prose 105 60 42.9% OK
JSON (lossless) 67 44 34.3% OK
Logs 563 300 46.7% OK
TOTAL 1779 840 52.8% ALL OK → 100% recoverable
Mode comparison (Thai prose, 522 tok):
extractive 52.7% (1ms) · semantic 63.8% (2.1s) · auto 63.8% (2.1s)
abstractive 44.4% (8.3s) · pipeline 78.2% (5.8s) ← gemma4:12b
Token counts via tiktoken cl100k_base. That is OpenAI's tokenizer and undercounts Claude,
most of all on Thai; bench\benchmark.py --claude adds a column from Claude's own tokenizer
(Anthropic count_tokens, needs an API key).
Does Claude still get it right?
Fewer tokens only help if the facts survive. bench\eval_qa.py compresses 8 Thai documents
(incident report, meeting notes, API docs, HR policy, product spec, support chat, README, sales
report; half line-per-sentence, half dense paragraphs) and asks Claude Opus 5.5 32 questions
from the compressed view alone, graded deterministically:
$ .\.venv\Scripts\python.exe bench\eval_qa.py # uses your local `claude` login
mode claude tok saved correct abstain wrong
──────────────────────────────────────────────────────────────
original 4601 0.0% 100% 0% 0
extractive:0.5 3204 30.4% 69% 31% 0
extractive:0.3 1969 57.2% 44% 56% 0
semantic:0.5 3214 30.1% 62% 38% 0
extractive:0.5+q 3207 30.3% 100% 0% 0
abstractive:0.5 2697 41.6% 91% 9% 0 ← gemma4:12b
pipeline:0.5 1854 59.8% 59% 41% 0 ← gemma4:12b
(gemma4:e4b scored the same: abstractive 91%, pipeline 59%, wrong 0.)
- wrong = 0 is the safety property: when a fact was dropped, Claude said so (abstain) and
in real use would call
sarup_retrieve. It did not guess from what was left. - abstain is the real cost of blind compression: about a third of specific questions need a
retrieval round-trip at
0.5. Know what you're looking for? Pass it asquery(+qrow). - abstractive keeps the most facts per token (91% at 42% saved) because a rewrite can keep
every number while dropping words; extraction can only drop whole sentences. It costs seconds
per document, so use it through
sarup_compress, not the hook. - In both evals no paraphrase changed a fact into a wrong one, but 8 documents can't prove a
rewrite never will, which is why the original stays one
sarup_retrieveaway. - Token counts here are Claude's own (from the answer calls' usage) and include the ~40-token
[sarup: …]footer, which weighs heavily on these short (~570-token) documents and fades on the large outputs the hook actually compresses. - 8 documents is a small sample; differences of a couple of questions between modes are noise.
Before the fixes this eval drove (Oct 2026): dense Thai paragraphs were not compressed at all
(the sentence splitter's model wasn't installed), extractive:0.5 answered 59% with one
confident wrong answer, and semantic:0.5 47%.
Example
A real sarup_compress call on a Thai paragraph (mode="auto", Ollama up → semantic):
// → sarup_compress(content="…518-token Thai paragraph…", mode="auto")
{
"compressed": "จุดเด่นที่สำคัญที่สุดคือมันไม่มีทางทำให้ Claude พัง…",
"hash": "caa568140bec0ff734937cf5",
"original_tokens": 518,
"compressed_tokens": 154,
"tokens_saved": 364,
"savings_percent": 70.3,
"transforms": ["semantic_extractive", "embeddings", "thai"],
"lossy": true,
"verified": true, // round-trip proven byte-for-byte
"token_method": "tiktoken:cl100k_base"
}
The model keeps working on the 154-token view; the full 518-token original is one call away:
// → sarup_retrieve(hash="caa568140bec0ff734937cf5")
{ "content": "…the exact original text, restored byte-for-byte…" }
Install
As a Claude Code plugin (easiest)
Needs uv: the plugin runs Sarup through uvx, which fetches
Python and the dependencies on first use. Nothing to clone. In Claude Code:
/plugin marketplace add PHUICMT/sarup
/plugin install sarup@sarup
That installs the MCP server, the auto-compression hook and the /sarup:sarup-setup skill.
The cache lives in the plugin's data directory, so it survives plugin updates. The first start
takes a few seconds while uvx builds the environment. From a shell instead:
claude plugin marketplace add PHUICMT/sarup, then claude plugin install sarup@sarup.
Ollama is optional. Without it everything works offline: the hook and the default
extractive mode never touch it, and asking for semantic, abstractive or pipeline
falls back to extractive with a setup field that names what to install. sarup_stats
shows what is available, and /sarup:sarup-setup walks through adding
Ollama and the models below.
From PyPI (any MCP client)
The package is sarup on PyPI. With uv, nothing to install
by hand; point any MCP client at uvx sarup:
{
"mcpServers": {
"sarup": {
"command": "uvx",
"args": ["sarup"],
"env": { "SARUP_DB_PATH": "/home/me/.sarup-cache.db" }
}
}
}
In Claude Code without the plugin: claude mcp add sarup -s user -- uvx sarup. The hook is
uvx --from sarup sarup-hook --db <same file as SARUP_DB_PATH>. pip install sarup works
too and puts sarup and sarup-hook on PATH.
From a clone
One command (creates the venv, installs everything, registers the MCP server for all projects, idempotent):
.\scripts\setup.ps1 -All # Windows (-All also adds the hook, the /sarup-setup skill, pulls Ollama models)
./scripts/setup.sh --all # Linux / WSL / macOS
Tip:
-All/--allinstalls a global/sarup-setupskill, so on any other machine you can just type/sarup-setupin Claude Code and it walks through the install. (Or runscripts/install-skill.ps1/install-skill.shon its own.)
Uninstall just as cleanly (only removes what Sarup added; -Purge/--purge also
deletes the venv + cache):
.\scripts\uninstall.ps1 # Windows
./scripts/uninstall.sh # Linux / WSL / macOS
Manual install
py -3.11 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
Optional local-LLM modes (semantic / abstractive / pipeline) need Ollama:
ollama pull nomic-embed-text # embeddings → semantic mode
ollama pull gemma4:12b # rewrite → abstractive / pipeline, 7.6 GB (or gemma4:e4b / gemma3:12b)
Register with Claude Code
One-command setup (recommended). Detects this machine's paths, probes Ollama
(picks the best mode + models), and merges into .mcp.json / .claude/settings.json
without clobbering anything already there (a .bak is written first):
.\.venv\Scripts\python.exe scripts\install.py --with-hook --pull
- No Ollama? It configures offline
extractivemode — still fully works. - Ollama up? It auto-selects
nomic-embed-text(semantic) + the best installed rewrite model (gemma4:12b→gemma4:e4b→gemma3:12b→qwen3) forsarup_compress. The hook staysextractiveeither way (it runs on every tool call).--pullfetches any missing models. - Idempotent — safe to re-run;
--globalwrites to~/.claudeinstead.
Manual — or add it yourself to your MCP config (e.g. .mcp.json or ~/.claude.json).
Replace <SARUP_DIR> with the absolute path where you cloned this repo (the installer
above fills these in for you):
{
"mcpServers": {
"sarup": {
"command": "<SARUP_DIR>/.venv/Scripts/python.exe",
"args": ["-m", "sarup.server"],
"env": { "SARUP_DB_PATH": "<SARUP_DIR>/.sarup-cache.db" }
}
}
}
On Linux/macOS the interpreter is
<SARUP_DIR>/.venv/bin/python.
Or run it directly over stdio:
.\.venv\Scripts\python.exe -m sarup.server
Auto-compression hook
Skip manual tool calls entirely: install the PostToolUse hook and large Read/Bash/Grep
outputs are compressed before they enter context, with the original cached for retrieval.
Source-code reads are skipped for safety. Full setup in hooks/README.md.
Fixed in October 2026. Earlier versions returned the compressed text as a plain string. Claude Code only accepts
updatedToolOutputin the tool's own shape ({stdout, stderr, …}for Bash,{type, file: {content, …}}for Read) and silently dropped the string, so the hook never applied on any surface. It now keeps each tool's shape; verified on the CLI (2.1.289): the model receives the compressed view and the retrieval hash. To see what the hook does on your build, setSARUP_HOOK_LOGto a file path (one JSON line per call).
The plugin wires this up for you. From a clone, replace <SARUP_DIR> with your clone path, or
run install.py --with-hook:
{
"hooks": {
"PostToolUse": [
{ "matcher": "Read|Bash|Grep",
"hooks": [{ "type": "command",
"command": "<SARUP_DIR>/.venv/Scripts/sarup-hook --db <SARUP_DIR>/.sarup-cache.db" }] }
]
}
}
Privacy & data
To guarantee recovery, Sarup caches the original content in the store. Two things to know:
- With
SARUP_DB_PATHset, originals are written to that SQLite file in plaintext (no encryption). Treat it like a cache of whatever you compressed. - If you compress tool outputs that contain secrets (e.g. a
.envdump or credentials in a log), those land in the cache too. The auto-hook skips source-code/config file reads, butBashoutput is fair game — review what you point it at.
*.db is git-ignored, so the cache never gets committed. For zero on-disk
footprint, leave SARUP_DB_PATH unset (memory-only; the MCP server then loses
the cache on restart, and the hook will not substitute — see the hook docs).
Configuration
| Var | Default | Meaning |
|---|---|---|
SARUP_DB_PATH | (in-memory) | SQLite path for a persistent, cross-process store.Required for hook retrieval. |
OLLAMA_HOST | http://localhost:11434 | Ollama endpoint. |
SARUP_ABSTRACTIVE_MODEL | best installed | Model for abstractive / pipeline rewrite. Unset: first installed of gemma4:12b, gemma4:e4b, gemma3:12b, qwen3. |
SARUP_EMBED_MODEL | nomic-embed-text | Model for semantic embeddings. |
SARUP_HOOK_MODE | extractive | Hook compression mode. auto/semantic need Ollama and cost ~2 s per tool call. |
SARUP_HOOK_MIN_TOKENS | 400 | Hook only compresses outputs with at least this many tokens (token-based, fair across languages). |
SARUP_HOOK_LOG | (off) | File to append one JSON line per hook call (event, tool, size, substituted). |
Project structure
sarup/
├── src/sarup/
│ ├── server.py # MCP stdio server — 3 tools
│ ├── compressor.py # router + modes (extractive/semantic/abstractive/pipeline/auto)
│ ├── thai.py # PyThaiNLP tokenization, sentence split, TF-IDF
│ ├── semantic.py # embedding centrality + cosine dedup
│ ├── llm.py # optional Ollama backend (generate + embed)
│ ├── tokens.py # real token counting (tiktoken)
│ └── store.py # CCR store: hash → original (memory + SQLite)
├── hooks/
│ ├── sarup_hook.py # PostToolUse auto-compression hook
│ └── README.md # hook install guide
├── bench/benchmark.py # before/after measurement
├── tests/ # test_thai, test_mcp, test_hook, ...
├── README.md
└── STACK.md # full stack + techniques
Tech stack & techniques
Python 3.11 · MCP · PyThaiNLP newmm · tiktoken · Ollama (optional) · SQLite · hatchling · pytest.
The technique behind each mode — TF-IDF scoring, embedding centrality, cascade pipeline, content routing, and graceful degradation — is documented in STACK.md.
Testing
.\.venv\Scripts\python.exe -m pytest tests/ -q
The suite covers Thai NLP, the MCP tool contracts, every mode (including Ollama-fallback paths), the roundtrip-verify guarantee, and the auto-compression hook (incl. cross-process retrieval).
Quality is measured separately, since it needs a model: bench\eval_qa.py (see
Does Claude still get it right?). It runs on the local
claude CLI by default, or --backend api with an Anthropic API key; --dry-run and
--limit keep a check cheap; a full run is ~170 Claude calls.
Releasing
- Bump
versioninpyproject.tomlandserver.json(both places in it). - Push a matching tag:
git tag v0.2.0 && git push origin v0.2.0. Thepublishworkflow tests, builds and uploads to PyPI through trusted publishing (no token). - List the release in the MCP registry:
mcp-publisher login github, thenmcp-publisher publish(readsserver.json; themcp-namecomment at the top of this README proves the PyPI package belongs to it).
The plugin needs none of this: it installs from the repo and updates with each commit.
Roadmap
- [ ] Grow the QA eval past 8 documents (long tool outputs, logs, mixed code) before
changing defaults such as
auto→ semantic on the strength of it. - [ ] Move to
mcp2.x (pinned<2for now: 2.x droppedServer.list_tools). - [ ] Optional Typhoon 2.1 abstractive (blocked on an Ollama template fix).
- [ ] Per-content adaptive
target_ratio. - [x] Published PyPI package (
sarup) and MCP registry listing (io.github.PHUICMT/sarup). - [x] Installable as a Claude Code plugin.
License
MIT
الأسئلة الشائعة
ما هو Sarup؟
Thai-first context compression; shrinks long Thai and English text, originals retrievable.
كيف أثبّت Sarup؟
يعمل على جهازك. في Claude Code: claude mcp add sarup -- uvx sarup==0.2.0. وفي العملاء الآخرين، الصق إعداد JSON من هذه الصفحة في إعدادات MCP الخاصة بهم.
هل يحتاج Sarup إلى مفتاح API؟
لا يذكر إدراجه في السجل أي مفتاح أو سر إلزامي. قد تتطلب بعض الميزات حسابًا لدى الخدمة التي يتصل بها.
هل Sarup مفتوح المصدر؟
نعم. شيفرته متاحة على GitHub بترخيص MIT.
من يصون Sarup؟
ينشره PHUICMT. يدرجه AI.tn لكنه لا يصونه ولا يوصي به.
من سجل MCP الرسمي. الإدراج ليس توصية: راجع شيفرة الخادم وصلاحياته قبل منحه الوصول إلى بياناتك.