defmodule Hermes.Client.Supervisor do @moduledoc """ Supervisor for MCP client processes. This module manages the lifecycle of both the MCP client and its associated transport process. It uses a `:one_for_all` strategy, meaning if either the client or transport crashes, both are restarted together to maintain consistency. The supervisor automatically: - Starts the appropriate transport based on configuration - Starts the client with a reference to the transport - Ensures proper initialization order (transport first, then client) - Handles process naming for both client and transport ## Process Naming - Client process: Uses the module name or custom `:name` option - Transport process: Named as `Module.concat(ClientName, "Transport")` ## Transport Configuration Supports all Hermes transport types: - `:stdio` - For command-line MCP servers - `:sse` - For Server-Sent Events transports - `:websocket` - For WebSocket connections - `:streamable_http` - For streaming HTTP transports """ use Supervisor alias Hermes.Client.Base alias Hermes.Transport.SSE alias Hermes.Transport.STDIO alias Hermes.Transport.StreamableHTTP alias Hermes.Transport.Websocket @type transport_config :: {:stdio, keyword()} | {:sse, keyword()} | {:websocket, keyword()} | {:streamable_http, keyword()} @doc """ Starts the client supervisor. ## Arguments * `client_module` - The client module using `Hermes.Client` * `opts` - Supervisor options including: * `:name` - Optional custom name for the client process * `:transport` - Transport configuration (required) * `:transport_name` - Optional custom name for the transport process * `:client_info` - Client identification info * `:capabilities` - Client capabilities map * `:protocol_version` - MCP protocol version ## Examples # Simple usage with module names Hermes.Client.Supervisor.start_link(MyApp.MCPClient, transport: {:stdio, command: "mcp", args: ["server"]}, client_info: %{"name" => "MyApp", "version" => "1.0.0"}, capabilities: %{"roots" => %{}}, protocol_version: "2024-11-05" ) # With custom names (e.g., for distributed systems) Hermes.Client.Supervisor.start_link(MyApp.MCPClient, name: {:via, Horde.Registry, {MyCluster, "client_1"}}, transport_name: {:via, Horde.Registry, {MyCluster, "transport_1"}}, transport: {:stdio, command: "mcp", args: ["server"]}, client_info: %{"name" => "MyApp", "version" => "1.0.0"}, capabilities: %{"roots" => %{}}, protocol_version: "2024-11-05" ) """ @spec start_link(module(), keyword()) :: Supervisor.on_start() def start_link(client_module, opts) do Supervisor.start_link(__MODULE__, Keyword.put(opts, :client_module, client_module)) end @impl true def init(opts) do client_module = Keyword.fetch!(opts, :client_module) transport = Keyword.fetch!(opts, :transport) client_info = Keyword.fetch!(opts, :client_info) capabilities = Keyword.fetch!(opts, :capabilities) protocol_version = Keyword.fetch!(opts, :protocol_version) client_name = opts[:name] || client_module transport_name = derive_transport_name(opts[:transport_name], client_name) {layer, transport_opts} = parse_transport_config(transport) client_transport = [layer: layer, name: transport_name] client_opts = [ transport: client_transport, client_info: client_info, capabilities: capabilities, protocol_version: protocol_version, name: client_name ] children = [ {Base, client_opts}, {layer, transport_opts ++ [name: transport_name, client: client_name]} ] Supervisor.init(children, strategy: :one_for_all) end defp derive_transport_name(transport, _client) when not is_nil(transport), do: transport defp derive_transport_name(nil, client) when is_atom(client) do Module.concat(client, "Transport") end defp derive_transport_name(_transport, _client) do raise ArgumentError, """ When using a non-atom client name (e.g., via tuple), you must provide an explicit :transport_name option. Example: name: {:via, Registry, {MyRegistry, "client"}}, transport_name: {:via, Registry, {MyRegistry, "transport"}} """ end defp parse_transport_config({:stdio, opts}), do: {STDIO, opts} defp parse_transport_config({:streamable_http, opts}), do: {StreamableHTTP, opts} defp parse_transport_config({:sse, opts}), do: {SSE, opts} defp parse_transport_config({:websocket, opts}), do: {Websocket, opts} end