Client Usage Guide
This guide covers the basic usage patterns for the Hermes MCP client.
Client Lifecycle
Once properly configured in your application's supervision tree, the Hermes client automatically handles:
- Initial connection to the server
- Protocol version negotiation
- Capability exchange
- Handshake completion
For example, if you're using the Hermes.Transport.STDIO
layer for your client, this flow would lloks like to:
sequenceDiagram
participant Client
participant Server Process
Client->>+Server Process: Launch subprocess
loop Message Exchange
Client->>Server Process: Write to stdin
Server Process->>Client: Write to stdout
Server Process--)Client: Optional logs on stderr
end
Client->>Server Process: Close stdin, terminate subprocess
deactivate Server Process
Otherwise, if you're using the Hermes.Transport.SSE
layer, the flow would look like this:
sequenceDiagram
participant Client
participant Server
Client->>Server: Open SSE connection
Server->>Client: endpoint event
loop Message Exchange
Client->>Server: HTTP POST messages
Server->>Client: SSE message events
end
Client->>Server: Close SSE connection
Note
These sequence diagrams can be found on the official MCP specification
Generally speaking, the client <> server lifecycle on MCP can be summarized as:
- Initialization: Capability negotiation and protocol version agreement
- Operation: Normal protocol communication
- Shutdown: Graceful termination of the connection
sequenceDiagram
participant Client
participant Server
Note over Client,Server: Initialization Phase
activate Client
Client->>+Server: initialize request
Server-->>Client: initialize response
Client--)Server: initialized notification
Note over Client,Server: Operation Phase
rect rgb(200, 220, 250)
note over Client,Server: Normal protocol operations
end
Note over Client,Server: Shutdown
Client--)-Server: Disconnect
deactivate Server
Note over Client,Server: Connection closed
Basic Operations
Checking Connection Status
You can check if a server is responsive with the ping/1
function:
case Hermes.Client.ping(MyApp.MCPClient) do
:pong -> IO.puts("Server is responsive")
{:error, reason} -> IO.puts("Connection error: #{inspect(reason)}")
end
Retrieving Server Information
After initialization, you can access the server's capabilities and information:
# Get server capabilities
server_capabilities = Hermes.Client.get_server_capabilities(MyApp.MCPClient)
IO.inspect(server_capabilities, label: "Server capabilities")
# Get server version information
server_info = Hermes.Client.get_server_info(MyApp.MCPClient)
IO.inspect(server_info, label: "Server info")
Working with Resources
Listing Resources
To retrieve the list of available resources from the server:
case Hermes.Client.list_resources(MyApp.MCPClient) do
# this is an "application" error, from the server
{:ok, %Hermes.MCP.Response{is_error: true}} ->
IO.puts("Error listing resources")
{:ok, %Hermes.MCP.Response{result: %{"resources" => resources}}} ->
IO.puts("Available resources:")
Enum.each(resources, fn resource ->
IO.puts(" - #{resource["name"]} (#{resource["uri"]})")
end)
# this is a transport/protocol error
{:error, %Hermes.MCP.Error{} = error} ->
IO.puts("Error listing resources: #{inspect(error)}")
end
Pagination
For large resource collections, you can use pagination with cursors:
# First page
{:ok, %Hermes.MCP.Response{result: %{"resources" => resources, "nextCursor" => cursor}}} =
Hermes.Client.list_resources(MyApp.MCPClient)
# Get next page if a cursor is available
if cursor do
{:ok, %Hermes.MCP.Response{result: %{"resources" => more_resources}}} =
Hermes.Client.list_resources(MyApp.MCPClient, cursor: cursor)
end
Reading Resources
To read the contents of a specific resource:
case Hermes.Client.read_resource(MyApp.MCPClient, "file:///example.txt") do
# this is an "application" error, from the server
{:ok, %Hermes.MCP.Response{is_error: true}} ->
IO.puts("Error reading resources")
{:ok, %Hermes.MCP.Response{result: %{"contents" => contents}}} ->
Enum.each(contents, fn content ->
case content do
%{"text" => text} -> IO.puts("Text content: #{text}")
%{"blob" => blob} -> IO.puts("Binary content: #{byte_size(blob)} bytes")
end
end)
# this is a transport/protocol error
{:error, error} ->
IO.puts("Error reading resource: #{inspect(error)}")
end
Working with Tools
Listing Tools
To discover available tools:
case Hermes.Client.list_tools(MyApp.MCPClient) do
# this is an "application" error, from the server
{:ok, %Hermes.MCP.Response{is_error: true}} ->
IO.puts("Error listing tools")
{:ok, %Hermes.MCP.Response{result: %{"tools" => tools}}} ->
IO.puts("Available tools:")
Enum.each(tools, fn tool ->
IO.puts(" - #{tool["name"]}: #{tool["description"] || "No description"}")
end)
# this is a transport/protocol error
{:error, error} ->
IO.puts("Error listing tools: #{inspect(error)}")
end
Calling Tools
To invoke a tool with arguments:
tool_name = "calculate"
tool_args = %{"expression" => "2 + 2"}
case Hermes.Client.call_tool(MyApp.MCPClient, tool_name, tool_args) do
# this is an "application" error, from the server
{:ok, %Hermes.MCP.Response{is_error: true}} ->
IO.puts("Tool execution error:")
Enum.each(content, fn item ->
case item do
%{"type" => "text", "text" => text} -> IO.puts(" #{text}")
_ -> IO.puts(" #{inspect(item)}")
end
end)
{:ok, %Hermes.MCP.Response{result: %{"content" => content}}} ->
IO.puts("Tool result:")
Enum.each(content, fn item ->
case item do
%{"type" => "text", "text" => text} -> IO.puts(" #{text}")
_ -> IO.puts(" #{inspect(item)}")
end
end)
# this is a transport/protocol error
{:error, error} ->
IO.puts("Error calling tool: #{inspect(error)}")
end
Working with Prompts
Listing Prompts
To list available prompts:
case Hermes.Client.list_prompts(MyApp.MCPClient) do
# this is an "application" error, from the server
{:ok, %Hermes.MCP.Response{is_error: true}} ->
IO.puts("Error listing prompts")
{:ok, %Hermes.MCP.Response{result: %{"prompts" => prompts}}} ->
IO.puts("Available prompts:")
Enum.each(prompts, fn prompt ->
required_args = prompt["arguments"]
|> Enum.filter(& &1["required"])
|> Enum.map(& &1["name"])
|> Enum.join(", ")
IO.puts(" - #{prompt["name"]}")
IO.puts(" Required args: #{required_args}")
end)
# this is a transport/protocol error
{:error, error} ->
IO.puts("Error listing prompts: #{inspect(error)}")
end
Getting Prompts
To retrieve a specific prompt with arguments:
prompt_name = "code_review"
prompt_args = %{"code" => "def hello():\n print('world')"}
case Hermes.Client.get_prompt(MyApp.MCPClient, prompt_name, prompt_args) do
# this is an "application" error, from the server
{:ok, %Hermes.MCP.Response{is_error: true}} ->
IO.puts("Error getting prompt")
{:ok, %Hermes.MCP.Response{result: %{"messages" => messages}}} ->
IO.puts("Prompt messages:")
Enum.each(messages, fn message ->
role = message["role"]
content = case message["content"] do
%{"type" => "text", "text" => text} -> text
_ -> inspect(message["content"])
end
IO.puts(" #{role}: #{content}")
end)
# this is a transport/protocol error
{:error, error} ->
IO.puts("Error getting prompt: #{inspect(error)}")
end
Timeouts and Cancellation
You can specify custom timeouts for operations:
# Use a custom timeout (in milliseconds)
Hermes.Client.call_tool(MyApp.MCPClient, "slow_tool", %{}, timeout: to_timeout(second: 60))
Extended Capabilities
To extend the client's capabilities after initialization:
# Add sampling capability
new_capabilities = %{"sampling" => %{}}
updated_capabilities = Hermes.Client.merge_capabilities(MyApp.MCPClient, new_capabilities)
Graceful Shutdown
To gracefully close a client connection:
Hermes.Client.close(MyApp.MCPClient)
This will shutdown the transport layer too