Starts the MCP servers from mcp_config.json and exposes their tools to the agent.
The file is the one that super-pi mcp add|remove|enable|disable edits (in the global
config dir). Each enabled server is started once under SuperPi.MCPClientSupervisor.
Its tools join the tool catalog as mcp__<server>__<tool>, so the model sees which server
a tool belongs to and two servers cannot collide. SuperPi.Tools.execute/3 routes those
names to call_tool/3.
Tool calls pass the same before_tool pipeline as built-in tools, so the permission
policy applies. Every MCP tool is registered with an "effects" list, which is what the
policy decides by (SuperPi.Plugins.Permissions): ["net_read"] when the server annotates
the tool readOnlyHint: true, else ["net_write"]. A read-only tool runs without a prompt
under accept-edits and in plan mode. Any other tool needs auto mode, a prompt answer,
permissions.allowTools, or dangerouslySkipPermissions. The network policy (deny,
allowlist) applies to every MCP tool, a local stdio one included, because the server may
do anything.
Configuration
config :super_pi,
mcp_clients: true, # start servers when a conversation starts
mcp_tool_exposure: :direct, # or :deferred (found through `tool_search`)
mcp_startup_timeout_ms: 10_000,
mcp_call_timeout_ms: 60_000Server entries
Each entry under "mcpServers" is one of two shapes. Both accept "disabled": true.
A local process, over stdio:
{"command": "npx", "args": ["-y", "some-server"], "env": {"TOKEN": "..."}}A hosted server, over Streamable HTTP (url, or serverUrl as other hosts write it):
{"url": "https://mcp.example.com/mcp",
"headers": {"Authorization": "Bearer ${EXAMPLE_TOKEN}"}}${NAME} in a header value is replaced from the environment at start-up. A name that is not
set fails that server with {:missing_env, names}. OAuth is not supported. See
SuperPi.MCP.Transport.StreamableHTTP for the session and fallback behaviour. An entry with
both command and url runs the command.
Scopes
A server belongs to a scope. The servers from mcp_config.json are :global: every
conversation sees their tools. The servers that a client hands over at run time
(add_servers/2) belong to one conversation. Their tools stay out of the shared catalog, only
that conversation declares them, and only a call from that conversation reaches them. Two
conversations that hand over a server with the same name get two processes, each with its own
env and headers.
Summary
Functions
Starts MCP servers that a client hands over at run time, in the ACP mcpServers shape
Runs an MCP tool by its exposed name and returns a SuperPi.Tools result map. scope is the
calling conversation: its own servers come first, then the :global ones. A tool of another
conversation's server is not available.
An unknown name, a stopped server, or a protocol error is an is_error result, never a raise.
Adds the MCP tools that conversation_id may use, and does not declare yet, to its agent
document: the :global tools that the catalog shows the model, and the tools of its own servers.
Starts every enabled stdio server that is not running yet and registers its tools.
Returns [%{server: name, status: :started | :running | {:error, reason}, tools: count}].
Servers that fail to start are logged and skipped. The call is safe to repeat.
The exposed tool name for a server and tool: mcp__<server>__<tool>, characters limited to [A-Za-z0-9_-].
Stops every MCP server and removes their tools from the catalog.
Stops the servers of one conversation and forgets their tools. The tools stay declared in its agent document, so a call fails as "not available" until the client hands the servers over again.
Names of the tools that the servers of scope provide (mcp__<server>__<tool>).
Functions
Starts MCP servers that a client hands over at run time, in the ACP mcpServers shape:
- stdio:
%{"name", "command", "args", "env" => [%{"name", "value"}]} HTTP:
%{"type" => "http" | "sse", "name", "url", "headers" => [%{"name", "value"}]}
The servers belong to conversation_id (see "Scopes"), and their tools are declared in its
agent document. A server that is already running for that conversation is left alone, and one
that fails to start is logged and skipped. Returns the same list as ensure_started/1.
Runs an MCP tool by its exposed name and returns a SuperPi.Tools result map. scope is the
calling conversation: its own servers come first, then the :global ones. A tool of another
conversation's server is not available.
An unknown name, a stopped server, or a protocol error is an is_error result, never a raise.
@spec declare_tools(term()) :: :ok
Adds the MCP tools that conversation_id may use, and does not declare yet, to its agent
document: the :global tools that the catalog shows the model, and the tools of its own servers.
Starts every enabled stdio server that is not running yet and registers its tools.
Returns [%{server: name, status: :started | :running | {:error, reason}, tools: count}].
Servers that fail to start are logged and skipped. The call is safe to repeat.
The exposed tool name for a server and tool: mcp__<server>__<tool>, characters limited to [A-Za-z0-9_-].
Stops every MCP server and removes their tools from the catalog.
@spec stop_scope(term()) :: :ok
Stops the servers of one conversation and forgets their tools. The tools stay declared in its agent document, so a call fails as "not available" until the client hands the servers over again.
Names of the tools that the servers of scope provide (mcp__<server>__<tool>).