Toolplane And MCP¶
Toolplane is a programmable workbench over tools. MCP is one important source of those tools, but Toolplane should not become only an MCP proxy.
Implementation tracking issue: #16.
The durable product boundary is:
toolplane
runtime, registry, config, policy, auth wiring, and execution backends
toolplane-mcp
optional MCP facade that lets Claude Code, Codex, Cursor, and other MCP
clients use a configured Toolplane runtime
Product Definition¶
MCP gives agents tools. Toolplane gives agents a Python workbench over tools.
That distinction matters when a workflow needs code-shaped composition:
issues = await linear.list_issues(query="label:bug")
docs = await context7.query_docs("FastMCP OAuth")
diff = await git.diff(name_only=True, _=["HEAD~1", "HEAD"]).lines()
return {
"issue_count": len(issues),
"docs_prefix": docs[:500],
"changed_files": diff,
}
The agent writes normal Python. The host controls which capabilities exist, how credentials are acquired, which CLIs are available, and which backend executes the code.
Why This Is Not Just MCP Code Mode¶
FastMCP Code Mode is a useful reference point. It wraps one MCP server catalog with discovery and execution meta-tools.
If a user has ten MCP servers and each server enables its own code mode, the client still sees ten separate code-mode islands:
linear.search
linear.get_schema
linear.execute
github.search
github.get_schema
github.execute
context7.search
context7.get_schema
context7.execute
Toolplane's target shape is one workbench over a unified capability registry:
Inside execute_code, the namespace can contain capabilities from MCP servers,
CLI wrappers, Python functions, host helpers, and Python packages.
issues = await linear.list_issues(query="assignee:me")
repo_status = await git.status(short=True).text()
table = pandas.DataFrame(issues)
return table[["identifier", "title"]].head(10).to_dict("records")
That is the product value: not "one MCP server to call other MCP servers", but a controlled Python runtime where multiple capability sources become composable.
User Flow¶
The first stable command-line surface should optimize for explicit setup. This
section is the target lifecycle, and all of it is implemented: toolplane
init writes a safe starter config, toolplane cli allow switches to
allowlist CLI policy, toolplane mcp add edits the project config with
comment-preserving TOML writes, toolplane mcp login primes a server
interactively, toolplane mcp list/status inspect configured servers,
toolplane config check/doctor validate the config and local environment,
toolplane run executes a snippet file, and toolplane serve mcp serves the
configured facade.
toolplane init
toolplane mcp add linear --url https://mcp.linear.app/mcp
toolplane mcp login linear
toolplane cli allow git gh rg
toolplane doctor
The current mcp add command writes a server block to project config:
It preserves existing comments and formatting with tomlkit, errors if the
server already exists, and requires --force to replace an existing server.
Use --print to emit the TOML snippet to stdout instead of editing the file.
A direct remote URL with auth = "oauth" gets persistent login: Toolplane
wires FastMCP's own OAuth helper to an encrypted token store
(~/.toolplane/oauth, Fernet at rest via FastMCP's storage wrappers) whose
key lives in the OS keyring (TOOLPLANE_STORAGE_KEY overrides it on
headless hosts). The boundary this preserves: Toolplane writes no
token-handling code and rolls no crypto — the flow, refresh, persistence,
and encryption are all FastMCP's; Toolplane only configures where they
happen and holds the key material in the platform keyring. Losing the key
is self-healing: encrypted tokens read as missing and the next login
re-prompts once.
Toolplane also accepts stdio-style upstream server definitions, including bridges used by stdio-only hosts:
which maps to:
(fastmcp-remote is FastMCP's own bridge and keeps its token cache under
~/.fastmcp/remote; the npm mcp-remote shape works identically.)
The current mcp login command primes one configured server interactively by
connecting with the exact configured command, args, and env, with the browser
allowed and a long default timeout:
For a fastmcp-remote bridge this triggers the bridge's own OAuth flow on
first connect and persists its tokens in the bridge's cache. For a direct
url + auth = "oauth" server, login runs FastMCP's OAuth flow against
Toolplane's encrypted token store — one browser consent, and every later
serve mcp and execute_code call reuses (and silently refreshes) the
stored tokens without a browser. (mcp status stays credential-free by
design; on a primed server it reports auth_required with a note that
the saved login exists and serve/execute will use it.)
The current mcp status command reads the project config and reports each
configured MCP server as data:
Status probes do not construct FastMCP OAuth providers, so they do not open a
browser or write OAuth tokens. A protected remote server is reported as
auth_required or error instead — for a direct url + auth = "oauth"
server the auth_required detail names the fix
(prime it once with: toolplane mcp login <name>). Stdio servers are
checked by executing the configured command and listing tools, so status
can surface child-process diagnostics from the configured server. For stdio
probes, Toolplane preserves the current process environment plus configured
server env, overrides BROWSER so probes cannot open a browser, and forces
one-shot subprocess teardown.
Then a user can connect Toolplane to an MCP client:
or, for Claude Code:
A later Claude plugin can make this lower friction:
The plugin is distribution sugar. The core product remains the configured Toolplane runtime.
Walking Skeleton¶
Build toolplane serve mcp before the full auth lifecycle. The facade is the
highest-information slice: it proves whether clients can use Toolplane as one
MCP server that offers progressive discovery and code execution over a curated
namespace.
The current implementation provides this skeleton, exposes only the three
Toolplane meta-tools, and guards the config-backed MCP facade from unsafe
defaults. On successful toolplane serve mcp startup, it prints the effective
backend, CLI, MCP-server, and unsafe-override policy to stderr for the operator.
Remote OAuth is delegated to FastMCP: direct auth = "oauth" servers use
FastMCP's OAuth helper with Toolplane-configured encrypted storage, and
fastmcp-remote bridges own their own cache — either way Toolplane never
grows its own credential store or token-handling code. Client install
helpers are not built.
A fresh config serves safely with no flags, because the defaults are the
sandboxed monty backend and disabled CLI policy:
On the default monty backend, agent code calls capabilities through flat
aliases or call_tool (await linear_list_issues(...),
await call_tool("mcp.linear.list_issues", {...})). The scoped
linear.list_issues(...) sugar shown in the product examples above requires
the local_unsafe or pyodide-deno backends — see the
backends page and the
Monty decision record.
Across execute_code calls within one served process, snippets can hand data
forward without routing it through the model's context:
await save_result(value, label=...) returns a handle,
await load_result(handle) retrieves it in a later run. See the
result store design record and the
configuration page.
For trusted local development with the local_unsafe backend or ambient CLI
policy, the operator must opt in explicitly:
Validate it against a config with only no-auth capabilities, such as host Python helpers, allowlisted CLI binaries, and a local stdio MCP server. Then connect an MCP client and run:
This skeleton was the early risk-reduction step before the OAuth lifecycle
work. That work resolved as bridge delegation (toolplane mcp login priming
a fastmcp-remote bridge that owns its own token cache), not as
Toolplane-owned storage.
Auth Boundary¶
Remote MCP authentication belongs to the host process, not to agent-written Python.
For remote MCP servers, adding a server and authenticating to it should remain separate operations. The add command records how to reach the server. The login command discovers or negotiates the required authentication flow and stores credentials outside project TOML.
For a remote server:
Toolplane delegates both the MCP OAuth flow and its token persistence to the
FastMCP client layer: a fastmcp-remote stdio bridge performs the
browser-based authorization code flow with PKCE, handles dynamic client
registration and token refresh, and owns its own token cache. Toolplane's job
is the host command surface around that machinery — it never holds tokens
itself:
For non-interactive environments, secrets should be referenced, not stored in plain TOML:
[mcp.servers.linear]
url = "https://mcp.linear.app/mcp"
[mcp.servers.linear.auth]
type = "bearer"
env = "LINEAR_MCP_TOKEN"
Rules:
- Agent code never receives raw OAuth tokens, refresh tokens, or API keys.
- Toolplane does not silently borrow Claude Code or Codex's private MCP auth sessions.
- Headless execution requires pre-login (
toolplane mcp login <name>) or explicit secret-referenced bearer credentials. - Token persistence is delegated to FastMCP: direct
url+auth = "oauth"servers use FastMCP's OAuth helper with a Fernet-encrypted store that Toolplane configures (~/.toolplane/oauth, key in the OS keyring), andfastmcp-remotebridges own their own cache. Toolplane itself contains no token-handling code and rolls no crypto. toolplane.tomlshould describe upstream MCP servers and policy, not contain long-lived secrets — reference them askeyring://<name>orenv://<VAR>instead.
What Toolplane-MCP Should Expose¶
toolplane-mcp currently exposes a small meta-tool surface:
search_capabilities(query, tags?)
get_capability_schemas(names, detail?)
execute_code(code, backend?, packages?)
Maybe later:
It should not re-export every underlying tool as a flat MCP catalog by default. That recreates context bloat and loses the workbench model.
FastMCP CodeMode Decision¶
FastMCP's experimental CodeMode transform already provides the same broad
shape as the Toolplane facade: staged discovery, schema lookup, and code
execution through a sandbox provider.
A throwaway spike showed this is technically viable:
- CodeMode can own the client-visible
search,get_schema, andexecutetools. - Toolplane-backed capabilities can be adapted into the hidden FastMCP catalog.
- A custom sandbox provider can inject Toolplane-style scoped namespaces such as
demo.add(...)while delegating calls through CodeMode'sexternal_functions.
The spike also exposed the deciding semantic difference:
- Scalar tool results are wrapped as
{"result": value}inside CodeMode's intra-snippet execution path. This means an agent composing normal Python would receive{"result": 12}from a scalar tool call instead of12. - Toolplane's custom execution path preserves normal Python values inside the
snippet, which is central to the product contract. At the final MCP boundary,
Toolplane returns an explicit
ExecutionResultobject such as{"value": 12, ...}.
The product decision is therefore: keep Toolplane's custom execute/namespace core, and treat CodeMode as a parts bin for commodity pieces where it is clearly better:
- BM25-style search instead of Toolplane's current token-count baseline.
- Discovery-tool patterns.
- Execute-time tool-call caps.
Do not adopt CodeMode wholesale unless FastMCP makes scalar unwrapping configurable or Toolplane intentionally changes its Python-first composition semantics.
Remaining integration caveats:
- CodeMode's discovery and schema rendering are FastMCP-native, not Toolplane-native.
- Tool names exposed to CodeMode need safe FastMCP wrapper names, so canonical Toolplane ids and aliases need a stable mapping.
- CodeMode is currently documented as experimental, so depending on it makes FastMCP's transform API part of Toolplane's compatibility surface.
Non-Goals¶
Toolplane should not:
- become a full agent framework.
- pretend it can automatically access sibling MCP servers already configured in Claude Code, Codex, Cursor, or another client.
- become a general credential manager.
- mutate a user's Claude/Codex config without an explicit install command.
- expose ambient local Python and arbitrary CLI execution through MCP by default.
- implement OAuth itself when the MCP client library already owns the protocol.
Dependency Order¶
toolplane-mcp should front-load the facade skeleton once config and policy are
real, then harden auth before calling the public surface complete:
config-driven runtime setup
-> CLI policy: disabled, allowlist, ambient
-> MCP server config loading
-> minimal toolplane serve mcp walking skeleton without remote auth
-> FastMCP OAuth/token-storage behavior verification
-> MCP auth command surface (durable tokens delegated to the bridge)
-> client install helpers
-> Claude plugin packaging
The first public MCP facade should default to safe policy:
or:
ambient CLI mode is useful for local development, but it should be an explicit
project choice before Toolplane is exposed as an MCP server.
Design Test¶
A good Toolplane workflow should answer yes to all of these:
- Can the agent compose more than one capability source in one Python snippet?
- Can the user inspect what capabilities are available before execution?
- Can the host explain and enforce CLI, MCP, and backend policy?
- Can credentials stay outside the agent-visible namespace?
- Can the same configured runtime be used directly from Python and through MCP?
If the answer is no, the feature probably belongs in a narrower adapter or a later iteration.