SuperPi.MCP.Manager (super_pi v0.1.0)

Copy Markdown View Source

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_000

Server 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

add_servers(conversation_id, servers)

@spec add_servers(term(), [map()]) :: [map()]

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.

call_tool(exposed_name, args, scope \\ :global)

@spec call_tool(String.t(), map(), term()) :: map()

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.

declare_tools(conversation_id)

@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.

ensure_started(opts \\ [])

@spec ensure_started(keyword()) :: [map()]

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.

exposed_name(server, tool)

The exposed tool name for a server and tool: mcp__<server>__<tool>, characters limited to [A-Za-z0-9_-].

stop_all()

Stops every MCP server and removes their tools from the catalog.

stop_scope(scope)

@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.

tool_names(scope \\ :global)

Names of the tools that the servers of scope provide (mcp__<server>__<tool>).