Skip to main content

A controlled Python code-mode runtime for programmable tool surfaces.

Project description

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.
  • 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 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

toolplane-0.3.0.tar.gz (163.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

toolplane-0.3.0-py3-none-any.whl (89.2 kB view details)

Uploaded Python 3

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

Hashes for toolplane-0.3.0.tar.gz
Algorithm Hash digest
SHA256 b5b4d311c165c85e524083a103a0f0c0cb6c9ec1d0a85b2525771d570381e32e
MD5 2d3058a99b051b68d6c5b0408eb6391f
BLAKE2b-256 22cb43d4ba61c523b7313a372af954eb73f1c7bb430c45265e579da269a19a50

See more details on using hashes here.

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

Hashes for toolplane-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e2a3ba99e6920b70d227e7b07de894034d2d7844377786e993f048cd3c8aa34f
MD5 fa0d6cf59ba023b918951818bf11a0a0
BLAKE2b-256 f000d0147b3cb9ef38c9019847c89656f9601ae6c79c90e8b117e7b32ce9e407

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page