Hermolaos Architecture

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}}