Examples
This page provides practical examples of using Hermes MCP in different scenarios. Each example demonstrates a specific use case or pattern to help you implement MCP in your Elixir applications.
Basic Client Example
This example shows a complete implementation of a simple Hermes MCP client that connects to a Python MCP server.
Given an Echo Python MCP server:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Echo")
@mcp.resource("echo://{message}")
def echo_resource(message: str) -> str:
"""Echo a message as a resource"""
return f"Resource echo: {message}"
@mcp.tool()
def echo_tool(message: str) -> str:
"""Echo a message as a tool"""
return f"Tool echo: {message}"
@mcp.prompt()
def echo_prompt(message: str) -> str:
"""Create an echo prompt"""
return f"Please process this message: {message}"
if __name__ == "__main__":
mcp.run(transport='stdio')
You can check the custom Mix.Tasks that Hermes defines
to run and interactive with this server from Elixir, that are:
Authentication Example
This example demonstrates a custom authentication middleware for STDIO transport:
defmodule MyApp.AuthenticatedTransport do
@moduledoc """
A transport middleware that adds authentication to JSON-RPC messages.
"""
@behaviour Hermes.Transport.Behaviour
alias Hermes.Transport.STDIO
defstruct [:inner_transport, :auth_token, :client]
@doc """
Starts a new authenticated transport.
## Options
* `:inner_transport_module` - The underlying transport module to use (required)
* `:auth_token` - The authentication token to use (required)
* Other options are passed to the inner transport
"""
@impl true
def start_link(opts) do
client = Keyword.fetch!(opts, :client)
auth_token = Keyword.fetch!(opts, :auth_token)
inner_transport_module = Keyword.get(opts, :inner_transport_module, STDIO)
# Remove our custom options before passing to inner transport
inner_opts = opts
|> Keyword.delete(:auth_token)
|> Keyword.delete(:inner_transport_module)
with {:ok, transport} <- inner_transport_module.start_link(inner_opts) do
state = %__MODULE__{
inner_transport: transport,
auth_token: auth_token,
client: client
}
pid = spawn_link(fn -> process_loop(state) end)
# Register the process name if provided
if name = Keyword.get(opts, :name) do
Process.register(pid, name)
end
{:ok, pid}
end
end
@impl true
def send_message(pid, message) when is_pid(pid) and is_binary(message) do
send(pid, {:send, message})
:ok
end
@impl true
def send_message(name, message) when is_atom(name) and is_binary(message) do
send(name, {:send, message})
:ok
end
# Main process loop
defp process_loop(state) do
receive do
{:send, message} ->
# Add authentication to outgoing messages
case Jason.decode(message) do
{:ok, decoded} ->
authenticated = add_auth(decoded, state.auth_token)
case Jason.encode(authenticated) do
{:ok, json} ->
Hermes.Transport.Behaviour.send_message(state.inner_transport, json <> "\n")
{:error, reason} ->
error = "Failed to encode authenticated message: #{inspect(reason)}"
IO.puts(:stderr, error)
end
{:error, reason} ->
error = "Failed to decode message for authentication: #{inspect(reason)}"
IO.puts(:stderr, error)
end
{:response, data} ->
# Forward responses directly to the client
send(state.client, {:response, data})
other ->
IO.puts(:stderr, "AuthenticatedTransport received unknown message: #{inspect(other)}")
end
process_loop(state)
end
# Add authentication metadata to requests
defp add_auth(%{"method" => method, "params" => params} = request, token) do
# Skip authentication for the initialization message
if method == "initialize" do
request
else
auth_meta = %{"_meta" => %{"auth" => %{"token" => token}}}
# Merge with existing params
updated_params = Map.merge(params || %{}, auth_meta)
%{request | "params" => updated_params}
end
end
# Handle messages without params
defp add_auth(%{"method" => method} = request, token) do
if method == "initialize" do
request
else
auth_meta = %{"_meta" => %{"auth" => %{"token" => token}}}
Map.put(request, "params", auth_meta)
end
end
# Pass through other message types unchanged
defp add_auth(message, _token), do: message
end
Usage example with authenticated transport:
# In your application's supervision tree
children = [
# ...
# Start the authenticated transport
{MyApp.AuthenticatedTransport, [
name: MyApp.MCPTransport,
client: MyApp.MCPClient,
auth_token: System.fetch_env!("MCP_AUTH_TOKEN"),
inner_transport_module: Hermes.Transport.STDIO,
command: "mcp",
args: ["run", "server.py"]
]},
# Start the MCP client using the authenticated transport
{Hermes.Client, [
name: MyApp.MCPClient,
transport: MyApp.MCPTransport,
client_info: %{
"name" => "MyAuthenticatedApp",
"version" => "1.0.0"
},
capabilities: %{
"resources" => %{},
"tools" => %{},
"prompts" => %{}
}
]},
# ...
]
Resource Abstraction Example
This example demonstrates a higher-level abstraction over MCP resources:
defmodule MyApp.Resource do
@moduledoc """
A high-level abstraction for working with MCP resources.
"""
alias Hermes.Client
@type t :: %__MODULE__{
uri: String.t(),
name: String.t(),
description: String.t() | nil,
mime_type: String.t() | nil,
size: integer() | nil,
client: pid() | atom()
}
defstruct [:uri, :name, :description, :mime_type, :size, :client]
@doc """
Lists all available resources from the MCP server.
"""
@spec list(pid() | atom()) :: {:ok, [t()]} | {:error, term()}
def list(client) do
case Client.list_resources(client) do
{:ok, %{"resources" => resources}} ->
resources = Enum.map(resources, fn resource ->
%__MODULE__{
uri: resource["uri"],
name: resource["name"],
description: resource["description"],
mime_type: resource["mimeType"],
size: resource["size"],
client: client
}
end)
{:ok, resources}
error ->
error
end
end
@doc """
Reads the content of a resource.
Returns the content as text if possible, otherwise as binary.
"""
@spec read(t() | String.t(), pid() | atom()) :: {:ok, String.t() | binary()} | {:error, term()}
def read(%__MODULE__{} = resource), do: read(resource.uri, resource.client)
def read(uri, client) when is_binary(uri) do
case Client.read_resource(client, uri) do
{:ok, %{"contents" => contents}} ->
content = case contents do
[%{"text" => text} | _] -> {:ok, text}
[%{"blob" => blob} | _] -> {:ok, blob}
[] -> {:error, :empty_content}
other -> {:error, {:unexpected_content, other}}
end
content
error ->
error
end
end
@doc """
Creates a resource from a map of resource attributes.
"""
@spec from_map(map(), pid() | atom()) :: t()
def from_map(map, client) do
%__MODULE__{
uri: map["uri"],
name: map["name"],
description: map["description"],
mime_type: map["mimeType"],
size: map["size"],
client: client
}
end
@doc """
Finds a resource by name.
"""
@spec find_by_name(String.t(), pid() | atom()) :: {:ok, t()} | {:error, :not_found} | {:error, term()}
def find_by_name(name, client) do
with {:ok, resources} <- list(client) do
case Enum.find(resources, fn r -> r.name == name end) do
nil -> {:error, :not_found}
resource -> {:ok, resource}
end
end
end
@doc """
Checks if a resource is text-based or binary.
"""
@spec text?(t()) :: boolean()
def text?(%__MODULE__{mime_type: mime_type}) when is_binary(mime_type) do
String.starts_with?(mime_type, "text/") or
mime_type in ["application/json", "application/xml", "application/javascript"]
end
def text?(_), do: false
end
Usage example:
# List all resources
{:ok, resources} = MyApp.Resource.list(MyApp.MCPClient)
# Find a specific resource by name
{:ok, config} = MyApp.Resource.find_by_name("config.json", MyApp.MCPClient)
# Read a resource's content
{:ok, content} = MyApp.Resource.read(config)
if MyApp.Resource.text?(config) do
IO.puts("Text content: #{content}")
else
IO.puts("Binary content: #{byte_size(content)} bytes")
end
These examples demonstrate different approaches to using Hermes MCP in your Elixir applications. You can adapt and extend these patterns to suit your specific needs.