A controlled Python code-mode runtime for programmable tool surfaces.
Project description
toolplane
Define your agent's tool surface once — MCP servers, CLI binaries, Python functions — and get the same curated, sandboxed Python namespace under every agent client you use: Claude Code, Codex, opencode, or your own.
Full documentation: https://oneryalcin.github.io/toolplane/
Why
Agents are strongest when they use code as the control plane: looping, filtering, retrying, and combining tools in one pass instead of bouncing through one tool call at a time. Toolplane is the runtime for that:
discover capabilities -> inspect schemas -> execute Python against a curated namespace
Instead of registering N MCP servers in every agent client (and paying for N × dozens of tool schemas in every context window, in every client, with every client's different quirks), you register one server that exposes three meta-tools. Your agent searches for what it needs, reads the exact schemas, and writes a snippet:
# one execute_code call replaces a dozen round-trips
rows = []
for repo in ["toolplane", "cli-to-py"]:
prs = await github_list_pull_requests(repo=repo, state="merged", limit=25)
for pr in prs:
rows.append({"repo": repo, "title": pr["title"], "days_open": pr["days_open"]})
handle = await save_result(rows, label="pr-audit")
return {"count": len(rows), "handle": handle}
What makes toolplane different from other code-mode runtimes:
- CLI binaries are first-class, policy-gated capabilities.
git,gh,jqbecome async Python functions behind an allowlist you control — not a raw shell. No other code-mode runtime ships this lane. - Policy escalates to a human instead of dead-ending. If a snippet needs a binary outside the allowlist, your agent client shows you a form: allow it for this session, or refuse. Declines stick; nothing is ever written back to config. (MCP elicitation; degrades to a plain refusal on clients that can't prompt.)
- Safe by default, with zero infrastructure. The default backend
(Monty) is a sandboxed interpreter
with no filesystem or network access — a pure
pip install, no Docker, no Deno, no daemon. - Engineered for how agents actually behave. Every dead end emits a
signpost (no-match searches say how to browse; policy errors name what is
allowed), errors are catchable by builtin type on every backend, and the
live namespace manifest (
toolplane://namespace) never lies about the server's configuration. This surface is certified by cold-start agent sessions, not just unit tests — the design lessons are published in Agents Don't Find Resources and the firsthand MCP Client Capability Matrix.
Quickstart
Set up a config (safe defaults: sandboxed backend, CLI disabled):
uvx toolplane init
uvx toolplane cli allow git
uvx toolplane mcp add context7 --url https://mcp.context7.com/mcp
uvx toolplane doctor
Prove it runs — put this in first.py:
v = await git("version")
return {"ok": v["ok"], "git": v["stdout"].strip()}
uvx toolplane run first.py
Then register it with your agent client (use the absolute path to your
toolplane.toml):
# Claude Code
claude mcp add toolplane -- uvx toolplane serve mcp --config /path/to/toolplane.toml
# Codex
codex mcp add toolplane -- uvx toolplane serve mcp --config /path/to/toolplane.toml
// opencode (~/.config/opencode/opencode.json)
{
"mcp": {
"toolplane": {
"type": "local",
"command": ["uvx", "toolplane", "serve", "mcp", "--config", "/path/to/toolplane.toml"]
}
}
}
Every client now sees the same three tools — search_capabilities,
get_capability_schemas, execute_code — plus the live manifest resource
toolplane://namespace and a bundled usage skill. Ask your agent to read the
manifest and go.
What your agent gets
- Three meta-tools instead of a tool catalog. Search is exact-word (an empty query lists everything); schemas come back only for the capabilities the snippet will actually use.
- A flat, async Python namespace. MCP tools as flat functions —
await context7_get_docs(...)— or canonically asawait call_tool("mcp:context7/get_docs", {...}); allowlisted CLI binaries asawait git("log", oneline=True, max_count=3)returning{'stdout', 'stderr', 'exit_code', 'ok'}. (Scoped sugar likecontext7.get_docs(...)exists on the non-default backends; the monty default is flat-only — the manifest always shows the shapes that work.) - State between runs, off the context window. Variables persist across
runs like notebook cells (a timed-out run rolls back cleanly;
await reset_session()starts fresh). For values that must survive a reset or be read as MCP resources:save_result/load_resultfor JSON-shaped data,save_artifact/load_artifactfor bytes (CSVs, images, parquet) — on Claude Code, a saved artifact materializes as a real local file. - Errors written for agents, not log files. The real exception type and
message on every backend, catchable by builtin type (
except ValueErrorfor store failures,except PermissionErrorfor CLI policy,except LookupErrorfor unknown capabilities) — identically on all three backends.
Use it as a library
The same runtime embeds directly in Python — register your own functions and execute agent-written snippets against them, sandboxed, with no server in between:
from toolplane import Toolplane
runtime = Toolplane()
@runtime.tool(tags={"math"})
def add(x: int, y: int) -> int:
"""Add two numbers."""
return x + y
result = await runtime.execute("""
value = await call_tool("add", {"x": 2, "y": 3})
return value
""")
And if you already have an agent framework, as_tool() packages the whole
runtime as one run_code tool — a plain async function with a generated
description (compact enough for OpenAI's tool-description cap) that
pydantic-ai, the OpenAI Agents SDK, and LangChain/LangGraph all accept
directly. It is sandboxed monty by default: model-authored code never
inherits the local_unsafe dev backend implicitly — that requires an
explicit backend="local_unsafe" opt-in:
from pydantic_ai import Agent
agent = Agent("anthropic:claude-sonnet-5", tools=[runtime.as_tool()])
MCP servers, explicit CLI wrappers, and scoped Python namespaces register the
same way — see the library guide
and examples for the full surface, including
config-driven setup (Toolplane.from_config), the result/artifact stores,
and runnable as_tool examples for all three frameworks.
Backends
| Backend | Use | Status |
|---|---|---|
monty |
Default. Sandboxed by construction (no filesystem/network), pure pip install. Flat callable namespace — no classes, no third-party imports. | Active |
pyodide-deno |
Opt-in for package-capable snippets (pandas/NumPy-style) via Pyodide in Deno. | Supported, feature-frozen |
local_unsafe |
In-process execution for development only. | Dev only |
A container backend (real CPython, arbitrary packages) lands when a concrete use case needs it. See Code Mode Backends for the design record.
Design goals
- Normalize heterogeneous tools into a Python-first API surface; JSON is the wire format, not the programming model.
- Keep the exposed runtime curated rather than ambiently powerful; the host decides which capabilities exist and what boundaries apply.
- Durable policy lives in config; session policy (escalation grants) dies with the process.
- Canonical capability ids are qualified (
mcp:server/tool); friendly aliases exist only when unambiguous. - Every slice earns its place through behavior, tests, or live certification.
Development
make test
make examples
make ci
make publish-check
Docs: make docs / make docs-serve. Publishing:
PYPI_TOKEN=... make publish — see the
release checklist.
See ROADMAP.md for sequencing and Architecture for code organization.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file toolplane-0.3.0.tar.gz.
File metadata
- Download URL: toolplane-0.3.0.tar.gz
- Upload date:
- Size: 163.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b5b4d311c165c85e524083a103a0f0c0cb6c9ec1d0a85b2525771d570381e32e
|
|
| MD5 |
2d3058a99b051b68d6c5b0408eb6391f
|
|
| BLAKE2b-256 |
22cb43d4ba61c523b7313a372af954eb73f1c7bb430c45265e579da269a19a50
|
File details
Details for the file toolplane-0.3.0-py3-none-any.whl.
File metadata
- Download URL: toolplane-0.3.0-py3-none-any.whl
- Upload date:
- Size: 89.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2a3ba99e6920b70d227e7b07de894034d2d7844377786e993f048cd3c8aa34f
|
|
| MD5 |
fa0d6cf59ba023b918951818bf11a0a0
|
|
| BLAKE2b-256 |
f000d0147b3cb9ef38c9019847c89656f9601ae6c79c90e8b117e7b32ce9e407
|