Namespace
Loom is itself a standard MCP server. It re‑exposes every downstream tool as <server>_<tool>. Your client points at one server; Loom fans out to all of them, and forwards small results untouched.
the context loom for mcp
mcp‑loom is a stdio proxy for the Model Context Protocol. It catches oversized tabular results before they reach your model, files them in an embedded DuckDB, and hands back a compact envelope — schema, a sample, join hints — plus SQL to query the data across every connected server.
# github_list_issues → 4,200 rows (~48k tokens) loom intercepts, and returns: { "kind": "loom_dataset_ref", "ref": "ds_github_list_issues_1", "rows": 4200, "schema": ["number","title","state", "assignee.login", …], "sample": [ … 5 rows … ], "joinHints": ["assignee.login → members.login"], "usage": "SELECT * FROM ds_github_list_issues_1 WHERE state = 'open'" }
the cost
A tool that returns four thousand rows returns them once — as JSON your model skims, half‑remembers, and pays for on every turn after. The answer you wanted is in there. So is the bloat that crowds out everything else.
how it works
Loom is itself a standard MCP server. It re‑exposes every downstream tool as <server>_<tool>. Your client points at one server; Loom fans out to all of them, and forwards small results untouched.
When a result crosses the token threshold and looks tabular, Loom ingests it into an embedded DuckDB instead of forwarding the payload. It reads the protocol’s structured channel when present, and falls back to parsing JSON — or even formatted text — under a strict never‑lie bar.
The model gets back a loom_dataset_ref: the schema with per‑column stats, a five‑row sample, join hints to other cached datasets, provenance, and an example query. The rows themselves stay in the store.
loom_query runs read‑only SQL over any cached dataset — filter, aggregate, and join across data from different servers. Large query results are cached the same way, so the loom never floods the context it just cleared.
the differentiator
Each intercepted result becomes a table. Because they all live in the same DuckDB, the model can join them — issues from one server against people from another, orders against inventory, logs against deploys.
The join key isn’t a guess: Loom scores candidate columns by name, type, and actual value overlap, and puts the best matches in the envelope’s join hints. The model reads the hint and writes the join.
SELECT i.title, m.name FROM ds_github_issues_1 i JOIN ds_linear_members_2 m ON i.assignee = m.handle WHERE i.state = 'open';
two servers, one query — the join key came from the envelope
the prime directive
Every failure path — a bad ingest, a profiling error, the engine itself — degrades to passing the original result through, untouched. A half‑built dataset is dropped before it can ever be seen. Interception can only help; it is built so it can never cost you a result.
safe by default
SELECT only, verified by the engine’s own parser, not a regex.loom_export writes csv or json to a directory you name, and nowhere else.configuration
Everything is optional but the servers list. The rest tunes when interception fires and how much it’s allowed to hold.
| Key | Default | What it does |
|---|---|---|
| tokenThreshold | 2000 | Results estimated above this (chars ÷ 4) get intercepted; smaller ones pass through byte‑identical. |
| memoryBudgetBytes | 268435456 | Soft cache budget (256 MB). Past it, query‑result datasets evict before source ones, oldest first. |
| duckdbMemoryLimit | "512MB" | Hard engine backstop. Spills to disk under pressure rather than taking down the process. |
| exportDir | "./exports" | The only place loom_export may write. Filenames are basename‑sanitized. |
| queryTimeoutMs | 30000 | Per‑query wall‑clock ceiling; a runaway query is interrupted, not left to hang. |
| restart | 5 / 1000ms | Downstream crash‑restart with exponential backoff, then a clean delist. |
get started
Write a loom.config.json listing the servers you want behind Loom. Each entry is a normal MCP stdio server — put per‑server secrets in its own env, never in the shell that launches Loom.
Then point your client — Claude Desktop, Claude Code, Cursor — at Loom instead of at the servers directly. It re‑exposes them all, and the interception is invisible until a result gets big.
{
"servers": [
{ "name": "repo", "command": "npx",
"args": ["-y",
"@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "…" } }
],
"tokenThreshold": 2000
}
{
"mcpServers": {
"loom": {
"command": "npx",
"args": ["-y", "mcp-loom",
"--config", "loom.config.json"]
}
}
}