Client Usage
Using the Hermes MCP client with the new DSL.
Basic Setup
Once your client module is defined and added to the supervision tree, it automatically:
- Connects to the server
- Negotiates protocol version
- Exchanges capabilities
- Completes handshake
Connection Status
# Using your client module directly
case MyApp.MCPClient.ping() do
:pong ->
IO.puts("Server is responsive")
{:error, reason} ->
IO.puts("Connection error: #{inspect(reason)}")
end
Server Information
# Get capabilities
capabilities = MyApp.MCPClient.get_server_capabilities()
# Get server info
info = MyApp.MCPClient.get_server_info()
Working with Tools
List Tools
case MyApp.MCPClient.list_tools() do
{:ok, %{result: %{"tools" => tools}}} ->
for tool <- tools do
IO.puts("#{tool["name"]}: #{tool["description"]}")
end
{:error, error} ->
IO.puts("Error: #{inspect(error)}")
end
Call Tool
case MyApp.MCPClient.call_tool("calculator", %{expr: "2+2"}) do
{:ok, %{is_error: false, result: result}} ->
IO.puts("Result: #{inspect(result)}")
{:ok, %{is_error: true, result: error}} ->
IO.puts("Tool error: #{error["message"]}")
{:error, error} ->
IO.puts("Protocol error: #{inspect(error)}")
end
Working with Resources
List Resources
{:ok, %{result: %{"resources" => resources}}} =
MyApp.MCPClient.list_resources()
for resource <- resources do
IO.puts("#{resource["name"]} (#{resource["uri"]})")
end
Read Resource
case MyApp.MCPClient.read_resource("file:///data.txt") do
{:ok, %{result: %{"contents" => contents}}} ->
for content <- contents do
case content do
%{"text" => text} -> IO.puts(text)
%{"blob" => blob} -> IO.puts("Binary: #{byte_size(blob)} bytes")
end
end
{:error, error} ->
IO.puts("Error: #{inspect(error)}")
end
Pagination
# First page
{:ok, %{result: result}} = MyApp.MCPClient.list_resources()
resources = result["resources"]
cursor = result["nextCursor"]
# Next page
if cursor do
{:ok, %{result: more}} =
MyApp.MCPClient.list_resources(cursor: cursor)
end
Working with Prompts
List Prompts
{:ok, %{result: %{"prompts" => prompts}}} =
MyApp.MCPClient.list_prompts()
for prompt <- prompts do
IO.puts("#{prompt["name"]}: #{prompt["description"]}")
end
Get Prompt
args = %{"language" => "elixir", "task" => "refactor"}
case MyApp.MCPClient.get_prompt("code_review", args) do
{:ok, %{result: %{"messages" => messages}}} ->
for msg <- messages do
IO.puts("#{msg["role"]}: #{msg["content"]}")
end
{:error, error} ->
IO.puts("Error: #{inspect(error)}")
end
Timeouts
Set custom timeouts per operation:
# 60 second timeout
opts = [timeout: 60_000]
MyApp.MCPClient.call_tool("slow_tool", %{}, opts)
Default timeout is 30 seconds.
Autocompletion
Get completion suggestions:
# For prompt arguments
ref = %{"type" => "ref/prompt", "name" => "code_review"}
arg = %{"name" => "language", "value" => "py"}
{:ok, response} = MyApp.MCPClient.complete(ref, arg)
# => ["python", "pytorch", "pydantic"]
# For resource URIs
ref = %{"type" => "ref/resource", "uri" => "file:///"}
arg = %{"name" => "path", "value" => "doc"}
{:ok, response} = MyApp.MCPClient.complete(ref, arg)
# => ["docs/", "docker-compose.yml", "documentation.md"]
Error Handling
See Error Handling for patterns.
Graceful Shutdown
MyApp.MCPClient.close()
This also shuts down the transport.
Advanced Usage
Multiple Client Instances
You can run multiple instances of the same client with different names:
# In your supervision tree
children = [
{MyApp.MCPClient,
name: :client_one,
transport: {:stdio, command: "server1", args: []}},
{MyApp.MCPClient,
name: :client_two,
transport: {:stdio, command: "server2", args: []}}
]
# Use specific instances
MyApp.MCPClient.ping(:client_one)
MyApp.MCPClient.ping(:client_two)
Custom Process Naming
For distributed systems using registries like Horde:
{MyApp.MCPClient,
name: {:via, Horde.Registry, {MyCluster, "client_1"}},
transport_name: {:via, Horde.Registry, {MyCluster, "transport_1"}},
transport: {:stdio, command: "server", args: []}}
Manual Client Setup (Advanced)
For advanced use cases, you can manually start the client base and transport:
# Start client first (it will hibernate waiting for transport)
{:ok, client_pid} = Hermes.Client.Base.start_link(
name: :my_client,
transport: [
layer: Hermes.Transport.STDIO,
name: :my_transport
],
client_info: %{
"name" => "MyApp",
"version" => "1.0.0"
},
capabilities: %{
"roots" => %{"listChanged" => true},
"sampling" => %{}
},
protocol_version: "2024-11-05"
)
# Then start the transport (it will send :initialize to the client)
{:ok, transport_pid} = Hermes.Transport.STDIO.start_link(
name: :my_transport,
client: :my_client,
command: "python",
args: ["-m", "mcp.server"]
)
# Use the client
:pong = Hermes.Client.Base.ping(:my_client)
{:ok, tools} = Hermes.Client.Base.list_tools(:my_client)
Important
The client must be started before the transport. The client process will hibernate
waiting for the :initialize
message from the transport process. The client name
in the transport configuration must match the actual client process name.