Pass MCP results by reference, not through the LLM.
ToolPipe is an MCP data-plane proxy that keeps large tool outputs out of LLM context by storing them as references and piping the underlying data directly between tools.
Without ToolPipe: Tool A → 18 MB → LLM → Tool B
With ToolPipe: Tool A → ToolPipe storage ├→ LLM: res_xxx └→ Tool B: actual data
The LLM stays the reasoning plane (it sees a small res_* reference plus a
preview). ToolPipe is the data plane (it holds the bytes and feeds them to
the next tool).
- ToolPipe exposes downstream MCP servers' tools through one server, namespaced
(
producer_generate_customers,analytics_calculate_statistics). - When a downstream tool returns more than
inline_max_bytes(default 32 KiB), ToolPipe stores the payload locally (SQLite metadata + content-hashed blobs) and returns a compact reference envelope instead. - The agent then uses control tools to inspect, read, search, or select from the stored result — or pipes data straight into another tool without the payload ever re-entering context.
100,000 customer objects (~5.1 MiB)
│ producer_generate_customers
▼
ToolPipe res_A (542 B: ref + preview)
│ mapping customers[].revenue
▼ analytics_calculate_statistics
{count: 100000, min: 0.0, max: 4999.0, mean: 2499.5} (60 B)
Measured on the bundled demo: 5,355,595 B → 542 B (~9881x smaller) for the producer step; the final stats response is 60 B.
Requires Python 3.11+.
Try it without installing (needs uv):
uvx toolpipe serveInstall as a tool:
pipx install toolpipe # isolated CLI: `toolpipe serve`
# or
uv tool install toolpipe # uv equivalentInstall into a project:
pip install toolpipe
# or
uv add toolpipeFrom source (development):
git clone https://github.com/kunalkaul/toolpipe
cd toolpipe
uv sync
uv run toolpipe --helpZero-config first: just add ToolPipe to your agent — no toolpipe.toml
needed.
toolpipe serveOn startup ToolPipe auto-discovers the MCP servers you already configured in
Claude Code (.mcp.json), Cursor (.cursor/mcp.json), Codex
(.codex/config.toml), OpenCode (opencode.json), and Zed
(.zed/settings.json), workspace files first. Discovered servers spawn
nothing until you approve them: the first time the agent routes a call through
one, you are asked — once, for this session, for this workspace, or always
— and your choice is saved. Static TOML and explicit imports work as before:
toolpipe serve --config toolpipe.toml
toolpipe serve --import-mcp-json path/to/mcp.json
toolpipe serve --import-codex-toml ~/.codex/config.toml
toolpipe serve --no-discover --no-dynamic-serversSee examples/toolpipe.toml (working demo config). Format:
[toolpipe]
name = "ToolPipe"
[results]
storage_dir = ".toolpipe"
inline_max_bytes = 32768 # below this: pass through unchanged
preview_max_bytes = 4096 # preview budget inside reference envelopes
read_max_bytes = 16384 # max bytes per toolpipe_read_result call
max_result_bytes = 104857600
max_store_bytes = 1073741824
ttl_seconds = 3600
[servers.producer]
command = "uv"
args = ["run", "python", "examples/demo/producer_mcp.py"]
[servers.analytics]
command = "uv"
args = ["run", "python", "examples/demo/analytics_mcp.py"]
[servers.remote]
url = "http://localhost:9001/mcp"Secrets use ${VAR} substitution and fail fast when unset:
[servers.github]
command = "npx"
args = ["-y", "@example/github-mcp"]
[servers.github.env]
GITHUB_TOKEN = "${GITHUB_TOKEN}"Plug-and-play entry — no config file, no local checkout:
{
"mcpServers": {
"toolpipe": {
"command": "uvx",
"args": ["toolpipe", "serve"]
}
}
}If uvx cold-start latency matters, pipx install toolpipe (or
uv tool install toolpipe) and use "command": "toolpipe" instead.
Pinned-config entry:
{
"mcpServers": {
"toolpipe": {
"command": "toolpipe",
"args": ["serve", "--config", "/path/to/toolpipe.toml"]
}
}
}| Scope | Meaning | Stored in |
|---|---|---|
once |
This one call only | Memory |
session |
This ToolPipe process | Memory |
workspace |
This project, across restarts | <storage_dir>/servers.json |
always |
Everywhere, across restarts | ~/.toolpipe/servers.json |
Approvals persist connect info with ${VAR} references unresolved (resolved
secrets are never written to disk). Removing a server revokes it everywhere
but keeps stored results. If your client cannot render approval prompts, the
agent asks in chat and calls toolpipe_approve_server instead.
| Tool | Purpose |
|---|---|
toolpipe_inspect_result |
Metadata + bounded preview for a res_* ref |
toolpipe_read_result |
Bounded byte-slice reads (diagnostic; prefer select for JSON) |
toolpipe_select_result |
JMESPath selection, e.g. customers[].revenue (small→inline, large→new ref) |
toolpipe_search_result |
Substring/regex search; line matches for text, leaf paths for JSON |
toolpipe_release_result |
Drop a stored result immediately |
toolpipe_pipe_result |
Pipe mapped data directly into a downstream tool |
toolpipe_pending_servers |
Discovered servers waiting for approval |
toolpipe_approve_server |
Approve a server (once/session/workspace/always) |
toolpipe_add_server |
Manually register a server discovery cannot see |
toolpipe_remove_server |
Revoke a server (stored results are kept) |
toolpipe_list_servers |
Servers with scope, status, tool counts (env redacted) |
toolpipe_set_policy |
auto/always/never per server or server:tool |
toolpipe_get_policy |
Configured policies (default is auto) |
Selectors are JMESPath only — no Python/Jinja/shell evaluation, ever.
toolpipe serve [--config toolpipe.toml] [--log-level INFO]
toolpipe serve --import-mcp-json mcp.json --import-codex-toml config.toml
toolpipe serve --no-discover --no-dynamic-servers
toolpipe inspect res_abc123 --config toolpipe.toml
toolpipe results --config toolpipe.toml
toolpipe stats --config toolpipe.toml # includes "estimated bytes avoided"
toolpipe clean --config toolpipe.toml # expired results onlyLogs go to stderr (stdout is the MCP protocol channel). Payloads and secrets are never logged.
uv run toolpipe serve --config examples/toolpipe.tomlThen, from any MCP client: producer_generate_customers(count=100000) returns
a res_* reference; toolpipe_pipe_result with mapping
{"values": "customers[].revenue"} into analytics_calculate_statistics
returns the stats without the array crossing context.
- STDIO upstream only; one process = one result scope (no multi-user mode).
- JSON and text virtualization only — images/audio/binary pass through.
- No CSV/row operations, no streaming, no cross-host shared refs.
- Storage pressure fails loudly instead of evicting (
StorageLimitError); no LRU. - No
save_result(path=...)by design (avoids path-traversal policy).
- JMESPath only;
eval/exec/shell/Jinja/JS are not supported anywhere. - Refs are opaque random tokens, never filesystem paths or hashes.
- Pipe targets must be approved downstream tools;
toolpipe_*control tools are rejected. - First use asks for approval showing the exact command;
denypersists nothing. - Scope files keep
${VAR}references unresolved; listings redact env values. neverpolicy disables virtualization only — a context-bloat footgun, not a data leak.- All size/TTL limits enforced;
stdoutcarries protocol output only.
uv sync
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run pyright
uv buildSee CHANGELOG.md for release history.