MCP Server Components Guide

This guide shows you how to build Tools, Prompts, and Resources using Hermes component system.

Tools

Tools are functions that AI assistants can call to perform actions.

Basic Tool

defmodule MyServer.Tools.Calculator do
  @moduledoc "Add two numbers"

  use Hermes.Server.Component, type: :tool

  alias Hermes.Server.Response

  schema do
    %{
      a: {:required, :number},
      b: {:required, :number}
    }
  end

  @impl true
  def execute(%{a: a, b: b}, frame) do
    {:reply, Response.text(Response.tool(), "Result: #{a + b}"), frame}
  end
end

Tool with Error Handling

defmodule MyServer.Tools.Divider do
  @moduledoc "Divide two numbers"

  use Hermes.Server.Component, type: :tool

  alias Hermes.Server.Response

  schema do
    %{
      a: {:required, :number},
      b: {:required, :number}
    }
  end

  @impl true
  def execute(%{a: a, b: 0}, frame) do    
    {:reply, Response.error(Response.tool(), "Cannot divide by zero"), frame}
  end

  def execute(%{a: a, b: b}, frame) do
    {:reply, Response.text(Response.tool(), "#{a} รท #{b} = #{a / b}"), frame}
  end
end

Tool with JSON Response

defmodule MyServer.Tools.SystemInfo do
  @moduledoc "Get system information"

  use Hermes.Server.Component, type: :tool

  alias Hermes.Server.Response
  
  @impl true
  def execute(_params, frame) do  
    info = %{
      os: :os.type(),
      memory: :erlang.memory(:total),
      processes: :erlang.system_info(:process_count)
    }
        
    {:reply, Response.json(Response.tool(), info), frame}
  end
end

Prompts

Prompts provide reusable message templates for AI conversations.

Basic Prompt

defmodule MyServer.Prompts.Assistant do
  @moduledoc "General assistant prompt"

  use Hermes.Server.Component, type: :prompt

  alias Hermes.Server.Response

  schema do
    %{style: {:required, {:enum, ["formal", "casual"]}}}
  end

  @impl true
  def get_messages(%{style: "formal"}, frame) do    
    response = 
      Response.prompt()
      |> Response.system_message("You are a formal, professional assistant.")
      |> Response.user_message("Please help me with my task.")
    
    {:reply, response, frame}
  end

  def get_messages(%{style: "casual"}, frame) do
    response = 
      Response.prompt()
      |> Response.system_message("You are a friendly, casual assistant.")
      |> Response.user_message("Hey, can you help me out?")
    
    {:reply, response, frame}
  end
end

Prompt with Context

defmodule MyServer.Prompts.CodeReview do
  @moduledoc "Code review prompt"

  use Hermes.Server.Component, type: :prompt

  schema do
    %{language: {:required, :string}}
  end

  @impl true
  def get_messages(%{language: lang}, frame) do
    response = 
      Response.prompt("Code review for #{lang}")
      |> Response.system_message("You are an expert #{lang} code reviewer.")
      |> Response.user_message("Please review the following #{lang} code for best practices and potential issues.")
    
    {:reply, response, frame}
  end
end

Resources

Resources provide data that AI can read, identified by URIs.

Text Resource

defmodule MyServer.Resources.Config do
  @moduledoc "Application configuration"

  use Hermes.Server.Component,
    type: :resource,
    uri: "config://app",
    mime_type: "application/json"

  alias Hermes.Server.Response

  @impl true
  def read(_params, frame) do
    config = %{version: "1.0.0", env: Mix.env()}
    
    {:reply, Response.json(Response.resource(), config), frame}
  end
end

Binary Resource

defmodule MyServer.Resources.Logo do
  @moduledoc "Company logo"

  use Hermes.Server.Component,
    type: :resource,
    uri: "assets://logo",
    mime_type: "image/png"

  alias Hermes.MCP.Error  
  alias Hermes.Server.Response

  @impl true
  def read(_params, frame) do
    case File.read("priv/static/logo.png") do
      {:ok, binary} ->
        {:reply, Response.blob(Response.resource(), Base.encode64(binary)), frame}
      
      {:error, reason} ->
        {:error, Error.internal_error(%{reason: reason}), frame}
    end
  end
end

Response Builder

The Hermes.Server.Response module provides a fluent API for building responses.

Tool Responses

import Hermes.Server.Response

# Text response
tool() |> text("Hello!") |> build()
# => %{"content" => [%{"type" => "text", "text" => "Hello!"}], "isError" => false}

# JSON response
tool() |> json(%{status: "ok"}) |> build()
# => %{"content" => [%{"type" => "text", "text" => "{\"status\":\"ok\"}"}], "isError" => false}

# Error response
tool() |> error("Something went wrong") |> build()
# => %{"content" => [%{"type" => "text", "text" => "Something went wrong"}], "isError" => true}

# Multiple content items
tool() 
|> text("Processing complete")
|> json(%{items: 10, processed: 10})
|> build()

Prompt Responses

import Hermes.Server.Response

# Simple conversation
prompt()
|> user_message("What's the weather?")
|> assistant_message("Let me check that for you.")
|> build()
# => %{"messages" => [
#      %{"role" => "user", "content" => "What's the weather?"}, 
#      %{"role" => "assistant", "content" => "Let me check that for you."}
#    ]}

# With system context
prompt("Weather Assistant")
|> system_message("You are a helpful weather assistant.")
|> user_message("What's the forecast for tomorrow?")
|> build()

Resource Responses

import Hermes.Server.Response

# Text resource
resource() |> text("File contents here") |> build()
# => %{"text" => "File contents here"}

# Binary resource  
resource() |> blob(base64_data) |> build()
# => %{"blob" => base64_data}

# With metadata
resource()
|> text("Config data")
|> name("Application Config")
|> description("Current app configuration")
|> build()

Registering Components

In your server module:

defmodule MyServer do
  use Hermes.Server,
    name: "My Server",
    version: "1.0.0",
    capabilities: [:tools, :prompts, :resources]

  # Register components
  component MyServer.Tools.Calculator
  component MyServer.Tools.Divider
  component MyServer.Prompts.Assistant
  component MyServer.Resources.Config
  
  @impl true
  def init(_arg, frame) do
    {:ok, frame}
  end
end

Using Frame State

The frame parameter carries state through requests:

defmodule MyServer.Tools.Counter do
  use Hermes.Server.Component, type: :tool

  alias Hermes.Server.Response

  schema do
    %{increment: {:optional, :integer, default: 1}}
  end

  @impl true
  def execute(%{increment: inc}, frame) do
    # Get current count
    count = frame.assigns[:count] || 0
    new_count = count + inc
    
    # Update frame
    new_frame = assign(frame, :count, new_count)
    
    {:reply, Response.text(Response.tool(), "Count: #{new_count}"), new_frame}
  end
end

Return Types

All component callbacks must return one of:

  • {:reply, response, frame} - Success with response
  • {:noreply, frame} - Success without response or delayed response
  • {:error, error, frame} - Error response

Where:

Next Steps