Content
# savage-mcp
<p align="center">
<img src="docs/assets/savage-cave-painting.png" width="520" alt="Savage cave painting mark">
</p>
<p align="center">
<strong>KISS wrapper for MCP: fewer words, smaller image payloads, graph-shaped context.</strong>
</p>
<p align="center">
<a href="#before--after">Before/After</a> •
<a href="#install">Install</a> •
<a href="#what-you-get">What You Get</a> •
<a href="#mcp-tools">MCP Tools</a> •
<a href="#benchmarks">Benchmarks</a> •
<a href="#links">Links</a>
</p>
---
A Codex-first MCP proxy and skill/plugin that compresses what agents read. Text becomes compact machine-ish prose. Images become either primitive cave-painting graphs or block-diagram scene graphs. The goal is simple: keep meaning, burn fewer tokens.
## Before / After
| Normal answer | Savage answer |
| --- | --- |
| **React render loop**<br><br>"The reason your React component is re-rendering is likely because you create a new object reference on each render. When that inline object is passed as a prop, React shallow comparison sees a different object every time, so the child renders again. Use useMemo to keep the reference stable."<br><br>**73 est. tokens** | `tg[React-cmp rerender<-obj-ref/render;inline-obj-prop->diff-ref->child-rerender;fix:useMemo stable ref]`<br><br>**26 est. tokens, 64% saved** |
| **Auth expiry bug**<br><br>"Sure, the issue is most likely caused by authentication middleware not validating token expiry consistently. The comparison should reject tokens when exp is less than or equal to the current timestamp, and the error path should return 401 instead of falling through."<br><br>**67 est. tokens** | `tg[bug:auth-mw tok-expiry invalid;reject exp<=now err-path->401/no-fallthrough]`<br><br>**20 est. tokens, 70% saved** |
Same fix. Less prose. Code, URLs, paths, identifiers, API names, and exact errors stay intact.
| Signal | Meaning |
| --- | --- |
| Text graph saved: 62% avg | Average reduction from the local text-to-graph compressor on ten short prose samples. This is the meaningful text mode for MCP context. |
| Diagram image saved: 85% sample | Base64 image replaced by a small scene diagram graph. This is where MCP context savings get large. |
| Technical tokens | Preserved exactly when they look like code, paths, URLs, API names, or errors. |
| Shape | Graph nodes/edges, not parent/children trees. |
> [!IMPORTANT]
> Text numbers are local estimates from `examples/run_examples.js` using `format: "graph"`, not API billing records. Image numbers compare base64 chars with the generated graph payload.
## Install
```bash
npm install -g savage-mcp
```
Wrap any stdio MCP server:
```json
{
"mcpServers": {
"fs-savage": {
"command": "npx",
"args": [
"savage-mcp",
"--mode",
"auto",
"--image-mode",
"diagram",
"--",
"npx",
"@modelcontextprotocol/server-filesystem",
"/tmp"
]
}
}
}
```
CLI:
```bash
savage-mcp [--mode auto|text|image|off] \
[--text-level lite|balanced|ultra] \
[--text-format prose|graph] \
[--image-mode graph|svg|diagram|pass] \
[--max-primitives 64] \
[--min-image-bytes 4096] \
-- <upstream> [...args]
```
## What You Get
| Feature | What |
| --- | --- |
| Text graph compression | KISS frames like `tg[subject->action;fix:step]` for safe prose fields and text content. |
| Image primitives | `image/png`, `jpeg`, `webp`, etc. -> `savage_graph_v1` nodes/edges/colors. |
| Scene diagrams | Entity boxes + verb arrows -> `savage_diagram_v1`. |
| Codex skill | `plugins/savage` and `skills/savage` for compact Codex replies. |
| MCP utility tools | Local tools injected into upstream `tools/list`. |
| Stats | Process-level byte counters for text/image savings. |
## MCP Tools
`savage-mcp` adds these local tools:
| Tool | What |
| --- | --- |
| `savage.text` | Compress prose into compact KISS prose or text graph frames. Preserves code, URLs, paths, identifiers, API names, and exact errors. |
| `savage.image` | Convert base64 image data into compact visual primitives, SVG, or diagram blocks. |
| `savage.diagram` | Build a compact scene/block graph from labeled entities and verb relations. |
| `savage.payload` | Compress safe text fields and image blocks inside a JSON payload. |
| `savage.stats` | Return bytes saved by this proxy process. |
Tool-call variants:
```json
{ "name": "savage.text", "arguments": { "text": "The database configuration response is very important.", "level": "ultra", "format": "graph" } }
```
```json
{ "name": "savage.image", "arguments": { "data": "<base64>", "mimeType": "image/png", "format": "graph", "maxPrimitives": 24 } }
```
```json
{
"name": "savage.diagram",
"arguments": {
"items": [
{ "id": "person", "label": "person" },
{ "id": "mountain", "label": "mountain" }
],
"relations": [
{ "from": "person", "to": "mountain", "verb": "climbs" }
]
}
}
```
## Image Modes
| Mode | Output | Best For |
| --- | --- | --- |
| `graph` | `savage_graph_v1` rectangles, colors, adjacency edges | Deterministic visual simplification; cave-painting primitive feel. |
| `svg` | Compact SVG made from primitive cells | Visual preview when the client can render SVG. |
| `diagram` | `savage_diagram_v1` labeled boxes + verb arrows | Semantic block scheme: `person --climbs--> mountain`. |
| `pass` | Original image block | When binary fidelity matters more than context size. |
Example image receipt from the generated sample scene:
<p align="center">
<img src="examples/generated/sample-scene.png" width="220" alt="Generated sample scene before savage image compression">
<img src="examples/generated/diagram-preview.png" width="300" alt="Diagram preview with person climbs mountain">
</p>
| Payload | Chars | Mode | Saved |
| --- | ---: | --- | ---: |
| Base64 PNG | 3752 | original | - |
| Primitive graph | 3347 | `savage_graph_v1` | 11% |
| Scene diagram | 562 | `savage_diagram_v1` | 85% |
Scene diagram output:
```text
person --climbs--> mountain
sun --lights--> mountain
```
> [!NOTE]
> `diagram` mode is deterministic. If you want labels like `person`, `mountain`, and `climbs`, pass scene labels/relations in the tool call. Without labels, the wrapper can only emit visual regions, not claim object recognition.
## Benchmarks
Measured by the local example runner with the current compressor and a simple UTF-8 byte/4 token estimate.
| Task | Normal est. toks | Savage graph est. toks | Saved |
| --- | ---: | ---: | ---: |
| Explain React re-render bug | 73 | 26 | 64% |
| Fix token expiry middleware | 67 | 20 | 70% |
| Set up PostgreSQL pool | 62 | 13 | 79% |
| Explain MCP stdio wrapper | 54 | 18 | 67% |
| Refactor callback API | 49 | 23 | 53% |
| Compare monolith and services | 52 | 22 | 58% |
| Review PR security risk | 51 | 27 | 47% |
| Docker build optimization | 52 | 17 | 67% |
| Debug race condition | 51 | 28 | 45% |
| Implement error boundary | 49 | 15 | 69% |
| **Average** | **56** | **21** | **62%** |
### Savage Receipts
Memory-style files, same runner:
| File | Original est. toks | Savage graph est. toks | Saved |
| --- | ---: | ---: | ---: |
| `agent-instructions.md` | 50 | 17 | 66% |
| `mcp-server-notes.md` | 46 | 21 | 54% |
| `image-policy.md` | 46 | 9 | 80% |
| `release-checklist.md` | 51 | 11 | 78% |
| `codex-style.md` | 43 | 18 | 58% |
| **Average** | **47** | **15** | **67%** |
> [!TIP]
> Graph text mode is intentionally more aggressive than prose mode. Use `--text-format prose` when readability matters more than savings; use `graph` for MCP context size.
## Graph Shapes
Text graph frame:
```text
tg[React-cmp rerender<-obj-ref/render;inline-obj-prop->diff-ref->child-rerender;fix:useMemo stable ref]
```
Primitive image graph:
```json
{
"v": 1,
"kind": "savage_graph_v1",
"nodes": [{ "id": "n0", "t": "rect", "bbox": [0, 0, 0.1, 0.1], "color": "#8ecae6", "score": 0.9 }],
"edges": [{ "a": "n0", "b": "n1", "r": "adj", "w": 0.42 }],
"meta": { "mimeType": "image/png", "width": 192, "height": 128 }
}
```
Scene diagram graph:
```json
{
"v": 1,
"kind": "savage_diagram_v1",
"nodes": [{ "id": "person", "t": "box", "label": "person" }],
"edges": [{ "a": "person", "b": "mountain", "r": "verb", "label": "climbs" }]
}
```
## Links
- [README benchmark source](examples/run_examples.js)
- [Generated example results](examples/generated/results.json)
- [Codex plugin manifest](plugins/savage/.codex-plugin/plugin.json)
- [Savage skill](skills/savage/SKILL.md)
- [MCP proxy source](src/savage-mcp/index.js)
- [GitHub repo](https://github.com/denzasikora-lab/savage-mcp)
## Limits
- No hidden vision model. Semantic diagrams need caller-supplied labels.
- Unsupported image MIME blocks pass through unchanged.
- Compression avoids rewriting code-like tokens; exact technical strings win over extra savings.