Current section
Files
Jump to
Current section
Files
lib/ex_claw/mcp/manager.ex
defmodule ExClaw.MCP.Manager do
@moduledoc """
Manages MCP client connections. Auto-connects to servers declared in config
and skills. Provides a unified interface to discover tools from all connected servers.
"""
use GenServer
require Logger
alias ExClaw.MCP.Client
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc "Connect to an MCP server. Starts the client, initializes, and discovers tools."
@spec connect(map()) :: {:ok, [map()]} | {:error, term()}
def connect(server_config) do
GenServer.call(__MODULE__, {:connect, server_config}, 30_000)
end
@doc "List all tools from all connected MCP servers."
@spec all_tools() :: [map()]
def all_tools do
GenServer.call(__MODULE__, :all_tools)
end
@doc "List connected server names."
@spec connected_servers() :: [String.t()]
def connected_servers do
GenServer.call(__MODULE__, :connected_servers)
end
@doc "Call a tool on a specific server."
@spec call_tool(String.t(), String.t(), map()) :: {:ok, term()} | {:error, term()}
def call_tool(server_name, tool_name, arguments \\ %{}) do
Client.call_tool(server_name, tool_name, arguments)
end
# --- GenServer ---
@impl true
def init(_opts) do
{:ok, %{servers: %{}}}
end
@impl true
def handle_call({:connect, config}, _from, state) do
name = config["name"] || config[:name]
case Client.start_link(config) do
{:ok, _pid} ->
case Client.initialize(name) do
{:ok, _caps} ->
case Client.list_tools(name) do
{:ok, tools} ->
Logger.info("[MCP:Manager] Connected to #{name}, #{length(tools)} tools available")
state = put_in(state, [:servers, name], %{config: config, tools: tools})
{:reply, {:ok, tools}, state}
{:error, reason} ->
Logger.warning("[MCP:Manager] Failed to list tools from #{name}: #{inspect(reason)}")
{:reply, {:error, reason}, state}
end
{:error, reason} ->
Logger.warning("[MCP:Manager] Failed to initialize #{name}: #{inspect(reason)}")
{:reply, {:error, reason}, state}
end
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
def handle_call(:all_tools, _from, state) do
tools =
state.servers
|> Enum.flat_map(fn {server_name, %{tools: tools}} ->
Enum.map(tools, &Map.put(&1, "server", server_name))
end)
{:reply, tools, state}
end
def handle_call(:connected_servers, _from, state) do
{:reply, Map.keys(state.servers), state}
end
end