Current section
Files
Jump to
Current section
Files
lib/hermes/server/supervisor.ex
defmodule Hermes.Server.Supervisor do
@moduledoc """
Supervisor for MCP server processes.
This supervisor manages the lifecycle of an MCP server, including:
- The Base server process that handles MCP protocol
- The transport layer (STDIO, StreamableHTTP, or SSE)
- Session supervisors for StreamableHTTP transport
The supervision strategy is `:one_for_all`, meaning if any child
process crashes, all processes are restarted to maintain consistency.
## Conditional Startup
The supervisor intelligently handles startup based on transport type:
- **STDIO transport**: Always starts
- **StreamableHTTP/SSE transport**: Conditional startup based on:
1. Explicit `:start` option in transport config (highest priority)
2. `HERMES_MCP_SERVER` environment variable present
3. `PHX_SERVER` environment variable present, for phoenix apps using releases
4. Phoenix `:serve_endpoints` internalr runtime config (usage with `mix phx.server`)
5. Default: `true` (starts by default in production releases)
This ensures MCP servers start correctly in all environments:
- Mix releases: Use `PHX_SERVER=true` or `HERMES_MCP_SERVER=true`
- Development: Use `mix phx.server`
- Tests/Tasks: Servers don't start unless explicitly configured
## Configuration Examples
```elixir
# Force start with transport option (highest priority)
{MyServer, transport: {:streamable_http, start: true}}
# Control via environment variables (works in releases)
PHX_SERVER=true ./my_app start
```
## Supervision Tree
For STDIO transport:
```
Supervisor
├── Base Server
└── STDIO Transport
```
For StreamableHTTP transport:
```
Supervisor
├── Session.Supervisor
├── Base Server
└── StreamableHTTP Transport
```
"""
use Supervisor, restart: :permanent
alias Hermes.Server.Base
alias Hermes.Server.Session
alias Hermes.Server.Transport.SSE
alias Hermes.Server.Transport.STDIO
alias Hermes.Server.Transport.StreamableHTTP
@type sse :: {:sse, keyword()}
@type stream_http :: {:streamable_http, keyword()}
@type transport :: :stdio | stream_http | sse | StubTransport
@type start_option :: {:transport, transport} | {:name, Supervisor.name()}
@doc """
Starts the server supervisor.
## Parameters
* `server` - The module implementing `Hermes.Server.Behaviour`
* `init_arg` - Argument passed to the server's `init/1` callback
* `opts` - Options including:
* `:transport` - Transport configuration (required)
* `:name` - Supervisor name (optional, defaults to registered name)
* `:registry` - The custom registry to use to manage processes names (defaults to `Hermes.Server.Registry`)
## Examples
# Start with STDIO transport
Hermes.Server.Supervisor.start_link(MyServer, [], transport: :stdio)
# Start with StreamableHTTP transport
Hermes.Server.Supervisor.start_link(MyServer, [],
transport: {:streamable_http, port: 8080}
)
"""
@spec start_link(server :: module, init_arg :: term, list(start_option)) :: Supervisor.on_start()
def start_link(server, init_arg, opts) when is_atom(server) and is_list(opts) do
registry = Keyword.get(opts, :registry, Hermes.Server.Registry)
name = Keyword.get(opts, :name, registry.supervisor(server))
opts = Keyword.merge(opts, module: server, init_arg: init_arg, registry: registry)
Supervisor.start_link(__MODULE__, opts, name: name)
end
@impl true
def init(opts) do
server = Keyword.fetch!(opts, :module)
transport = Keyword.fetch!(opts, :transport)
init_arg = Keyword.fetch!(opts, :init_arg)
registry = Keyword.fetch!(opts, :registry)
if should_start?(transport) do
{layer, transport_opts} = parse_transport_child(transport, server, registry)
server_name = registry.server(server)
server_transport = [layer: layer, name: transport_opts[:name]]
server_opts = [
module: server,
name: server_name,
transport: server_transport,
init_arg: init_arg,
registry: registry
]
children = [
{Session.Supervisor, server: server, registry: registry},
{Base, server_opts},
{layer, transport_opts}
]
Supervisor.init(children, strategy: :one_for_all)
else
:ignore
end
end
if Mix.env() == :test do
defp parse_transport_child(StubTransport = kind, server, registry) do
name = registry.transport(server, kind)
opts = [name: name, server: server, registry: registry]
{kind, opts}
end
end
defp parse_transport_child(:stdio, server, registry) do
name = registry.transport(server, :stdio)
opts = [name: name, server: server, registry: registry]
{STDIO, opts}
end
defp parse_transport_child({:streamable_http, opts}, server, registry) do
name = registry.transport(server, :streamable_http)
opts = Keyword.merge(opts, name: name, server: server, registry: registry)
{StreamableHTTP, opts}
end
defp parse_transport_child({:sse, opts}, server, registry) do
name = registry.transport(server, :sse)
opts = Keyword.merge(opts, name: name, server: server, registry: registry)
{SSE, opts}
end
if Mix.env() == :test do
defp should_start?(StubTransport), do: true
end
defp should_start?(:stdio), do: true
defp should_start?({transport, opts}) when transport in ~w(sse streamable_http)a do
start? = Keyword.get(opts, :start)
if is_nil(start?), do: http_server_running?(), else: start?
end
defp http_server_running? do
cond do
System.get_env("HERMES_MCP_SERVER") -> true
System.get_env("PHX_SERVER") -> true
true -> check_phoenix_config()
end
end
defp check_phoenix_config do
Application.get_env(:phoenix, :serve_endpoints, false)
end
end