MCP Lifecycle CLI Spike¶
Issue: #23
Date: 2026-07-01
FastMCP version inspected locally: 3.4.2
Summary¶
Toolplane should build the MCP lifecycle CLI as thin config and operator UX over FastMCP, not as a new OAuth or transport implementation.
The first implementation slice is emit-only:
prints:
# add this to your toolplane.toml:
[mcp.servers.linear]
url = "https://mcp.linear.app/mcp"
auth = "oauth"
# warning: direct OAuth tokens are ephemeral in Toolplane v1.
# use a fastmcp-remote bridge for persistent login.
It should not mutate toolplane.toml in v1. Python has tomllib for reading
TOML, but no stdlib TOML writer. In-place editing would force either tomlkit
for comment-preserving round trips or a worse alternative. Emit-only keeps v1
dependency-free and cannot destroy comments or formatting in a human-edited
config file.
FastMCP Findings¶
Client(..., auth="oauth")andOAuth(...)are already implemented. OAuth covers browser authorization, PKCE, callback server, token exchange, caching, and refresh. See FastMCP's OAuth Authentication docs andfastmcp.client.auth.oauth.OAuth.- OAuth is relevant only for HTTP-based transports. Stdio MCP servers should use command/env configuration instead. See FastMCP's OAuth Authentication and Client Transports docs.
OAuth(token_storage=...)accepts anAsyncKeyValuebackend. Without one, FastMCP uses in-memory token storage and warns that tokens are lost on restart. Source checked:fastmcp.client.auth.oauth.OAuth.- FastMCP's encrypted persistent storage example uses
DiskStorewrapped inFernetEncryptionWrapper. See FastMCP's OAuth token storage docs. - FastMCP config
auth = "oauth"is not enough for persistent Toolplane tokens:RemoteMCPServer.to_transport()passes the string to the transport, and the transport constructsOAuth(...)without atoken_storage. Toolplane must construct and injectOAuth(token_storage=...)later if it wants durable tokens for direct remote MCP connections. Sources checked:fastmcp.mcp_config.RemoteMCPServer,fastmcp.client.transports.http.StreamableHttpTransport, andfastmcp.client.transports.sse.SSETransport. fastmcp-remoteis already the stdio bridge for remote MCP hosts. It auto-enables OAuth for HTTPS, stores tokens under~/.fastmcp/remoteby default, supportsFASTMCP_REMOTE_CONFIG_DIR, and has--resourcefor token isolation. See FastMCP's fastmcp-remote docs.- FastMCP
MCPConfigis JSON-oriented for file writes. Itswrite_to_file()writes JSON, so it does not solve Toolplane's TOML editing problem. See FastMCP'smcp_configdocs and source checked:fastmcp.mcp_config.MCPConfig.write_to_file. - FastMCP multi-server clients prefix tool names by server. Toolplane's MCP
adapter already maps configured MCP tools into canonical ids, aliases, and
scoped namespaces, then normalizes FastMCP
.dataresults back to Python values forexecute_code. See FastMCP's Client, Calling Tools, and Client Transports docs. Toolplane source checked:src/toolplane/adapters/mcp.py.
Evidence Map¶
Official FastMCP docs read:
- OAuth Authentication
- fastmcp-remote
- Client Transports
- The FastMCP Client
- Calling Tools
- Client Commands
fastmcp.mcp_config
FastMCP source inspected from installed package fastmcp==3.4.2:
fastmcp.client.auth.oauth.OAuthfastmcp.client.auth.oauth.TokenStorageAdapterfastmcp.client.transports.http.StreamableHttpTransportfastmcp.client.transports.sse.SSETransportfastmcp.mcp_config.MCPConfigfastmcp.mcp_config.RemoteMCPServerfastmcp.mcp_config.StdioMCPServer
Toolplane source inspected:
src/toolplane/config.pysrc/toolplane/adapters/mcp.pytests/test_config.pytests/test_mcp_adapter.py
V1 Command Decisions¶
toolplane mcp add¶
The initial implementation was emit-only. The current command edits
toolplane.toml by default using tomlkit, preserving existing comments and
formatting. --print keeps the old self-documenting TOML output when a user
wants to paste the block manually.
Remote URL form:
Written config:
Stdio command form:
Written config:
If the server name already exists, mcp add errors and leaves the file
unchanged. Replacements require --force; silent overwrites are not allowed.
Name validation allows only [A-Za-z0-9_-]+. This keeps the unquoted TOML table
header safe and avoids dotted or quoted server keys.
--auth should be explicit. Do not silently infer OAuth from HTTPS in
Toolplane's direct config path because FastMCP direct config does not persist
tokens without a supplied token store. For stdio bridge snippets using
fastmcp-remote, FastMCP's bridge owns its own OAuth defaults and token store.
When mcp add emits direct url + auth = "oauth", include a warning comment
that the token is ephemeral and a fastmcp-remote bridge is the persistent v1
path.
When mcp add emits a fastmcp-remote bridge snippet, include a comment that
the bridge should be primed before relying on status or execute workflows. Do
not bake --auth-timeout into the persisted snippet: short timeouts are useful
for probes but bad for human login, and long timeouts are useful for login but
bad for status.
toolplane mcp status¶
Implemented after mcp add. Status is a deterministic connection check over
configured servers using FastMCP Client directly, not Toolplane.from_config.
It reads config, selects one or more servers, strips FastMCP auth from the
probe config, and lists tools under a per-server timeout.
Status must not open a browser or write OAuth tokens. That guarantee is
structural: the probe client is never constructed with FastMCP auth=....
Servers that require credentials are reported as auth_required or error in
the output.
Status reports the same warning for configured direct url + auth = "oauth"
servers. The warning is data in the status output, not stderr and not a non-zero
exit condition.
Exit codes reflect whether the status command itself ran. Bad arguments, unknown server names, or malformed config return non-zero. A reachable config with a down, timed-out, or auth-protected server returns zero and reports that server state as data.
Stdio status checks execute the configured command in order to list tools. That is the honest connection check, but it means status can run user-configured processes and those processes may write diagnostics to stderr.
For stdio probes, status passes an explicit subprocess environment built from
the current process environment plus the configured server env, then overrides
BROWSER with a non-opening command. This preserves required values such as
PATH and FASTMCP_REMOTE_CONFIG_DIR while preventing a status probe from
opening a browser. Status also forces keep_alive = false on the probe config
so the subprocess is not reused after the one-shot check.
toolplane mcp login/logout¶
Do not implement until Toolplane chooses token storage.
Direct remote login requires Toolplane to construct OAuth(token_storage=...)
instead of passing plain auth = "oauth" through TOML. The likely storage
choice is encrypted disk storage via the AsyncKeyValue interface. Keychain is
not needed for v1.
fastmcp-remote already covers a separate stdio bridge lifecycle and stores
its own tokens under ~/.fastmcp/remote; Toolplane should not duplicate that
unless it needs tighter integration later.
FastMCP Remote Bridge Spike¶
Date: 2026-07-01
Test the bridge path first because it is the delete-code hypothesis: if
fastmcp-remote works as a normal stdio MCP server, Toolplane may not need to
own OAuth token storage for remote servers in v1.
Upstream Behavior¶
FastMCP's fastmcp-remote docs state that HTTPS servers use OAuth by default
and that the first connection opens the browser-based OAuth flow when the server
requires authentication. They also document the relevant lifecycle controls:
--auth-timeoutbounds how long the bridge waits for the OAuth callback.--resourceisolates token storage for one remote identity.FASTMCP_REMOTE_CONFIG_DIRmoves the bridge token directory.--auth nonedisables OAuth for unauthenticated HTTP development servers.
The installed uvx fastmcp-remote --help output in this environment matches
those options.
No-Auth Bridge Proof¶
A local FastMCP HTTP server was bridged through stdio with:
[mcp.servers.bridge]
command = "uvx"
args = [
"fastmcp-remote",
"http://127.0.0.1:8791/mcp",
"--auth",
"none",
"--silent",
]
Observed results:
toolplane mcp status --config /tmp/toolplane-fastmcp-remote-bridge.toml --timeout 20
-> bridge: ok transport=stdio auth=none tools=1
Toolplane.from_config registered the bridged tool as mcp:bridge/add, and
execute_code successfully called it:
Result: 42.
Conclusion: from Toolplane's perspective, a fastmcp-remote bridge is ordinary
stdio MCP after it initializes.
Unprimed Auth Bridge Behavior¶
An unprimed Linear bridge was tested with an isolated token directory, disabled browser command, and short auth timeout:
[mcp.servers.linear_bridge]
command = "uvx"
args = [
"fastmcp-remote",
"https://mcp.linear.app/mcp",
"--auth-timeout",
"1",
"--resource",
"toolplane-spike",
"--silent",
]
[mcp.servers.linear_bridge.env]
FASTMCP_REMOTE_CONFIG_DIR = "/tmp/toolplane-fastmcp-remote-linear-empty"
BROWSER = "/usr/bin/false"
Observed result:
toolplane mcp status --config /tmp/toolplane-fastmcp-remote-linear-unprimed.toml --timeout 5
-> linear_bridge: error transport=stdio auth=none
detail=Client failed to connect: OAuth callback timed out after 1.0 seconds
Toolplane returned exit 0 because status ran successfully and reported the server failure as data. The bridge wrote FastMCP remote metadata into the isolated temp token directory during the failed OAuth attempt.
Do not put this unprimed behavior in automated tests: a real unprimed
fastmcp-remote HTTPS bridge can open a browser and otherwise waits up to the
configured auth timeout.
Primed Linear Bridge Proof¶
Linear was then primed once through the bridge using an isolated token directory:
[mcp.servers.linear_bridge]
command = "uvx"
args = [
"fastmcp-remote",
"https://mcp.linear.app/mcp",
"--auth-timeout",
"180",
"--resource",
"toolplane-linear-spike",
"--silent",
]
[mcp.servers.linear_bridge.env]
FASTMCP_REMOTE_CONFIG_DIR = "/tmp/toolplane-fastmcp-remote-linear-prime"
After browser consent, toolplane mcp status reported:
Toolplane.from_config registered the Linear tools, search("issue") returned
Linear capabilities such as mcp:linear_bridge/list_issues, and execute_code
successfully called the scoped namespace:
issues = await linear_bridge.list_issues(query="", limit=1)
return {
"type": type(issues).__name__,
"count": len(issues) if hasattr(issues, "__len__") else None,
}
Observed execution result:
The Linear tool returned a string payload, so the count is string length, not
issue count. The important product proof still holds: authenticated Linear
tools reached Toolplane's namespace and were callable inside execute_code.
Slice-2 Decision¶
V1 remote MCP auth uses the fastmcp-remote stdio bridge path. Toolplane will
not build direct OAuth(token_storage=...) storage for v1.
Rationale:
- Toolplane needs no OAuth protocol or token-storage code for the bridge path.
mcp add --command ... fastmcp-remote ...already emits the right config shape.mcp status,Toolplane.from_config, search, andexecute_codealready work once the bridge is primed.- Logout and token cleanup belong to
fastmcp-remote's token directory for this path.
The sharp edge is unprimed HTTPS bridges: spawning them can initiate OAuth. The operator contract should be explicit:
- Prime the bridge deliberately before relying on status or execute workflows.
- Use
--auth-timeoutto avoid long hangs. - Use
--resourceandFASTMCP_REMOTE_CONFIG_DIRwhen isolation matters.
Toolplane status reduces the unprimed sharp edge by neutralizing BROWSER for
stdio probes and applying its normal per-server timeout. It does not rewrite the
persisted command arguments and does not make unprimed bridges equivalent to a
real login; it only prevents a status check from opening a browser.
Direct Toolplane-managed OAuth is deferred. Revisit it only when a concrete
trigger appears, such as a remote MCP server that cannot work through
fastmcp-remote, a headless deployment that cannot rely on browser priming, or
a product requirement for Toolplane-owned logout, token encryption, or token
auditing semantics.
toolplane doctor¶
Doctor should reuse EffectivePolicy; it must not re-derive backend or CLI
policy. The policy dataclass exists so operator surfaces share one definition.
End-to-End Proof Target¶
The product proof is not "OAuth opens a browser." It is:
configured MCP server
-> FastMCP client connects
-> Toolplane registers remote tools
-> search_capabilities shows them
-> execute_code can call them through the Toolplane namespace
This has already been proven for local stdio MCP servers. The remaining proof is an authenticated remote or remote-like HTTP server. A live third-party OAuth run is optional for the spike; most facts are settled by FastMCP source and docs. When empirical auth proof is needed, prefer a local throwaway FastMCP auth server before depending on a Linear account.
mcp add Acceptance¶
- Edits
toolplane.tomlby default withtomlkit. - Preserves existing comments and formatting.
- Supports
--printfor self-documenting stdout snippets. - Rejects invalid server names before writing.
- Supports remote URL form with optional explicit
--auth oauth. - Supports stdio command form with repeated
--arg. - Errors if the server already exists.
- Requires
--forceto replace an existing server. - Never silently overwrites a user-authored config entry.
- Tests load the written config through
load_toolplane_config. - Tests feed
config.mcp.to_fastmcp_config()intofastmcp.mcp_config.MCPConfig.