Recipes

Let's explore patterns we've discovered while building with Hermes MCP. What challenges are you facing?

Authentication & Authorization

How do you ensure only authorized clients can access your server? Let's explore a few approaches:

API Key Authentication

defmodule MyApp.AuthenticatedServer do
  use Hermes.Server,
    name: "secure-app",
    version: "1.0.0",
    capabilities: [:tools]

  def init(arg, frame) do
    # Check API key from transport metadata
    api_key = get_in(frame.transport, [:headers, "x-api-key"])

    case authenticate_api_key(api_key) do
      {:ok, user} ->
        # Store user in frame assigns for later use
        {:ok, Map.put(frame, :assigns, %{user: user})}

      :error ->
        {:stop, :unauthorized}
    end
  end

  defp authenticate_api_key(nil), do: :error
  defp authenticate_api_key(key) do
    # Your authentication logic here
    MyApp.Auth.verify_api_key(key)
  end
end

Now your tools can access the authenticated user:

defmodule MyApp.SecureTool do
  use Hermes.Server.Component, type: :tool

  def execute(params, frame) do
    user = frame.assigns.user

    # User-scoped operations
    {:ok, "Hello #{user.name}, you have access to this tool!"}
  end
end

OAuth Integration

For more complex authentication flows:

defmodule MyApp.OAuthResource do
  use Hermes.Server.Component,
    type: :resource,
    uri: "auth://oauth/status"

  def read(_params, frame) do
    case frame.assigns[:oauth_token] do
      nil ->
        {:ok, Jason.encode!(%{
          authenticated: false,
          login_url: generate_oauth_url(frame.private.session_id)
        })}

      token ->
        {:ok, Jason.encode!(%{
          authenticated: true,
          user: fetch_user_info(token)
        })}
    end
  end
end

File Operations

Need to work with files? Here's a safe pattern:

defmodule MyApp.FileManager do
  use Hermes.Server.Component, type: :tool

  @moduledoc "Safely read files from allowed directories"

  schema do
    field :path, :string, required: true
  end

  def execute(%{path: path}, frame) do
    user = frame.assigns.user
    allowed_dirs = get_allowed_directories(user)

    with :ok <- validate_path_access(path, allowed_dirs),
         {:ok, content} <- File.read(path) do
      {:ok, %{
        path: path,
        size: byte_size(content),
        content: content,
        mime_type: MIME.from_path(path)
      }}
    else
      {:error, :access_denied} ->
        {:error, "Access denied to path: #{path}"}

      {:error, reason} ->
        {:error, "Failed to read file: #{inspect(reason)}"}
    end
  end

  defp validate_path_access(path, allowed_dirs) do
    real_path = Path.expand(path)

    if Enum.any?(allowed_dirs, &String.starts_with?(real_path, &1)) do
      :ok
    else
      {:error, :access_denied}
    end
  end
end

Database Operations

How do you expose database queries safely?

defmodule MyApp.QueryBuilder do
  use Hermes.Server.Component, type: :tool

  @moduledoc "Build and execute safe database queries"

  schema do
    field :table, :string, required: true, values: ["users", "products", "orders"]
    field :filters, :map
    field :limit, :integer, default: 100, max: 1000
    field :order_by, :string
  end

  def execute(params, frame) do
    query =
      build_base_query(params.table)
      |> apply_filters(params[:filters])
      |> apply_order(params[:order_by])
      |> apply_limit(params.limit)
      |> apply_user_scope(frame.assigns.user)

    case MyApp.Repo.all(query) do
      results when is_list(results) ->
        {:ok, Enum.map(results, &sanitize_result/1)}

      error ->
        {:error, "Query failed: #{inspect(error)}"}
    end
  end

  defp build_base_query("users"), do: from(u in User)
  defp build_base_query("products"), do: from(p in Product)
  defp build_base_query("orders"), do: from(o in Order)

  defp apply_filters(query, nil), do: query
  defp apply_filters(query, filters) do
    Enum.reduce(filters, query, fn {field, value}, q ->
      where(q, [r], field(r, ^String.to_atom(field)) == ^value)
    end)
  end

  defp sanitize_result(record) do
    # Remove sensitive fields
    record
    |> Map.from_struct()
    |> Map.drop([:password_hash, :api_key, :secret_token])
  end
end

Long-Running Operations

What about operations that take time? Let's handle them gracefully:

defmodule MyApp.ReportGenerator do
  use Hermes.Server.Component, type: :tool

  @moduledoc "Generate complex reports"

  schema do
    field :report_type, :string, required: true
    field :date_range, :map
  end

  def execute(params, frame) do
    # Start async task
    task_id = UUID.uuid4()

    Task.start(fn ->
      result = generate_report(params)
      ReportStore.save(task_id, result)
    end)

    # Return immediately with task ID
    {:ok, %{
      task_id: task_id,
      status: "processing",
      check_status_with: "report_status"
    }}
  end
end

defmodule MyApp.ReportStatus do
  use Hermes.Server.Component, type: :tool

  schema do
    field :task_id, :string, required: true
  end

  def execute(%{task_id: task_id}, _frame) do
    case ReportStore.get(task_id) do
      nil ->
        {:ok, %{status: "processing"}}

      {:completed, result} ->
        {:ok, %{status: "completed", result: result}}

      {:error, reason} ->
        {:ok, %{status: "failed", error: reason}}
    end
  end
end

Real-time Updates

Need to push updates to clients? Here's how:

defmodule MyApp.LiveDataServer do
  use Hermes.Server,
    name: "live-data",
    version: "1.0.0",
    capabilities: [:tools]

  def init(arg, frame) do
    # Subscribe to Phoenix PubSub
    Phoenix.PubSub.subscribe(MyApp.PubSub, "data_updates")
    {:ok, frame}
  end

  def handle_info({:data_update, data}, frame) do
    # Send notification to client
    notification = %{
      method: "notifications/resources/list_changed",
      params: %{}
    }

    send_notification(notification)
    {:noreply, frame}
  end
end

Testing Patterns

How do you test MCP components effectively?

defmodule MyApp.ComponentTest do
  use ExUnit.Case, async: true

  describe "complex tool validation" do
    test "validates required fields" do
      tool = MyApp.ComplexTool

      # Test schema validation
      assert {:error, errors} =
        Hermes.Server.Component.validate_params(tool, %{})

      assert errors[:required_field] == ["is required"]
    end

    test "executes with valid params" do
      params = %{required_field: "value", optional_field: 42}
      frame = %Hermes.Server.Frame{assigns: %{user: %{id: 1}}}

      assert {:ok, result} = MyApp.ComplexTool.execute(params, frame)
      assert result.processed == true
    end
  end
end

Error Recovery

How do you handle and recover from errors gracefully?

defmodule MyApp.ResilientTool do
  use Hermes.Server.Component, type: :tool

  def execute(params, frame) do
    with {:ok, data} <- fetch_external_data(params),
         {:ok, processed} <- process_data(data),
         {:ok, stored} <- store_results(processed) do
      {:ok, format_success(stored)}
    else
      {:error, :external_service_down} ->
        # Fallback to cache
        case get_cached_data(params) do
          {:ok, cached} ->
            {:ok, %{data: cached, source: "cache", warning: "Using cached data"}}
          :error ->
            {:error, "Service unavailable and no cached data found"}
        end

      {:error, :rate_limited} ->
        {:error, "Rate limited. Please try again in a few minutes."}

      {:error, reason} ->
        Logger.error("Tool execution failed: #{inspect(reason)}")
        {:error, "An unexpected error occurred"}
    end
  end
end

Performance Optimization

Need to handle high-throughput scenarios?

defmodule MyApp.BatchProcessor do
  use Hermes.Server.Component, type: :tool

  schema do
    field :items, {:array, :map}, required: true, max_items: 1000
  end

  def execute(%{items: items}, frame) do
    # Process in parallel with flow control
    results =
      items
      |> Task.async_stream(
        &process_item/1,
        max_concurrency: System.schedulers_online() * 2,
        timeout: 5_000,
        on_timeout: :kill_task
      )
      |> Enum.map(fn
        {:ok, result} -> result
        {:exit, :timeout} -> {:error, "Processing timeout"}
      end)

    successful = Enum.filter(results, &match?({:ok, _}, &1))
    failed = Enum.filter(results, &match?({:error, _}, &1))

    {:ok, %{
      processed: length(successful),
      failed: length(failed),
      results: successful
    }}
  end
end

Logging & Monitoring

How do you track what's happening in your MCP system? Let's explore logging patterns:

MCP Protocol Logging

Control log verbosity from servers you connect to:

# Tell the server to only send warnings and above
MyApp.Client.set_log_level("warning")

# Register a handler for incoming logs
MyApp.Client.register_log_callback(fn level, data, logger ->
  # Forward to your monitoring service
  MyApp.Telemetry.log_event(level, data, source: logger)

  # Or filter specific loggers
  if logger == "database" do
    Logger.warning("Database: #{data}")
  end
end)

Server-Side Logging

Send structured logs to connected clients:

defmodule MyApp.LoggingServer do
  use Hermes.Server,
    name: "logging-demo",
    version: "1.0.0",
    capabilities: [:logging]  # Advertise logging support

  def handle_request(%{"method" => "tools/call"} = request, frame) do
    # Send log notifications to client
    send_log_message(self(), "info", "Processing tool request", "request_handler")

    # Do the work...
    result = process_request(request)

    send_log_message(self(), "debug", "Request completed", "request_handler")
    {:reply, result, frame}
  end
end

Configuring Library Logging

Control Hermes's internal logging verbosity:

# In config/config.exs
config :hermes_mcp, :logging,
  client_events: :info,      # Client lifecycle events
  server_events: :info,      # Server request handling
  transport_events: :warning, # Connection issues
  protocol_messages: :debug   # Raw message exchanges

# Or disable completely for production
config :hermes_mcp, log: false

What Pattern Do You Need?

These recipes come from real-world usage. What challenges are you facing?

  • Complex authentication flows?
  • Multi-step workflows?
  • Integration with existing systems?
  • Performance at scale?
  • Logging and observability?

Each pattern can be adapted to your specific needs. The key insight? MCP handles the protocol complexity while you focus on your domain logic.

Ready to implement one of these patterns?