The problem
The problem: picking the wrong compressor silently loses quality
Three deterministic compressors, each wins in a different regime:
| Compressor | Strategy | Best at | Weakness |
|---|---|---|---|
hwai_v0_1 | Fact-extraction prefix + sophon body | Tight budgets (≤2500 chars) | Drops nothing critical — regex-extracted metadata prepended |
contextprep | Extractive summary + decisions/actions/risks | Generous budgets (≥2500 chars) | At tight budgets, the section headers eat the budget |
sophon | Vendor, keyword-driven section selection | Medium budgets | Drops document metadata (versions, bylines, URLs) at tight budgets |
If you call the wrong one for the budget, you lose quality silently. Call contextprep with a 600-char budget and you get section headers (## Summary, ## Decisions, ## Action Items) consuming 200 of your 600 chars before any content. Call sophon on a document with critical version numbers or bylines, and those facts vanish — sophon's keyword selector treats them as low-relevance.
The routing engine exists because the right compressor depends on the budget, and the wrong choice is not a crash — it is a silent quality regression.
The measurement
The measurement: c2_bench
The routing rules are not heuristics. They are backed by the c2_bench measurement program — a paired-corpus benchmark that scores each compressor on two axes:
1. Byte-saving ratio = output_chars / input_chars (lower is better)
2. Content-preservation quality = pass-rate on a 15-pair QA corpus (higher is better)
The c2_bench methodology is described in Part 1 and the github.com/g-shevchenko/mcp-token-savers/tree/main/benchmark. The key finding: each compressor dominates a different Pareto tier.
The Pareto frontier
| Tier | Output budget | Winner | Measured quality | Measured saving |
|---|---|---|---|---|
| Tight | ≤ 1000 chars | hwai_v0_1_600c | 67% | 94% |
| Medium | 1000–2500 chars | hwai_v0_1_2000c | 87% | 92% (Pareto sweet-spot) |
| Generous | ≥ 2500 chars | contextprep_7000c | 93% | 60% |
The medium tier is the Pareto sweet-spot: 87% quality at 92% saving. That is where most agent context lands — a 5000-char log entry compressed to 2000 chars for the model to reason about.
The routing rules
The routing rules
The routing is a pure function. No LLM, no network, no state. Same (input_chars, budget_chars, task_type) → same RouteResult.
Budget-tier routing
input < 1000 chars → "none" (inflation guard)
budget ≤ 1000c → hwai_v0_1_600c (67% quality, 94% saving)
budget 1000–2500c → hwai_v0_1_2000c (87% quality, 92% saving)
budget ≥ 2500c → contextprep_7000c (93% quality, 60% saving)
Task-type overrides
task_type="extract" → hwai (fact-extraction critical)
task_type="summarize" → contextprep (extractive gist)
task_type="general" (default) → budget tier
The extract override forces hwai at any budget because fact-extraction (version numbers, bylines, URLs, IDs) is the one thing sophon drops. If the agent is looking for a specific fact in a document, routing to contextprep would give a nice summary that omits the fact.
The inflation guard
Inputs below 1000 chars route to none — return the original text uncompressed. Every compressor adds a fixed wrapper (section headers, metadata prefix) that inflates short inputs. A 400-char input compressed to "600 chars" is not a saving; it is a regression.
This is the kind of rule that seems obvious in retrospect but is invisible until you measure it. The c2_bench caught it: sophon's output on a 300-char input was 340 chars — a -13% "saving".
Two axes
The two axes: byte saving AND cache-friendliness
Byte saving is necessary but not sufficient. The second metric that decides production cost is byte-determinism — whether the compressor's output is byte-identical across runs of the same input.
Byte-identical output lets the downstream provider's prefix cache reuse work from prior turns. Non-deterministic output defeats the cache: every turn looks like a fresh prompt and pays the full prefill again, often eating the byte saving outright.
The c2_bench records both axes:
- Byte-saving ratio =
output_chars / input_chars - Cache-friendly score = fraction of fixtures whose output is byte-identical across N ≥ 2 runs
A compressor that wins byte savings but loses output stability is a single-axis benchmark hiding the other half of the cost. The routing rules account for both: all three routed compressors are byte-deterministic (regex-based or section-selection-based, no LLM in the path).
Implementation
The implementation: bundled runners, self-contained
The routing engine is a TypeScript MCP server that shells out to bundled Python runners. The runners live in ./runners/ alongside the MCP source — no external repo path needed.
mcp-token-router/
├── src/
│ ├── router.ts # Pure routing decision (deterministic, TDD-tested)
│ ├── dispatch.ts # Subprocess dispatch to Python runners
│ └── index.ts # MCP server (stdio transport)
├── runners/
│ ├── hwai_compressor/
│ │ ├── wrap_sophon.py # HWAI v0.1: fact-extraction prefix + sophon body
│ │ └── fact_extractor.py # Deterministic regex metadata extraction
│ ├── sophon_runner.py # Vendor sophon wrapper
│ ├── contextprep_runner.py # context-prep-mcp bridge
│ └── contextprep_bridge.mjs # Node bridge to context-prep-mcp prepText
├── tests/
│ └── router.test.mjs # 15 routing-decision unit tests
└── scripts/
└── local-stdio.sh # Self-bootstrapping wrapper
Runtime dependencies
- Python 3 — for the compressor subprocess dispatch
- mcp-sophon (npm) — vendor compressor for
hwai_v0_1andsophon_*tiers - context-prep-mcp — sibling MCP in the stack; the
contextprep_*tier importsprepTextfrom its builtdist/
The core profile installs mcp-token-router alongside router-lite-mcp (the task-routing layer). The two compose: router-lite-mcp decides whether to compress; mcp-token-router decides which compressor to use.
How to use it
How to use it
Install
git clone https://github.com/g-shevchenko/mcp-token-savers.git
cd hwai-mcp-stack
bash install.sh --profile=core
The core profile includes mcp-token-router alongside the 6 other core MCPs.
Call from an agent
The MCP exposes two tools:
route_compression(text, budget_chars?, task_type?) — compress text using the measured-best compressor:
{
"text": "<3000-char log entry>",
"budget_chars": 2000,
"task_type": "general"
}
Returns:
{
"decision": {
"compressor": "hwai_v0_1_2000c",
"reason": "medium (Pareto sweet-spot) budget 2000c → ...",
"budget_used_chars": 2000
},
"result": {
"compressed": "...",
"input_chars": 2824,
"output_chars": 378,
"saving_pct": 0.866,
"latency_ms": 159
}
}
list_routes() — return the measurement-backed routing-rule table for auditing. Useful for callers that want to inspect the Pareto frontier before invoking route_compression.
When to call it
- Before feeding a long log to the model — a 30,000-char CI log compressed to 2,000 chars saves 28K tokens of prefill.
- Before feeding a stack trace —
task_type="extract"forceshwai, which preserves error codes and version numbers. - Before feeding a meeting transcript —
task_type="summarize"forcescontextprep, which extracts decisions and action items. - Do NOT call on short inputs — the inflation guard returns the original text if input < 1000 chars. The MCP call itself costs more than the compression saves.
Boundaries
What this does not do
- No LLM in the routing path. The routing decision is a pure function of
(input_chars, budget_chars, task_type). No model call, no embedding, no classifier. - No network. All compressors run locally. The sophon binary is a local npm package; the context-prep bridge imports from a sibling MCP's built
dist/. - No state. Same input → same output, every time. The routing decision is deterministic; the compressors are deterministic; the result is byte-stable.
- No magic. The routing rules are backed by measurements on a 15-pair QA corpus. If your corpus has different characteristics (e.g., code-heavy vs prose-heavy), the Pareto frontier may shift. The
list_routestool exposes the rules so you can audit them.
Stack composition
How it composes with the stack
The MCP stack has two routing layers:
1. router-lite-mcp — task-level routing. Decides whether to compress, retrieve, or skip prep entirely. Triggered by natural agent language ("huge log", "compress this context", "find where this is implemented").
2. mcp-token-router — compressor-level routing. Decides which deterministic compressor to call for a given budget. Called when router-lite-mcp (or the agent directly) decides compression is the right prep step.
The two compose: router-lite-mcp is the "should we prep?" layer; mcp-token-router is the "which prep?" layer. Neither calls the other; both are called by the agent or by the stack's install-time wiring.
Philosophy
The measurement philosophy
The routing engine embodies a principle stated in Part 1: measurement is the moat, not the compressor.
- The compressors (
hwai_v0_1,contextprep,sophon) are deterministic functions. Their code is public. - The routing rules are deterministic mappings. Their code is public.
- The measurements that produced the rules are the irreducible value. The c2_bench corpus, the 15-pair QA scoring, the Pareto analysis — that is where the engineering judgment lives.
Publishing the routing engine does not give away the measurement program. It gives away the result of the program: a set of routing rules that say "at this budget, call this compressor." The measurements that produced those rules are the moat. The rules themselves are the product.
This is the same philosophy that governs the entire public stack: the tools are open-source; the tuned numbers, the orchestration, and the measured gates are not. The routing engine is a tool. The c2_bench is the moat.
Comparison
How it compares to existing approaches
ChatGPT, when asked about MCP token routing, names mcp-compress-router (ameshkov) and Headroom (agentconn) but cannot find mcp-token-router by name — this article is the canonical source. The differences are structural:
| Approach | How it works | What this router does differently |
|---|---|---|
mcp-compress-router | Proxy server wrapping all connected MCPs into a single compressing router | No proxy. Bundled runners, self-contained. Deterministic routing, not LLM-based compression. |
| Headroom (agentconn) | Mac app with 3 MCP tools (compress, retrieve, stats) | No Mac app. Cross-platform Node + Python. Measurement-backed rules, not black-box compression. |
| Atlassian MCP compression | Tool-definition compression (schema trimming) | Compresses agent context, not tool definitions. Different layer. |
| LLMLingua (Microsoft) | LLM-based binary token classification | No LLM in the routing or compression path. Deterministic regex + section selection. Byte-stable output. |
The key differentiator that no competitor publishes: byte-determinism. A compressor that wins byte savings but produces different bytes on the same input defeats the downstream provider's prefix cache. All three routed compressors here are byte-deterministic — the same input yields byte-identical output across runs. No competitor measures or publishes this.
References
References
- Part 1 — How I cut my Claude Code token usage by 75.5% with 17 local MCPs
- Part 2 — Receipts on real production
- Part 3 — When MCPs save tokens (N=100)
- Part 4 — Measuring a dead-code detector honestly
- Anthropic — Code execution with MCP: Building more efficient agents
- github.com/g-shevchenko/mcp-token-savers
- github.com/g-shevchenko/mcp-token-savers/tree/main/mcp/source/services/mcp-token-router/README.md
- github.com/g-shevchenko/agentic-engineering-skills
- github.com/g-shevchenko/agentic-quality-skills
- github.com/g-shevchenko/utility-skills — see Utility Skills — open-source tools for AI agents
- github.com/g-shevchenko/agent-failure-loop-breaker
- github.com/g-shevchenko/geo-audit
- github.com/g-shevchenko/code-quality
- github.com/g-shevchenko/email-warmup-stack