Current section

Files

Jump to
hermes_mcp lib hermes server.ex
Raw

lib/hermes/server.ex

defmodule Hermes.Server do
@moduledoc """
High-level MCP server implementation.
This module provides the main API for implementing MCP (Model Context Protocol) servers.
It includes macros and functions to simplify server creation with standardized capabilities,
protocol version support, and supervision tree setup.
## Usage
defmodule MyServer do
use Hermes.Server,
name: "My MCP Server",
version: "1.0.0",
capabilities: [:tools, :resources, :logging]
@impl Hermes.Server.Behaviour
def init(_arg, frame) do
{:ok, frame}
end
@impl Hermes.Server.Behaviour
def handle_request(%{"method" => "tools/list"}, frame) do
{:reply, %{"tools" => []}, frame}
end
@impl Hermes.Server.Behaviour
def handle_notification(_notification, frame) do
{:noreply, frame}
end
end
## Server Capabilities
The following capabilities are supported:
- `:prompts` - Server can provide prompt templates
- `:tools` - Server can execute tools/functions
- `:resources` - Server can provide resources (files, data, etc.)
- `:logging` - Server supports log level configuration
Capabilities can be configured with options:
- `subscribe?: boolean` - Whether the capability supports subscriptions (resources only)
- `list_changed?: boolean` - Whether the capability emits list change notifications
## Protocol Versions
By default, servers support the following protocol versions:
- "2025-03-26" - Latest protocol version
- "2024-10-07" - Previous stable version
- "2024-05-11" - Legacy version for backward compatibility
"""
alias Hermes.Server.Component
alias Hermes.Server.ConfigurationError
alias Hermes.Server.Handlers
@server_capabilities ~w(prompts tools resources logging)a
@protocol_versions ~w(2025-03-26 2024-05-11 2024-10-07)
@doc """
Starts a server with its supervision tree.
## Examples
# Start with default options
Hermes.Server.start_link(MyServer, :ok, transport: :stdio)
# Start with custom name
Hermes.Server.start_link(MyServer, %{},
transport: :stdio,
name: {:local, :my_server}
)
"""
defdelegate start_link(mod, init_arg, opts), to: Hermes.Server.Supervisor
@doc """
Guard to check if a capability is valid.
## Examples
iex> is_server_capability(:tools)
true
iex> is_server_capability(:invalid)
false
"""
defguard is_server_capability(capability) when capability in @server_capabilities
@doc """
Guard to check if a capability is supported by the server.
## Examples
iex> capabilities = %{"tools" => %{}}
iex> is_supported_capability(capabilities, "tools")
true
"""
defguard is_supported_capability(capabilities, capability) when is_map_key(capabilities, capability)
@doc false
defmacro __using__(opts) do
module = __CALLER__.module
capabilities = Enum.reduce(opts[:capabilities] || [], %{}, &parse_capability/2)
protocol_versions = opts[:protocol_versions] || @protocol_versions
name = opts[:name]
version = opts[:version]
if is_nil(name) and is_nil(version) do
raise ConfigurationError, module: module, missing_key: :both
end
if is_nil(name), do: raise(ConfigurationError, module: module, missing_key: :name)
if is_nil(version), do: raise(ConfigurationError, module: module, missing_key: :version)
quote do
@behaviour Hermes.Server.Behaviour
import Hermes.Server, only: [component: 1, component: 2]
import Hermes.Server.Base,
only: [
send_resources_list_changed: 1,
send_resource_updated: 2,
send_resource_updated: 3,
send_prompts_list_changed: 1,
send_tools_list_changed: 1,
send_log_message: 3,
send_log_message: 4,
send_progress: 4,
send_progress: 5
]
import Hermes.Server.Frame
require Hermes.MCP.Message
Module.register_attribute(__MODULE__, :components, accumulate: true)
@before_compile Hermes.Server
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
type: :supervisor,
restart: :permanent
}
end
@impl Hermes.Server.Behaviour
def server_info do
%{"name" => unquote(name), "version" => unquote(version)}
end
@impl Hermes.Server.Behaviour
def server_capabilities, do: unquote(Macro.escape(capabilities))
@impl Hermes.Server.Behaviour
def supported_protocol_versions, do: unquote(protocol_versions)
defoverridable server_info: 0,
server_capabilities: 0,
supported_protocol_versions: 0,
child_spec: 1
end
end
defp parse_capability(capability, %{} = capabilities) when is_server_capability(capability) do
Map.put(capabilities, to_string(capability), %{})
end
defp parse_capability({:resources, opts}, %{} = capabilities) do
subscribe? = opts[:subscribe?]
list_changed? = opts[:list_changed?]
capabilities
|> Map.put("resources", %{})
|> then(&if(is_nil(subscribe?), do: &1, else: Map.put(&1, :subscribe, subscribe?)))
|> then(&if(is_nil(list_changed?), do: &1, else: Map.put(&1, :listChanged, list_changed?)))
end
defp parse_capability({capability, opts}, %{} = capabilities) when is_server_capability(capability) do
list_changed? = opts[:list_changed?]
capabilities
|> Map.put(to_string(capability), %{})
|> then(&if(is_nil(list_changed?), do: &1, else: Map.put(&1, :listChanged, list_changed?)))
end
@doc """
Registers a component (tool, prompt, or resource) with the server.
## Examples
# Register with auto-derived name
component MyServer.Tools.Calculator
# Register with custom name
component MyServer.Tools.FileManager, name: "files"
"""
defmacro component(module, opts \\ []) do
quote bind_quoted: [module: module, opts: opts] do
if not Component.component?(module) do
raise CompileError,
description:
"Module #{to_string(module)} is not a valid component. " <>
"Use `use Hermes.Server.Component, type: :tool/:prompt/:resource`"
end
@components {Component.get_type(module), opts[:name] || Hermes.Server.__derive_component_name__(module), module}
end
end
@doc false
def __derive_component_name__(module) do
module
|> Module.split()
|> List.last()
|> Macro.underscore()
end
@doc false
defmacro __before_compile__(env) do
components = Module.get_attribute(env.module, :components, [])
tools = for {:tool, name, mod} <- components, do: {name, mod}
prompts = for {:prompt, name, mod} <- components, do: {name, mod}
resources = for {:resource, name, mod} <- components, do: {name, mod}
quote do
def __components__(:tool), do: unquote(Macro.escape(tools))
def __components__(:prompt), do: unquote(Macro.escape(prompts))
def __components__(:resource), do: unquote(Macro.escape(resources))
def __components__(_), do: []
@impl Hermes.Server.Behaviour
def handle_request(%{} = request, frame) do
Handlers.handle(request, __MODULE__, frame)
end
defoverridable handle_request: 2
end
end
end