Hermolaos Architecture

Copy Markdown View Source

This document describes the internal architecture of the Hermolaos MCP client library.

Overview

Hermolaos is an Elixir client for the Model Context Protocol (MCP). It provides a clean API for connecting to MCP servers and invoking tools, reading resources, and using prompts.


                      Application                            

                       Hermolaos API                            
    connect/2, list_tools/1, call_tool/3, ping/1, etc.       

                    Hermolaos.Pool (optional)                   
            Connection pooling & load balancing              

                  Hermolaos.Client.Connection                   
           GenServer state machine per connection            

   Transport Layer             Protocol Layer               
        
   Transport.Stdio       Protocol.JsonRpc               
   Transport.HTTP        Protocol.Messages              
   MessageBuffer         Protocol.Capabilities          
       Protocol.Errors                
                            

               Client Support Modules                        
     RequestTracker (ETS)  NotificationHandler              

Module Structure

Public API (lib/hermolaos.ex)

The main entry point for users. Provides a simple, consistent API that delegates to the underlying connection management.

Key functions:

  • connect/2 - Establish a new MCP connection
  • disconnect/1 - Close a connection
  • list_tools/1, call_tool/3 - Tool operations
  • list_resources/1, read_resource/2 - Resource operations
  • list_prompts/1, get_prompt/3 - Prompt operations
  • ping/1 - Health check

Connection Management (lib/hermolaos/client/connection.ex)

A GenServer that manages the lifecycle of a single MCP connection. Implements a state machine:

               
 disconnected    connecting    initializing    ready  
               
                                                                       
        error/disconnect 

States:

  • disconnected: Initial state, no transport active
  • connecting: Transport is starting up
  • initializing: MCP initialize handshake in progress
  • ready: Connection is fully established, can process requests

Transport Layer

Transport Behaviour (lib/hermolaos/transport/behaviour.ex)

Defines the interface that all transports must implement:

@callback start_link(opts :: keyword()) :: GenServer.on_start()
@callback send_message(transport :: t(), message :: binary()) :: :ok | {:error, term()}
@callback stop(transport :: t()) :: :ok

Stdio Transport (lib/hermolaos/transport/stdio.ex)

Uses Erlang ports to communicate with subprocess MCP servers:

                    
  Hermolaos Client                         MCP Server    
                                        (subprocess)  
         stdin/stdout                    
     Port     server.exe   
                                         
                    

Features:

  • Spawns server as subprocess via Port.open/2
  • Binary, line-based communication
  • Automatic process cleanup on connection close
  • Environment variable passthrough

HTTP Transport (lib/hermolaos/transport/http.ex)

Uses Req to communicate with HTTP-based MCP servers:

                    
  Hermolaos Client        HTTP POST        MCP Server    
                    (HTTP)        
                     
      Req         JSON / SSE                        
                        

Features:

  • JSON-RPC over HTTP POST
  • Server-Sent Events (SSE) for streaming responses
  • Session ID tracking via Mcp-Session-Id header
  • Connection pooling via Finch (Req's HTTP client)

Message Buffer (lib/hermolaos/transport/message_buffer.ex)

Handles the newline-delimited JSON format used by MCP:

{"jsonrpc":"2.0","id":1,"method":"ping"}\n
{"jsonrpc":"2.0","id":2,"method":"tools/list"}\n

Features:

  • Accumulates partial data chunks
  • Extracts complete messages on newline boundaries
  • Handles edge cases (empty lines, partial JSON)
  • Tracks statistics (bytes received, parse errors)

Protocol Layer

JSON-RPC (lib/hermolaos/protocol/json_rpc.ex)

Implements JSON-RPC 2.0 message encoding/decoding:

Message types:

  • Request: {id, method, params} - Expects response
  • Notification: {method, params} - No response expected
  • Response: {id, result} - Success response
  • Error Response: {id, error} - Error response

Messages (lib/hermolaos/protocol/messages.ex)

Builds MCP-specific message payloads:

Messages.initialize(client_info, capabilities)
Messages.tools_list()
Messages.tools_call("tool_name", %{arg: "value"})
Messages.resources_read("file:///path")

Capabilities (lib/hermolaos/protocol/capabilities.ex)

Handles capability negotiation between client and server:

# Client capabilities (what we support)
%{
  "roots" => %{"listChanged" => true},
  "sampling" => %{}
}

# Server capabilities (what server supports)
%{
  "tools" => %{},
  "resources" => %{"subscribe" => true},
  "prompts" => %{}
}

Errors (lib/hermolaos/protocol/errors.ex)

Defines MCP error codes and provides helpers:

CodeNameDescription
-32700Parse ErrorInvalid JSON
-32600Invalid RequestNot a valid JSON-RPC request
-32601Method Not FoundUnknown method
-32602Invalid ParamsInvalid method parameters
-32603Internal ErrorInternal JSON-RPC error
-32001Request TimeoutRequest timed out
-32002Resource Not FoundResource doesn't exist
-32003Capability Not SupportedServer doesn't support capability

Request Tracking (lib/hermolaos/client/request_tracker.ex)

ETS-backed storage for correlating requests with responses:


                     ETS Table                              

   ID      Method         From           Timeout Ref    

    1    tools/list   {pid, ref}      #Reference<...>   
    2    ping         {pid, ref}      #Reference<...>   

Features:

  • O(1) lookups via ETS
  • Monotonically increasing integer IDs
  • Automatic timeout handling with per-request timers
  • Statistics tracking (tracked, completed, failed, timed out)

Notification Handling (lib/hermolaos/client/notification_handler.ex)

Behaviour for handling server-initiated messages:

defmodule MyHandler do
  @behaviour Hermolaos.Client.NotificationHandler

  @impl true
  def handle_notification({:notification, "notifications/tools/list_changed", _}, state) do
    # Tools list changed, maybe refresh cache
    {:ok, state}
  end
end

Built-in handlers:

  • DefaultNotificationHandler - Logs notifications
  • PubSubNotificationHandler - Broadcasts via Phoenix.PubSub or Registry

Connection Pool (lib/hermolaos/pool.ex)

Manages multiple connections for high-throughput scenarios:


                      Hermolaos.Pool                            
                                                             
     
                DynamicSupervisor                          
                                                           
                      
     Conn #1  │  │ Conn #2  │  │ Conn #3  │  ...      │   │
                      
     
                                                             
  Strategies: :round_robin | :random | :least_busy          

Request Flow

Making a Tool Call

1. User calls Hermolaos.call_tool(conn, "tool_name", args)
   
2. Connection.call_tool/3 called
   
3. RequestTracker assigns ID and stores caller info
   
4. Message built: Messages.tools_call("tool_name", args)
   
5. JSON-RPC encoded: JsonRpc.encode_request(id, "tools/call", params)
   
6. Transport.send_message(transport, json)
   
7. Transport sends to server (stdio/HTTP)
   
    (async - server processing)
   
8. Server response arrives at transport
   
9. MessageBuffer extracts complete JSON message
   
10. JsonRpc.decode/1 parses response
    
11. RequestTracker.complete/2 retrieves caller
    
12. GenServer.reply(from, {:ok, result})
    
13. User receives {:ok, %{content: [...]}}

MCP Initialization Handshake

Client                                  Server
                                          
    initialize 
        {protocolVersion, clientInfo,     
         capabilities}                    
                                          
    response 
        {protocolVersion, serverInfo,     
         capabilities}                    
                                          
    notifications/initialized 
        (no response expected)            
                                          
           Connection Ready               

Concurrency Model

  • Each Connection is an isolated GenServer (crash isolation)
  • RequestTracker uses ETS for concurrent-safe lookups
  • Pool uses DynamicSupervisor for connection management
  • Transports handle I/O asynchronously

Error Handling

Transport Errors

  • Connection closed → All pending requests failed
  • Send error → Request fails immediately
  • Process crash → Supervisor restarts transport

Protocol Errors

  • Parse error → Error response to client
  • Invalid request → Error response to client
  • Timeout → Request fails with timeout error

Request Errors

  • Server returns error → Wrapped in Hermolaos.Error
  • Timeout → {:error, %Hermolaos.Error{code: -32001}}