Skip to content

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:

toolplane.search_capabilities
toolplane.get_capability_schemas
toolplane.execute_code

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:

[mcp.servers.linear]
url = "https://mcp.linear.app/mcp"

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:

toolplane mcp add linear --command uvx --arg fastmcp-remote --arg https://mcp.linear.app/mcp

which maps to:

[mcp.servers.linear]
command = "uvx"
args = ["fastmcp-remote", "https://mcp.linear.app/mcp"]

(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:

toolplane mcp login linear-bridge --config ./toolplane.toml

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:

toolplane mcp status --config ./toolplane.toml

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:

codex mcp add toolplane -- toolplane serve mcp --config ./toolplane.toml

or, for Claude Code:

claude mcp add toolplane -- toolplane serve mcp --config ./toolplane.toml

A later Claude plugin can make this lower friction:

/plugin install toolplane@...

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:

toolplane serve mcp --config ./toolplane.toml

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:

toolplane serve mcp --config ./toolplane.toml --unsafe

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:

search_capabilities -> get_capability_schemas -> execute_code

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:

[mcp.servers.linear]
url = "https://mcp.linear.app/mcp"

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:

toolplane mcp login linear
toolplane mcp status
toolplane mcp logout linear

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), and fastmcp-remote bridges own their own cache. Toolplane itself contains no token-handling code and rolls no crypto.
  • toolplane.toml should describe upstream MCP servers and policy, not contain long-lived secrets — reference them as keyring://<name> or env://<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:

list_capabilities(tags?)
explain_policy()

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, and execute tools.
  • 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's external_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 of 12.
  • 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 ExecutionResult object 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:

[cli]
mode = "disabled"

or:

[cli]
mode = "allowlist"
allow = ["git", "gh", "rg"]

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.