ToolPipe
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).
How it works
- 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.
Installation
Requires Python 3.11+ and uv.
uv sync
uv run toolpipe --help
Configuration
Zero-config first: just add ToolPipe to your agent — no toolpipe.toml
needed.
toolpipe serve
On 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-servers
See 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}"
MCP client config (generic Claude/Cursor/Codex style)
Plug-and-play entry — no config file:
{
"mcpServers": {
"toolpipe": {
"command": "toolpipe",
"args": ["serve"]
}
}
}
Pinned-config entry:
{
"mcpServers": {
"toolpipe": {
"command": "toolpipe",
"args": ["serve", "--config", "/path/to/toolpipe.toml"]
}
}
}
Consent scopes
| 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.
Control tools
| 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.
CLI
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 only
Logs go to stderr (stdout is the MCP protocol channel). Payloads and secrets are never logged.
Demo
uv run toolpipe serve --config examples/toolpipe.toml
Then, 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.
Limitations
- 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-hose shared refs.
- Storage pressure fails loudly instead of evicting (
StorageLimitError); no LRU. - No
save_result(path=...)by design (avoids path-traversal policy).
Security
- 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.
Development
uv sync
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run pyright
uv build
See CHANGELOG.md for release history.
Metadata
Release files for toolpipe 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| toolpipe-0.1.0.tar.gz | 156.3 kB | Without provenance | Details for toolpipe-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| toolpipe-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for toolpipe-0.1.0-py3-none-any.whl |
Total release size: 207.1 kB
Release files / toolpipe-0.1.0.tar.gz
| Download URL | toolpipe-0.1.0.tar.gz |
|---|---|
| Size | 156.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fda02fa3ee993494138a695c069c32714b1573dbbcdd66bd4eef31841eb7172a
|
|
BLAKE2b-256 checksum How to use checksums |
dfaa43aa440eac18efe2cf1340df4a856d5e463a9f0c06a5815a79abdc269f2d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / toolpipe-0.1.0-py3-none-any.whl
| Download URL | toolpipe-0.1.0-py3-none-any.whl |
|---|---|
| Size | 50.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bc8de5e2f7292ccbc3422c5e4e3de20c95b72483d7d8492f5fa3ca85bc963220
|
|
BLAKE2b-256 checksum How to use checksums |
34dddc7b28401baef8a89b5d5617a6181803c9c0cb4e4caa4236b7efb908ffaf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for toolpipe 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.