Packages

fnord

0.9.39

AI code archaeology

Current section

Files

Jump to
fnord lib mcp client default.ex
Raw

lib/mcp/client/default.ex

defmodule MCP.Client.Default do
@moduledoc """
Real implementation of `MCP.Client`: talks to the Hermes client GenServers
registered by `MCP.Supervisor`. All the defensive plumbing for a runtime we
do not control lives here - registration/aliveness checks before every
call, rescue/catch around Hermes invocations, and response unwrapping.
"""
@behaviour MCP.Client
alias MCP.Supervisor, as: MCPSup
@impl MCP.Client
def start_supervisor() do
# The supervisor is VM-global: any process (the CLI phase, an agent, a
# tool call) may be first to want MCP, but the stack must survive its
# initiator. start_link links to the caller, so unlink immediately.
case Process.whereis(MCPSup) do
nil ->
case MCPSup.start_link([]) do
{:ok, pid} ->
Process.unlink(pid)
:ok
{:error, {:already_started, pid}} ->
Process.unlink(pid)
:ok
{:error, reason} ->
{:error, reason}
end
_pid ->
:ok
end
end
@impl MCP.Client
def connected?(server) do
case Process.whereis(MCPSup.instance_name(server)) do
nil -> false
pid -> Process.alive?(pid)
end
end
@impl MCP.Client
def list_tools(server) do
with_connected_client(server, fn client ->
# Give the server a moment to complete initialization and handshake
:timer.sleep(1000)
case Hermes.Client.Base.list_tools(client) do
{:ok, %Hermes.MCP.Response{result: %{"tools" => tools}}} -> {:ok, tools}
{:ok, _response} -> {:ok, []}
{:error, reason} -> {:error, reason}
end
end)
end
@impl MCP.Client
def get_server_capabilities(server) do
with_connected_client(server, fn client ->
case Hermes.Client.Base.get_server_capabilities(client) do
caps when is_map(caps) -> {:ok, caps}
nil -> {:ok, %{}}
end
end)
end
@impl MCP.Client
def call_tool(server, tool, args, opts) do
client = MCPSup.instance_name(server)
try do
case Hermes.Client.Base.call_tool(client, tool, args, opts) do
{:ok, %Hermes.MCP.Response{result: result}} -> {:ok, result}
{:error, reason} -> {:error, reason}
end
catch
:exit, _ -> {:error, "MCP client not available"}
end
end
# Runs `fun` against the server's client atom only when the process is
# registered and alive; shields the caller from Hermes raising or exiting.
defp with_connected_client(server, fun) do
client = MCPSup.instance_name(server)
case Process.whereis(client) do
nil ->
{:error, :not_started}
pid when is_pid(pid) ->
if Process.alive?(pid) do
try do
fun.(client)
rescue
error -> {:error, {:rescue, error}}
catch
:exit, reason -> {:error, {:exit, reason}}
end
else
{:error, :not_alive}
end
end
end
end