Hatter is pre-alpha. Expect breaking changes, rough edges, missing docs, and the occasional dropped teacup. Not production-ready. Pin to a specific image digest if you depend on it.
Hatter is an MCP proxy. It cuts LLM context cost from hundreds of tokens per tool to roughly five, and it rotates the tokens every turn so a prompt-injected tool name can't survive into the next one.
LLM Client ──MCP──▶ Hatter ──MCP──▶ Upstream MCP Servers
│
└─ admin dashboard at :8080/
The token cost of MCP tools scales linearly with the number you connect. Fifty tools is tens of thousands of input tokens burned before the model has done a single thing. Hatter turns that into a two-step dance: discovery, then act. The LLM only loads the schemas it actually needs.
Bonus: per-turn zero-trust. Short ids are regenerated on every tools/list. An attacker who smuggles a tool name into a tool response can't replay it the next turn — the id is already gone.
services:
hatter:
image: kinark/hatter:latest
ports: ["8080:8080"]
volumes: ["./data:/data"]
environment:
LOG_LEVEL: infoThen:
docker compose up -dImage lives on Docker Hub: kinark/hatter.
git clone https://github.com/YOUR-FORK/hatter
cd hatter
docker compose up --buildOn first boot Hatter prints a generated admin token to stdout. Save it.
Then:
- Open
http://localhost:8080/, paste the admin token to log in. - MCP Servers → Add server. For an example, use
npx -y @modelcontextprotocol/server-filesystem /tmp(transport: stdio, command:npx, args:-y @modelcontextprotocol/server-filesystem /tmp). - Tools should populate within a few seconds.
- Groups → New group. Slug:
filesystem, description:Read/write files under /tmp. Two-pane picker on the right — add the filesystem tools. - Point your LLM client at
http://localhost:8080/mcp(setMCP_CLIENT_TOKENif you want it auth-gated).
The LLM will see a group_filesystem meta-tool and a 2-char stub per filesystem tool, plus stubs for any other groups you've defined.
Here is the thing: the LLM at your tea party has a terrible memory for names. So Hatter hands it a seating card — a 2-character stub like q7 — instead of the full curriculum vitae of every tool you've ever connected. The guest (the LLM) says "what does q7 actually do?" and Hatter replies with the full schema, right then, just-in-time.
What the LLM sees:
Every tools/list returns two kinds of entries. First, one stub per upstream tool — no description, permissive empty-object schema, ~5 tokens each. Second, one group meta-tool per group you've defined — its description is the only real text the LLM sees (e.g. "Filesystem read/write operations"). The LLM calls the group meta-tool to get the schema map for everything inside it, then calls the stub by its short id to actually run a tool.
What Hatter does in the middle:
Each tools/list starts a fresh turn. Hatter mints new short ids for every upstream tool, stores the mapping in memory, and hands the stubs downstream. When the LLM calls group_filesystem, Hatter returns the current ids alongside the full schemas — the id it tells you in that response is always the valid one for this turn. When you call a stub, Hatter looks up the stable internal id, finds the upstream server, and forwards the call verbatim.
What the upstream servers see:
Nothing unusual. A normal MCP client that holds a persistent connection and calls tools by their original names.
tools/list (LLM)
│
▼
Hatter mints turn: q7 → fs.read_file
b4 → fs.write_file
x1 → search.web_search
group_filesystem → "Filesystem read/write operations"
│
├─ LLM calls group_filesystem
│ └─ Hatter returns schema map: {q7: read_file schema, b4: write_file schema}
│
└─ LLM calls q7 with {path: "/tmp/hello"}
└─ Hatter forwards → upstream filesystem server → read_file("/tmp/hello")
Short ids are regenerated on every tools/list. Calling an id from a previous turn returns: "Tool id 'q7' not in current turn. Call a group_* meta tool to discover current ids." The mapping from the previous turn is already gone.
When an upstream server's tool list changes, Hatter refreshes its registry and pushes notifications/tools/list_changed to every connected downstream client — they pick up the new tool list and fresh short ids on their next tools/list without waiting on a poll cadence.
hatter/
├── server/ Python 3.12, FastAPI + mcp SDK ≥1.12 + aiosqlite
│ ├── hatter/
│ │ ├── app.py FastAPI + MCP ASGI mount + static dashboard
│ │ ├── mcp_proxy.py Low-level MCP server (list_tools / call_tool)
│ │ ├── registry.py TurnMap: per-session short-id mapping
│ │ ├── upstream.py Persistent ClientSession to each upstream server
│ │ ├── admin_api.py /api/v1/* REST API
│ │ ├── db.py SQLite schema + CRUD
│ │ └── ...
│ └── tests/
└── web/ React 18 + TypeScript + Mantine v7
└── src/
├── pages/ Status / Servers / Tools / Groups
├── shell/ AppShell, header, nav
└── api/ Typed client for /api/v1
One container, port 8080. Multi-stage Dockerfile: stage 1 builds the React dashboard, stage 2 runs Python and serves it statically.
| Env var | Default | Purpose |
|---|---|---|
ADMIN_TOKEN |
auto-generated, written to $DATA_DIR/admin_token |
Protects /api/v1/* |
MCP_CLIENT_TOKEN |
unset (/mcp is open with a WARN log) |
Protects /mcp |
DATA_DIR |
/data |
SQLite + admin token file |
HOST |
0.0.0.0 |
Bind host |
PORT |
8080 |
Bind port |
LOG_LEVEL |
info |
Python log level |
TURN_TTL_MIN |
30 |
Per-session turn map eviction (minutes) |
SHORT_ID_LEN |
2 |
Base length for short ids (auto-grows to 3 or 4 if pool exhausted) |
cd server
python -m pip install -e ".[dev]"
DATA_DIR=/tmp/hatter ADMIN_TOKEN=dev python -m hatter
# tests
python -m pytest -qcd web
npm install
npm run dev # Vite at :5173, proxies /api /health /mcp → :8080
npm run build # outputs to web/distserver/tests/mcp_smoke.py boots a stdio echo server, mints a group, connects via the mcp Python client to /mcp, and verifies turn rotation:
cd server
DATA_DIR=/tmp/hatter ADMIN_TOKEN=smoketok python -m hatter &
sleep 2
python tests/mcp_smoke.py # exits 0 on successv0.1 — single container, three transports (stdio, sse, streamable_http), tool-only proxy.
Not in v1: resources, prompts, sampling, roots proxying. Multi-user / per-client groups. Hosting your own MCP servers from inside Hatter (we proxy; we don't host yet).
MIT — see LICENSE. Built by Igor (Kinark) with Wren.
