Skip to main content

toolplane

PyPI version Python versions CI Docs License: MIT

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, jq become 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.
  • Credentials handled like they matter. OAuth servers: log in once in a browser, tokens persist Fernet-encrypted with the key in your OS keyring — every later session connects silently. API keys: reference them as keyring://<name> so toolplane.toml stays committable with zero secret material. Toolplane contains no token-handling code of its own — the flows, refresh, and encryption are FastMCP's; toolplane only configures where they happen.
  • 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.

Already using MCP servers?

Bring your existing Claude Code or Codex servers over in one command:

uvx toolplane mcp import --from claude --dry-run   # see what would happen
uvx toolplane mcp import --from claude
uvx toolplane mcp import --from codex

Import never copies secrets into the TOML: secret-looking values are moved into your OS keyring (written as keyring://... references), and mcp-remote-style OAuth wrapper entries are rewritten to direct url entries that use toolplane's encrypted token storage instead. The report tells you exactly what it did and what to run next.

Private servers and credentials

For an OAuth-protected MCP server (Linear, Notion, ...), log in once — tokens persist encrypted (key in your OS keyring), and every later agent session connects silently:

uvx toolplane mcp add linear --url https://mcp.linear.app/mcp --auth oauth
uvx toolplane mcp login linear   # one browser consent, ever

For API-key servers, store the key in the OS keyring and reference it — your toolplane.toml stays committable with no secret material in it:

uvx toolplane secret set docs-api-token   # value via prompt or stdin
[mcp.servers.internal_docs]
url = "https://docs.example.com/mcp"

[mcp.servers.internal_docs.headers]
Authorization = "keyring://docs-api-token"   # or "env://DOCS_TOKEN" for CI

See configuration for the full credential story.

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 as await call_tool("mcp:context7/get_docs", {...}); allowlisted CLI binaries as await git("log", oneline=True, max_count=3) returning {'stdout', 'stderr', 'exit_code', 'ok'}. (Scoped sugar like context7.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_result for JSON-shaped data, save_artifact/load_artifact for 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 ValueError for store failures, except PermissionError for CLI policy, except LookupError for 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.

Release files for toolplane 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for toolplane 0.4.0
File Size Uploaded
toolplane-0.4.0.tar.gz 193.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for toolplane 0.4.0
File Interpreter ABI Platform
toolplane-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 301.8 kB

Release files / toolplane-0.4.0.tar.gz

Download URL toolplane-0.4.0.tar.gz
Size 193.8 kB
Tags Source
SHA-256 checksum
How to use checksums
497fa9c01a725daedd30f71d0ae84caa52c13c316e360df711f8b0a70bb20dcc
BLAKE2b-256 checksum
How to use checksums
82194568b637d140cd21e7f3d88408700ab2d98b01be42a875303a9090203d80
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / toolplane-0.4.0-py3-none-any.whl

Download URL toolplane-0.4.0-py3-none-any.whl
Size 108.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2d396a7072a77eb3097d2d230de267065407bf0d6a01a240b1059e70b0ce4a6c
BLAKE2b-256 checksum
How to use checksums
9b4d9f148322ff98de68f0462f2c252ee15943b37b632cb3e919121cdf2a6243
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page