toolplane 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. {toolplane-0.2.0 → toolplane-0.3.0}/AGENTS.md +9 -0
  2. {toolplane-0.2.0 → toolplane-0.3.0}/Makefile +3 -0
  3. toolplane-0.3.0/PKG-INFO +248 -0
  4. toolplane-0.3.0/README.md +221 -0
  5. {toolplane-0.2.0 → toolplane-0.3.0}/ROADMAP.md +7 -5
  6. toolplane-0.3.0/docs/agents-dont-find-resources.md +90 -0
  7. {toolplane-0.2.0 → toolplane-0.3.0}/docs/architecture.md +6 -2
  8. {toolplane-0.2.0 → toolplane-0.3.0}/docs/code-mode-backends.md +7 -3
  9. toolplane-0.3.0/docs/codex-elicitation-title-bug.md +102 -0
  10. toolplane-0.3.0/docs/configuration.md +283 -0
  11. toolplane-0.3.0/docs/mcp-client-capability-matrix.md +72 -0
  12. toolplane-0.3.0/docs/mcp-client-capability-spike.md +96 -0
  13. toolplane-0.3.0/docs/monty-session-spike.md +169 -0
  14. toolplane-0.3.0/docs/result-store-design.md +277 -0
  15. {toolplane-0.2.0 → toolplane-0.3.0}/docs/toolplane-mcp.md +37 -31
  16. {toolplane-0.2.0 → toolplane-0.3.0}/examples/README.md +21 -0
  17. toolplane-0.3.0/examples/as_tool_langgraph.py +83 -0
  18. toolplane-0.3.0/examples/as_tool_openai_agents.py +68 -0
  19. toolplane-0.3.0/examples/as_tool_pydantic_ai.py +78 -0
  20. toolplane-0.3.0/examples/mcp_client_probe.py +144 -0
  21. {toolplane-0.2.0 → toolplane-0.3.0}/mkdocs.yml +7 -0
  22. {toolplane-0.2.0 → toolplane-0.3.0}/pyproject.toml +8 -2
  23. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/__init__.py +7 -1
  24. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/adapters/ambient_cli.py +172 -7
  25. toolplane-0.3.0/src/toolplane/artifacts.py +391 -0
  26. toolplane-0.3.0/src/toolplane/audit.py +99 -0
  27. toolplane-0.3.0/src/toolplane/backends/_python.py +134 -0
  28. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/backends/local.py +121 -3
  29. toolplane-0.3.0/src/toolplane/backends/monty.py +583 -0
  30. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/backends/pyodide_deno.py +225 -11
  31. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/bridges/base.py +10 -0
  32. toolplane-0.3.0/src/toolplane/bridges/in_process.py +193 -0
  33. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/bridges/rpc.py +8 -1
  34. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/capabilities.py +19 -0
  35. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/config.py +55 -0
  36. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/discovery.py +10 -2
  37. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/errors.py +18 -2
  38. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/execution.py +4 -0
  39. toolplane-0.3.0/src/toolplane/mcp_facade.py +366 -0
  40. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/mcp_lifecycle.py +5 -4
  41. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/registry.py +90 -5
  42. toolplane-0.3.0/src/toolplane/results.py +301 -0
  43. toolplane-0.3.0/src/toolplane/runtime.py +718 -0
  44. toolplane-0.3.0/src/toolplane/skills/driving-toolplane/SKILL.md +168 -0
  45. {toolplane-0.2.0 → toolplane-0.3.0}/tests/test_ambient_cli.py +128 -0
  46. toolplane-0.3.0/tests/test_artifact_store.py +163 -0
  47. toolplane-0.3.0/tests/test_as_tool.py +226 -0
  48. toolplane-0.3.0/tests/test_audit_log.py +340 -0
  49. toolplane-0.3.0/tests/test_cli_escalation.py +515 -0
  50. {toolplane-0.2.0 → toolplane-0.3.0}/tests/test_cli_foundation.py +1 -1
  51. {toolplane-0.2.0 → toolplane-0.3.0}/tests/test_cli_to_py_adapter.py +73 -0
  52. {toolplane-0.2.0 → toolplane-0.3.0}/tests/test_mcp_facade.py +198 -7
  53. {toolplane-0.2.0 → toolplane-0.3.0}/tests/test_mcp_lifecycle.py +4 -4
  54. {toolplane-0.2.0 → toolplane-0.3.0}/tests/test_monty_backend.py +170 -0
  55. toolplane-0.3.0/tests/test_monty_session.py +392 -0
  56. toolplane-0.3.0/tests/test_pyodide_deno_backend.py +327 -0
  57. toolplane-0.3.0/tests/test_result_store.py +549 -0
  58. {toolplane-0.2.0 → toolplane-0.3.0}/tests/test_toolplane.py +106 -0
  59. toolplane-0.2.0/PKG-INFO +0 -284
  60. toolplane-0.2.0/README.md +0 -257
  61. toolplane-0.2.0/docs/configuration.md +0 -163
  62. toolplane-0.2.0/src/toolplane/backends/_python.py +0 -12
  63. toolplane-0.2.0/src/toolplane/backends/monty.py +0 -245
  64. toolplane-0.2.0/src/toolplane/bridges/in_process.py +0 -68
  65. toolplane-0.2.0/src/toolplane/mcp_facade.py +0 -126
  66. toolplane-0.2.0/src/toolplane/runtime.py +0 -259
  67. toolplane-0.2.0/tests/test_pyodide_deno_backend.py +0 -132
  68. {toolplane-0.2.0 → toolplane-0.3.0}/.github/workflows/ci.yml +0 -0
  69. {toolplane-0.2.0 → toolplane-0.3.0}/.github/workflows/pages.yml +0 -0
  70. {toolplane-0.2.0 → toolplane-0.3.0}/.gitignore +0 -0
  71. {toolplane-0.2.0 → toolplane-0.3.0}/CLAUDE.md +0 -0
  72. {toolplane-0.2.0 → toolplane-0.3.0}/docs/development/release-checklist.md +0 -0
  73. {toolplane-0.2.0 → toolplane-0.3.0}/docs/examples/ambient-cli.md +0 -0
  74. {toolplane-0.2.0 → toolplane-0.3.0}/docs/examples/index.md +0 -0
  75. {toolplane-0.2.0 → toolplane-0.3.0}/docs/examples/mcp-tools.md +0 -0
  76. {toolplane-0.2.0 → toolplane-0.3.0}/docs/index.md +0 -0
  77. {toolplane-0.2.0 → toolplane-0.3.0}/docs/mcp-lifecycle-spike.md +0 -0
  78. {toolplane-0.2.0 → toolplane-0.3.0}/docs/monty-default-spike.md +0 -0
  79. {toolplane-0.2.0 → toolplane-0.3.0}/docs/stylesheets/extra.css +0 -0
  80. {toolplane-0.2.0 → toolplane-0.3.0}/examples/ambient_cli_git.py +0 -0
  81. {toolplane-0.2.0 → toolplane-0.3.0}/examples/context7_remote.py +0 -0
  82. {toolplane-0.2.0 → toolplane-0.3.0}/examples/fastmcp_in_process.py +0 -0
  83. {toolplane-0.2.0 → toolplane-0.3.0}/examples/from_config.py +0 -0
  84. {toolplane-0.2.0 → toolplane-0.3.0}/examples/mcp_stdio_config.py +0 -0
  85. {toolplane-0.2.0 → toolplane-0.3.0}/examples/mcp_stdio_server.py +0 -0
  86. {toolplane-0.2.0 → toolplane-0.3.0}/examples/mcp_text_server.py +0 -0
  87. {toolplane-0.2.0 → toolplane-0.3.0}/examples/mixed_capability_report.py +0 -0
  88. {toolplane-0.2.0 → toolplane-0.3.0}/examples/multi_server.py +0 -0
  89. {toolplane-0.2.0 → toolplane-0.3.0}/examples/multi_server.toml +0 -0
  90. {toolplane-0.2.0 → toolplane-0.3.0}/examples/scoped_namespaces_context7.py +0 -0
  91. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/adapters/__init__.py +0 -0
  92. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/adapters/cli_to_py.py +0 -0
  93. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/adapters/mcp.py +0 -0
  94. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/adapters/python.py +0 -0
  95. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/backends/__init__.py +0 -0
  96. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/backends/base.py +0 -0
  97. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/bridges/__init__.py +0 -0
  98. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/cli.py +0 -0
  99. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/config_edit.py +0 -0
  100. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/doctor.py +0 -0
  101. {toolplane-0.2.0 → toolplane-0.3.0}/src/toolplane/policy.py +0 -0
  102. {toolplane-0.2.0 → toolplane-0.3.0}/tests/test_bridges.py +0 -0
  103. {toolplane-0.2.0 → toolplane-0.3.0}/tests/test_config.py +0 -0
  104. {toolplane-0.2.0 → toolplane-0.3.0}/tests/test_mcp_adapter.py +0 -0
@@ -54,6 +54,15 @@ more custom code around it.
54
54
  capability envelope.
55
55
  - Pyodide+Deno is the opt-in sandbox for package-capable snippets, especially
56
56
  pandas/NumPy-style workflows.
57
+ - Monty sessions (`docs/monty-session-spike.md`) persist the live namespace
58
+ across runs by default (stdio/embedded only): a timed-out run rolls the
59
+ namespace back via a pre-run snapshot — never remove that snapshot, it is
60
+ what keeps a cancelled run from poisoning the interpreter
61
+ (pydantic/monty#533) — and host-side effects are never rolled back.
62
+ - The result store (`save_result`/`load_result`, `docs/result-store-design.md`)
63
+ persists JSON-shaped data across runs within one process; it never persists
64
+ interpreters or anything to disk. With sessions on, its remaining jobs are
65
+ surviving a session reset, crossing backends, and MCP-resource reads.
57
66
  - Docker, Modal, E2B, and Blaxel are needed for arbitrary CPython packages,
58
67
  native system dependencies, subprocesses, GPUs, remote isolation, or
59
68
  long-running jobs.
@@ -27,6 +27,9 @@ examples:
27
27
  $(UV) run --no-project --with-editable . python examples/mcp_stdio_config.py
28
28
  $(UV) run --no-project --with-editable . python examples/multi_server.py
29
29
  $(UV) run --no-project --with-editable . python examples/from_config.py
30
+ env -u ANTHROPIC_API_KEY -u OPENAI_API_KEY $(UV) run --no-project --with-editable . --with pydantic-ai python examples/as_tool_pydantic_ai.py
31
+ env -u ANTHROPIC_API_KEY -u OPENAI_API_KEY $(UV) run --no-project --with-editable . --with openai-agents python examples/as_tool_openai_agents.py
32
+ env -u ANTHROPIC_API_KEY -u OPENAI_API_KEY $(UV) run --no-project --with-editable . --with langchain-core python examples/as_tool_langgraph.py
30
33
 
31
34
  docs:
32
35
  $(UV) run --no-project --with-editable ".[docs]" mkdocs build --strict --site-dir $(SITE_DIR)
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.4
2
+ Name: toolplane
3
+ Version: 0.3.0
4
+ Summary: A controlled Python code-mode runtime for programmable tool surfaces.
5
+ License: MIT
6
+ Keywords: agents,cli,code-mode,mcp,runtime,tools
7
+ Classifier: Development Status :: 2 - Pre-Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: cli-to-py>=0.2
17
+ Requires-Dist: fastmcp>=3.1
18
+ Requires-Dist: pydantic-monty<0.0.19,>=0.0.18
19
+ Requires-Dist: pydantic>=2.0
20
+ Requires-Dist: tomlkit>=0.15.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8; extra == 'dev'
23
+ Provides-Extra: docs
24
+ Requires-Dist: mkdocs-material<9.7,>=9.5; extra == 'docs'
25
+ Requires-Dist: mkdocs>=1.6; extra == 'docs'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # toolplane
29
+
30
+ [![PyPI version](https://img.shields.io/pypi/v/toolplane)](https://pypi.org/project/toolplane/)
31
+ [![Python versions](https://img.shields.io/pypi/pyversions/toolplane)](https://pypi.org/project/toolplane/)
32
+ [![CI](https://github.com/oneryalcin/toolplane/actions/workflows/ci.yml/badge.svg)](https://github.com/oneryalcin/toolplane/actions/workflows/ci.yml)
33
+ [![Docs](https://github.com/oneryalcin/toolplane/actions/workflows/pages.yml/badge.svg)](https://github.com/oneryalcin/toolplane/actions/workflows/pages.yml)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
35
+
36
+ Define your agent's tool surface **once** — MCP servers, CLI binaries, Python
37
+ functions — and get the same curated, sandboxed Python namespace under every
38
+ agent client you use: Claude Code, Codex, opencode, or your own.
39
+
40
+ Full documentation: https://oneryalcin.github.io/toolplane/
41
+
42
+ ## Why
43
+
44
+ Agents are strongest when they use code as the control plane: looping,
45
+ filtering, retrying, and combining tools in one pass instead of bouncing
46
+ through one tool call at a time. Toolplane is the runtime for that:
47
+
48
+ ```text
49
+ discover capabilities -> inspect schemas -> execute Python against a curated namespace
50
+ ```
51
+
52
+ Instead of registering N MCP servers in every agent client (and paying for
53
+ N × dozens of tool schemas in every context window, in every client, with
54
+ every client's different quirks), you register **one** server that exposes
55
+ three meta-tools. Your agent searches for what it needs, reads the exact
56
+ schemas, and writes a snippet:
57
+
58
+ ```python
59
+ # one execute_code call replaces a dozen round-trips
60
+ rows = []
61
+ for repo in ["toolplane", "cli-to-py"]:
62
+ prs = await github_list_pull_requests(repo=repo, state="merged", limit=25)
63
+ for pr in prs:
64
+ rows.append({"repo": repo, "title": pr["title"], "days_open": pr["days_open"]})
65
+ handle = await save_result(rows, label="pr-audit")
66
+ return {"count": len(rows), "handle": handle}
67
+ ```
68
+
69
+ What makes toolplane different from other code-mode runtimes:
70
+
71
+ - **CLI binaries are first-class, policy-gated capabilities.** `git`, `gh`,
72
+ `jq` become async Python functions behind an allowlist you control — not a
73
+ raw shell. No other code-mode runtime ships this lane.
74
+ - **Policy escalates to a human instead of dead-ending.** If a snippet needs
75
+ a binary outside the allowlist, your agent client shows *you* a form:
76
+ allow it for this session, or refuse. Declines stick; nothing is ever
77
+ written back to config. (MCP elicitation; degrades to a plain refusal on
78
+ clients that can't prompt.)
79
+ - **Safe by default, with zero infrastructure.** The default backend
80
+ ([Monty](https://github.com/pydantic/monty)) is a sandboxed interpreter
81
+ with no filesystem or network access — a pure `pip install`, no Docker, no
82
+ Deno, no daemon.
83
+ - **Engineered for how agents actually behave.** Every dead end emits a
84
+ signpost (no-match searches say how to browse; policy errors name what is
85
+ allowed), errors are catchable by builtin type on every backend, and the
86
+ live namespace manifest (`toolplane://namespace`) never lies about the
87
+ server's configuration. This surface is certified by cold-start agent
88
+ sessions, not just unit tests — the design lessons are published in
89
+ [Agents Don't Find Resources](https://oneryalcin.github.io/toolplane/agents-dont-find-resources/)
90
+ and the firsthand
91
+ [MCP Client Capability Matrix](https://oneryalcin.github.io/toolplane/mcp-client-capability-matrix/).
92
+
93
+ ## Quickstart
94
+
95
+ Set up a config (safe defaults: sandboxed backend, CLI disabled):
96
+
97
+ ```bash
98
+ uvx toolplane init
99
+ uvx toolplane cli allow git
100
+ uvx toolplane mcp add context7 --url https://mcp.context7.com/mcp
101
+ uvx toolplane doctor
102
+ ```
103
+
104
+ Prove it runs — put this in `first.py`:
105
+
106
+ ```python
107
+ v = await git("version")
108
+ return {"ok": v["ok"], "git": v["stdout"].strip()}
109
+ ```
110
+
111
+ ```bash
112
+ uvx toolplane run first.py
113
+ ```
114
+
115
+ Then register it with your agent client (use the absolute path to your
116
+ `toolplane.toml`):
117
+
118
+ ```bash
119
+ # Claude Code
120
+ claude mcp add toolplane -- uvx toolplane serve mcp --config /path/to/toolplane.toml
121
+
122
+ # Codex
123
+ codex mcp add toolplane -- uvx toolplane serve mcp --config /path/to/toolplane.toml
124
+ ```
125
+
126
+ ```jsonc
127
+ // opencode (~/.config/opencode/opencode.json)
128
+ {
129
+ "mcp": {
130
+ "toolplane": {
131
+ "type": "local",
132
+ "command": ["uvx", "toolplane", "serve", "mcp", "--config", "/path/to/toolplane.toml"]
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ Every client now sees the same three tools — `search_capabilities`,
139
+ `get_capability_schemas`, `execute_code` — plus the live manifest resource
140
+ `toolplane://namespace` and a bundled usage skill. Ask your agent to read the
141
+ manifest and go.
142
+
143
+ ## What your agent gets
144
+
145
+ - **Three meta-tools instead of a tool catalog.** Search is exact-word (an
146
+ empty query lists everything); schemas come back only for the capabilities
147
+ the snippet will actually use.
148
+ - **A flat, async Python namespace.** MCP tools as flat functions —
149
+ `await context7_get_docs(...)` — or canonically as
150
+ `await call_tool("mcp:context7/get_docs", {...})`; allowlisted CLI
151
+ binaries as `await git("log", oneline=True, max_count=3)` returning
152
+ `{'stdout', 'stderr', 'exit_code', 'ok'}`. (Scoped sugar like
153
+ `context7.get_docs(...)` exists on the non-default backends; the monty
154
+ default is flat-only — the manifest always shows the shapes that work.)
155
+ - **State between runs, off the context window.** Variables persist across
156
+ runs like notebook cells (a timed-out run rolls back cleanly;
157
+ `await reset_session()` starts fresh). For values that must survive a
158
+ reset or be read as MCP resources: `save_result`/`load_result` for
159
+ JSON-shaped data, `save_artifact`/`load_artifact` for bytes (CSVs,
160
+ images, parquet) — on Claude Code, a saved artifact materializes as a
161
+ real local file.
162
+ - **Errors written for agents, not log files.** The real exception type and
163
+ message on every backend, catchable by builtin type (`except ValueError`
164
+ for store failures, `except PermissionError` for CLI policy,
165
+ `except LookupError` for unknown capabilities) — identically on all three
166
+ backends.
167
+
168
+ ## Use it as a library
169
+
170
+ The same runtime embeds directly in Python — register your own functions and
171
+ execute agent-written snippets against them, sandboxed, with no server in
172
+ between:
173
+
174
+ ```python
175
+ from toolplane import Toolplane
176
+
177
+ runtime = Toolplane()
178
+
179
+ @runtime.tool(tags={"math"})
180
+ def add(x: int, y: int) -> int:
181
+ """Add two numbers."""
182
+ return x + y
183
+
184
+ result = await runtime.execute("""
185
+ value = await call_tool("add", {"x": 2, "y": 3})
186
+ return value
187
+ """)
188
+ ```
189
+
190
+ And if you already have an agent framework, `as_tool()` packages the whole
191
+ runtime as one `run_code` tool — a plain async function with a generated
192
+ description (compact enough for OpenAI's tool-description cap) that
193
+ pydantic-ai, the OpenAI Agents SDK, and LangChain/LangGraph all accept
194
+ directly. It is sandboxed monty by default: model-authored code never
195
+ inherits the `local_unsafe` dev backend implicitly — that requires an
196
+ explicit `backend="local_unsafe"` opt-in:
197
+
198
+ ```python
199
+ from pydantic_ai import Agent
200
+
201
+ agent = Agent("anthropic:claude-sonnet-5", tools=[runtime.as_tool()])
202
+ ```
203
+
204
+ MCP servers, explicit CLI wrappers, and scoped Python namespaces register the
205
+ same way — see the [library guide](https://oneryalcin.github.io/toolplane/)
206
+ and [examples](examples/README.md) for the full surface, including
207
+ config-driven setup (`Toolplane.from_config`), the result/artifact stores,
208
+ and runnable `as_tool` examples for all three frameworks.
209
+
210
+ ## Backends
211
+
212
+ | Backend | Use | Status |
213
+ | --- | --- | --- |
214
+ | `monty` | Default. Sandboxed by construction (no filesystem/network), pure pip install. Flat callable namespace — no classes, no third-party imports. | Active |
215
+ | `pyodide-deno` | Opt-in for package-capable snippets (pandas/NumPy-style) via Pyodide in Deno. | Supported, feature-frozen |
216
+ | `local_unsafe` | In-process execution for development only. | Dev only |
217
+
218
+ A container backend (real CPython, arbitrary packages) lands when a concrete
219
+ use case needs it. See [Code Mode Backends](docs/code-mode-backends.md) for
220
+ the design record.
221
+
222
+ ## Design goals
223
+
224
+ - Normalize heterogeneous tools into a Python-first API surface; JSON is the
225
+ wire format, not the programming model.
226
+ - Keep the exposed runtime curated rather than ambiently powerful; the host
227
+ decides which capabilities exist and what boundaries apply.
228
+ - Durable policy lives in config; session policy (escalation grants) dies
229
+ with the process.
230
+ - Canonical capability ids are qualified (`mcp:server/tool`); friendly
231
+ aliases exist only when unambiguous.
232
+ - Every slice earns its place through behavior, tests, or live certification.
233
+
234
+ ## Development
235
+
236
+ ```bash
237
+ make test
238
+ make examples
239
+ make ci
240
+ make publish-check
241
+ ```
242
+
243
+ Docs: `make docs` / `make docs-serve`. Publishing:
244
+ `PYPI_TOKEN=... make publish` — see the
245
+ [release checklist](docs/development/release-checklist.md).
246
+
247
+ See [ROADMAP.md](ROADMAP.md) for sequencing and
248
+ [Architecture](docs/architecture.md) for code organization.
@@ -0,0 +1,221 @@
1
+ # toolplane
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/toolplane)](https://pypi.org/project/toolplane/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/toolplane)](https://pypi.org/project/toolplane/)
5
+ [![CI](https://github.com/oneryalcin/toolplane/actions/workflows/ci.yml/badge.svg)](https://github.com/oneryalcin/toolplane/actions/workflows/ci.yml)
6
+ [![Docs](https://github.com/oneryalcin/toolplane/actions/workflows/pages.yml/badge.svg)](https://github.com/oneryalcin/toolplane/actions/workflows/pages.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
+
9
+ Define your agent's tool surface **once** — MCP servers, CLI binaries, Python
10
+ functions — and get the same curated, sandboxed Python namespace under every
11
+ agent client you use: Claude Code, Codex, opencode, or your own.
12
+
13
+ Full documentation: https://oneryalcin.github.io/toolplane/
14
+
15
+ ## Why
16
+
17
+ Agents are strongest when they use code as the control plane: looping,
18
+ filtering, retrying, and combining tools in one pass instead of bouncing
19
+ through one tool call at a time. Toolplane is the runtime for that:
20
+
21
+ ```text
22
+ discover capabilities -> inspect schemas -> execute Python against a curated namespace
23
+ ```
24
+
25
+ Instead of registering N MCP servers in every agent client (and paying for
26
+ N × dozens of tool schemas in every context window, in every client, with
27
+ every client's different quirks), you register **one** server that exposes
28
+ three meta-tools. Your agent searches for what it needs, reads the exact
29
+ schemas, and writes a snippet:
30
+
31
+ ```python
32
+ # one execute_code call replaces a dozen round-trips
33
+ rows = []
34
+ for repo in ["toolplane", "cli-to-py"]:
35
+ prs = await github_list_pull_requests(repo=repo, state="merged", limit=25)
36
+ for pr in prs:
37
+ rows.append({"repo": repo, "title": pr["title"], "days_open": pr["days_open"]})
38
+ handle = await save_result(rows, label="pr-audit")
39
+ return {"count": len(rows), "handle": handle}
40
+ ```
41
+
42
+ What makes toolplane different from other code-mode runtimes:
43
+
44
+ - **CLI binaries are first-class, policy-gated capabilities.** `git`, `gh`,
45
+ `jq` become async Python functions behind an allowlist you control — not a
46
+ raw shell. No other code-mode runtime ships this lane.
47
+ - **Policy escalates to a human instead of dead-ending.** If a snippet needs
48
+ a binary outside the allowlist, your agent client shows *you* a form:
49
+ allow it for this session, or refuse. Declines stick; nothing is ever
50
+ written back to config. (MCP elicitation; degrades to a plain refusal on
51
+ clients that can't prompt.)
52
+ - **Safe by default, with zero infrastructure.** The default backend
53
+ ([Monty](https://github.com/pydantic/monty)) is a sandboxed interpreter
54
+ with no filesystem or network access — a pure `pip install`, no Docker, no
55
+ Deno, no daemon.
56
+ - **Engineered for how agents actually behave.** Every dead end emits a
57
+ signpost (no-match searches say how to browse; policy errors name what is
58
+ allowed), errors are catchable by builtin type on every backend, and the
59
+ live namespace manifest (`toolplane://namespace`) never lies about the
60
+ server's configuration. This surface is certified by cold-start agent
61
+ sessions, not just unit tests — the design lessons are published in
62
+ [Agents Don't Find Resources](https://oneryalcin.github.io/toolplane/agents-dont-find-resources/)
63
+ and the firsthand
64
+ [MCP Client Capability Matrix](https://oneryalcin.github.io/toolplane/mcp-client-capability-matrix/).
65
+
66
+ ## Quickstart
67
+
68
+ Set up a config (safe defaults: sandboxed backend, CLI disabled):
69
+
70
+ ```bash
71
+ uvx toolplane init
72
+ uvx toolplane cli allow git
73
+ uvx toolplane mcp add context7 --url https://mcp.context7.com/mcp
74
+ uvx toolplane doctor
75
+ ```
76
+
77
+ Prove it runs — put this in `first.py`:
78
+
79
+ ```python
80
+ v = await git("version")
81
+ return {"ok": v["ok"], "git": v["stdout"].strip()}
82
+ ```
83
+
84
+ ```bash
85
+ uvx toolplane run first.py
86
+ ```
87
+
88
+ Then register it with your agent client (use the absolute path to your
89
+ `toolplane.toml`):
90
+
91
+ ```bash
92
+ # Claude Code
93
+ claude mcp add toolplane -- uvx toolplane serve mcp --config /path/to/toolplane.toml
94
+
95
+ # Codex
96
+ codex mcp add toolplane -- uvx toolplane serve mcp --config /path/to/toolplane.toml
97
+ ```
98
+
99
+ ```jsonc
100
+ // opencode (~/.config/opencode/opencode.json)
101
+ {
102
+ "mcp": {
103
+ "toolplane": {
104
+ "type": "local",
105
+ "command": ["uvx", "toolplane", "serve", "mcp", "--config", "/path/to/toolplane.toml"]
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ Every client now sees the same three tools — `search_capabilities`,
112
+ `get_capability_schemas`, `execute_code` — plus the live manifest resource
113
+ `toolplane://namespace` and a bundled usage skill. Ask your agent to read the
114
+ manifest and go.
115
+
116
+ ## What your agent gets
117
+
118
+ - **Three meta-tools instead of a tool catalog.** Search is exact-word (an
119
+ empty query lists everything); schemas come back only for the capabilities
120
+ the snippet will actually use.
121
+ - **A flat, async Python namespace.** MCP tools as flat functions —
122
+ `await context7_get_docs(...)` — or canonically as
123
+ `await call_tool("mcp:context7/get_docs", {...})`; allowlisted CLI
124
+ binaries as `await git("log", oneline=True, max_count=3)` returning
125
+ `{'stdout', 'stderr', 'exit_code', 'ok'}`. (Scoped sugar like
126
+ `context7.get_docs(...)` exists on the non-default backends; the monty
127
+ default is flat-only — the manifest always shows the shapes that work.)
128
+ - **State between runs, off the context window.** Variables persist across
129
+ runs like notebook cells (a timed-out run rolls back cleanly;
130
+ `await reset_session()` starts fresh). For values that must survive a
131
+ reset or be read as MCP resources: `save_result`/`load_result` for
132
+ JSON-shaped data, `save_artifact`/`load_artifact` for bytes (CSVs,
133
+ images, parquet) — on Claude Code, a saved artifact materializes as a
134
+ real local file.
135
+ - **Errors written for agents, not log files.** The real exception type and
136
+ message on every backend, catchable by builtin type (`except ValueError`
137
+ for store failures, `except PermissionError` for CLI policy,
138
+ `except LookupError` for unknown capabilities) — identically on all three
139
+ backends.
140
+
141
+ ## Use it as a library
142
+
143
+ The same runtime embeds directly in Python — register your own functions and
144
+ execute agent-written snippets against them, sandboxed, with no server in
145
+ between:
146
+
147
+ ```python
148
+ from toolplane import Toolplane
149
+
150
+ runtime = Toolplane()
151
+
152
+ @runtime.tool(tags={"math"})
153
+ def add(x: int, y: int) -> int:
154
+ """Add two numbers."""
155
+ return x + y
156
+
157
+ result = await runtime.execute("""
158
+ value = await call_tool("add", {"x": 2, "y": 3})
159
+ return value
160
+ """)
161
+ ```
162
+
163
+ And if you already have an agent framework, `as_tool()` packages the whole
164
+ runtime as one `run_code` tool — a plain async function with a generated
165
+ description (compact enough for OpenAI's tool-description cap) that
166
+ pydantic-ai, the OpenAI Agents SDK, and LangChain/LangGraph all accept
167
+ directly. It is sandboxed monty by default: model-authored code never
168
+ inherits the `local_unsafe` dev backend implicitly — that requires an
169
+ explicit `backend="local_unsafe"` opt-in:
170
+
171
+ ```python
172
+ from pydantic_ai import Agent
173
+
174
+ agent = Agent("anthropic:claude-sonnet-5", tools=[runtime.as_tool()])
175
+ ```
176
+
177
+ MCP servers, explicit CLI wrappers, and scoped Python namespaces register the
178
+ same way — see the [library guide](https://oneryalcin.github.io/toolplane/)
179
+ and [examples](examples/README.md) for the full surface, including
180
+ config-driven setup (`Toolplane.from_config`), the result/artifact stores,
181
+ and runnable `as_tool` examples for all three frameworks.
182
+
183
+ ## Backends
184
+
185
+ | Backend | Use | Status |
186
+ | --- | --- | --- |
187
+ | `monty` | Default. Sandboxed by construction (no filesystem/network), pure pip install. Flat callable namespace — no classes, no third-party imports. | Active |
188
+ | `pyodide-deno` | Opt-in for package-capable snippets (pandas/NumPy-style) via Pyodide in Deno. | Supported, feature-frozen |
189
+ | `local_unsafe` | In-process execution for development only. | Dev only |
190
+
191
+ A container backend (real CPython, arbitrary packages) lands when a concrete
192
+ use case needs it. See [Code Mode Backends](docs/code-mode-backends.md) for
193
+ the design record.
194
+
195
+ ## Design goals
196
+
197
+ - Normalize heterogeneous tools into a Python-first API surface; JSON is the
198
+ wire format, not the programming model.
199
+ - Keep the exposed runtime curated rather than ambiently powerful; the host
200
+ decides which capabilities exist and what boundaries apply.
201
+ - Durable policy lives in config; session policy (escalation grants) dies
202
+ with the process.
203
+ - Canonical capability ids are qualified (`mcp:server/tool`); friendly
204
+ aliases exist only when unambiguous.
205
+ - Every slice earns its place through behavior, tests, or live certification.
206
+
207
+ ## Development
208
+
209
+ ```bash
210
+ make test
211
+ make examples
212
+ make ci
213
+ make publish-check
214
+ ```
215
+
216
+ Docs: `make docs` / `make docs-serve`. Publishing:
217
+ `PYPI_TOKEN=... make publish` — see the
218
+ [release checklist](docs/development/release-checklist.md).
219
+
220
+ See [ROADMAP.md](ROADMAP.md) for sequencing and
221
+ [Architecture](docs/architecture.md) for code organization.
@@ -12,7 +12,7 @@ local_unsafe slice
12
12
  -> MCP adapter
13
13
  -> config-driven runtime setup
14
14
  -> Toolplane MCP facade
15
- -> Docker/Modal remote backends
15
+ -> container backend (when a named user needs arbitrary CPython packages)
16
16
  ```
17
17
 
18
18
  The goal is to test the core architecture before building a broad framework
@@ -22,10 +22,12 @@ around it.
22
22
 
23
23
  ### Execution Backends
24
24
 
25
- - Pyodide+Deno backend for package-capable sandboxed snippets.
26
- - Docker backend for real CPython, local CLI binaries, and system dependencies.
27
- - Modal backend for remote package-heavy or longer-running jobs.
28
- - E2B and Blaxel once the callback bridge and session model are stable.
25
+ - Pyodide+Deno backend for package-capable sandboxed snippets — shipped and
26
+ feature-frozen: supported as-is, no further edge investment; its use case
27
+ is served by a container backend when a real user needs it.
28
+ - One container backend (real CPython, arbitrary packages) when a named user
29
+ asks. Modal/E2B/Blaxel-style remote backends only after that contract is
30
+ proven — not before.
29
31
 
30
32
  ### Capability Adapters
31
33
 
@@ -0,0 +1,90 @@
1
+ # Agents Don't Find Resources
2
+
3
+ A design lesson from building toolplane's discovery surface, verified live
4
+ against real agents. It generalizes to any MCP server: **exposing a
5
+ capability is not the same as an agent discovering it.** If you ship a
6
+ resource, a template, or a clever affordance and don't put a plain-text
7
+ pointer to it in a channel the agent already reads, you shipped nothing.
8
+
9
+ ## The experiment that settled it
10
+
11
+ While building toolplane's namespace manifest (a live MCP resource
12
+ describing every binding an `execute_code` snippet can use), we ran cold
13
+ sessions: a fresh Claude Code agent, the toolplane server connected, a task
14
+ that the manifest would make easy. The agent had the resource-listing tool
15
+ available from turn one.
16
+
17
+ It never called it. Asked directly afterward — *"if we hadn't told you the
18
+ server exposes resources, would you have discovered them?"* — the agent's
19
+ answer was no. It guessed capability names, misread error text, and
20
+ retried, but at no point did "enumerate the server's resources" enter its
21
+ loop. The capability was present; the behavior wasn't.
22
+
23
+ That single observation reshaped the whole design: **the resource is the
24
+ payload; the pointer is the feature.**
25
+
26
+ ## The three channels agents actually read
27
+
28
+ Discovery works only through text that arrives in the agent's context in
29
+ the normal course of work:
30
+
31
+ 1. **Tool descriptions and server instructions** — read once, at session
32
+ start. This is where standing directives live: toolplane's
33
+ `execute_code` description says *"Read the toolplane://namespace
34
+ resource BEFORE writing code — guessing shapes fails."* Cold agents
35
+ follow it.
36
+ 2. **Tool response text on the happy path** — read at the moment of need.
37
+ When a toolplane snippet saves an artifact, the response carries the
38
+ handle *and* its resource URI; the agent never has to know that
39
+ artifacts are enumerable, because the one it needs is already named.
40
+ 3. **Error text on the failure path** — the strongest channel, because a
41
+ failing agent is a searching agent. Every dead end must emit a
42
+ signpost: toolplane's no-match search reports how many capabilities
43
+ exist and how to list them; a policy refusal names the allowed
44
+ binaries; a wrong call shape shows the right one, verbatim, ready to
45
+ paste.
46
+
47
+ Listings, templates, and documentation pages are not channels. Claude Code
48
+ doesn't even show templated resource URIs in its listing (see the
49
+ [capability matrix](mcp-client-capability-matrix.md)) — but even where
50
+ listings work, agents don't browse; they act, fail, and read the error.
51
+
52
+ ## The corollary we learned the hard way: signposts can dangle
53
+
54
+ The doctrine has a failure mode of its own, found when toolplane grew an
55
+ embedded mode (`runtime.as_tool()` — the same runtime as a single tool
56
+ inside pydantic-ai / OpenAI Agents SDK / LangGraph, no MCP anywhere).
57
+
58
+ A live agent run misremembered a capability name. The error helpfully
59
+ said: *search capabilities with an empty query, or read the
60
+ toolplane://namespace resource.* Neither exists in embedded mode — there
61
+ is no search tool and no resource protocol. The agent obediently tried to
62
+ "search" through the only tool it had, hit a second dead end, and gave up
63
+ on a task it was fully equipped to complete.
64
+
65
+ So the refined rule: **a signpost must only point at affordances that
66
+ exist in the reader's context — or carry the answer inline.** The fix was
67
+ to make the dead ends self-contained: the unknown-capability error now
68
+ lists the registered names itself; a miss on a CLI binary's name teaches
69
+ the correct call shape in the message; the tool description shows a real
70
+ canonical id instead of a `"canonical:name"` placeholder (which one model
71
+ read literally, inventing `canonical:git`). After the change, the same
72
+ agent completed the same task — the signposts walked it there.
73
+
74
+ ## Design rules, compressed
75
+
76
+ - Every surface ships with its pointer, in a channel from the list above.
77
+ A resource without a pointer is dark matter.
78
+ - Every dead end teaches the next move. If an error can be hit by a wrong
79
+ guess, the message must contain the right guess.
80
+ - Signposts name only what exists *here* — same server, same mode, same
81
+ policy — or include the answer inline.
82
+ - Deliver concrete URIs in results; never rely on the agent enumerating.
83
+ - Test discovery the only way that counts: a cold agent, a real task, no
84
+ hints. If it needs a human to say "there's a resource for that," the
85
+ pointer mesh has a hole.
86
+
87
+ Everything above was verified against live agents (Claude Code cold
88
+ sessions for the MCP mode; claude-sonnet-5, an OpenAI model, and a
89
+ LangGraph ReAct agent for embedded mode) — the failures described are
90
+ transcripts, not thought experiments.
@@ -22,7 +22,10 @@ Name the real boundaries early so the code does not collapse into glue:
22
22
  - `Schema`: how the agent learns to call something.
23
23
  - `Backend`: where code runs.
24
24
  - `Bridge`: how sandboxed code calls host capabilities.
25
- - `Session`: execution state across snippets.
25
+ - `Session`: execution state across snippets. Realized inside the monty
26
+ backend (persistent namespace, on by default — see the
27
+ [session spike](monty-session-spike.md) and `[session]` config) rather
28
+ than as a separate layer: the interpreter that holds the state owns it.
26
29
 
27
30
  The main rule:
28
31
 
@@ -181,7 +184,8 @@ src/toolplane/
181
184
  render.py # brief/detailed/full schema rendering
182
185
  json_schema.py # introspection helpers
183
186
 
184
- sessions.py # execution sessions and state
187
+ # (no sessions.py: session state lives in backends/monty.py — the
188
+ # interpreter that holds the namespace owns it)
185
189
  errors.py
186
190
  ```
187
191
 
@@ -10,9 +10,13 @@ discover capabilities -> inspect schemas -> execute Python against a curated nam
10
10
 
11
11
  Monty is the default backend: safe tool orchestration with no external
12
12
  dependencies (see the [decision record](monty-default-spike.md)).
13
- Pyodide+Deno is the opt-in sandbox for pandas/NumPy-style snippets.
14
- Docker, Modal, E2B, and Blaxel are for arbitrary CPython packages,
15
- subprocesses, native dependencies, GPUs, or stronger remote isolation.
13
+ Pyodide+Deno is the opt-in sandbox for pandas/NumPy-style snippets —
14
+ shipped and **feature-frozen** as of 2026-07: supported as-is, but no
15
+ further investment in its edges; workloads beyond it are served by a
16
+ container backend when a concrete user needs one. Docker/Modal/E2B-class
17
+ backends (arbitrary CPython packages, subprocesses, native dependencies,
18
+ GPUs, remote isolation) stay design-only until then; the material below
19
+ about them is a design record, not a commitment.
16
20
 
17
21
  The execution backend should be swappable. Different workloads need different
18
22
  tradeoffs between safety, package support, startup latency, local development