<p align="center">
<img src="images/header.jpeg" alt="Hermolaos - An Elixir client for the Model Context Protocol (MCP)" width="100%">
</p>
<p align="center">
<a href="https://hex.pm/packages/hermolaos"><img src="https://img.shields.io/hexpm/v/hermolaos.svg" alt="Hex.pm"></a>
<a href="https://hexdocs.pm/hermolaos"><img src="https://img.shields.io/badge/hex-docs-blue.svg" alt="Docs"></a>
<a href="LICENSE"><img src="https://img.shields.io/hexpm/l/hermolaos.svg" alt="License"></a>
</p>
An Elixir client for the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), enabling communication with AI tools and resources through a standardized protocol.
## Features
- **Two transports**: Stdio (subprocess) and HTTP/SSE (remote servers)
- **Full MCP support**: Tools, resources, prompts, and notifications
- **Connection pooling**: Built-in pool with load balancing strategies
- **Non-blocking**: Async operations with ETS-backed request tracking
- **Extensible**: Custom notification handlers and transport implementations
## Requirements
- **Elixir** >= 1.14
- **Erlang/OTP** >= 25
- **Node.js** >= 18 (only for stdio transport with npm-based MCP servers)
## Installation
Add `hermolaos` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:hermolaos, "~> 0.3.0"}
]
end
```
## Quick Start
### Connecting to a Stdio Server
```elixir
# Connect to a local MCP server via subprocess
{:ok, conn} = Hermolaos.connect(:stdio,
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
)
# List available tools
{:ok, %{tools: tools}} = Hermolaos.list_tools(conn)
# Call a tool
{:ok, result} = Hermolaos.call_tool(conn, "read_file", %{path: "/tmp/test.txt"})
# Disconnect when done
:ok = Hermolaos.disconnect(conn)
```
### Connecting to an HTTP Server
```elixir
# Connect to a remote MCP server via HTTP
{:ok, conn} = Hermolaos.connect(:http,
url: "http://localhost:3000/mcp"
)
# Use the same API as stdio
{:ok, %{tools: tools}} = Hermolaos.list_tools(conn)
```
## API Reference
### Connection Management
```elixir
# Connect with options
{:ok, conn} = Hermolaos.connect(:stdio, command: "server", args: ["--flag"])
{:ok, conn} = Hermolaos.connect(:http, url: "http://localhost:3000/mcp")
# Disconnect
:ok = Hermolaos.disconnect(conn)
# Health check
{:ok, %{}} = Hermolaos.ping(conn)
# Get connection status
:ready = Hermolaos.status(conn)
```
### Tools
```elixir
# List all available tools
{:ok, %{tools: tools}} = Hermolaos.list_tools(conn)
# Call a tool with arguments
{:ok, result} = Hermolaos.call_tool(conn, "tool_name", %{arg1: "value"})
# With custom timeout
{:ok, result} = Hermolaos.call_tool(conn, "slow_tool", %{}, timeout: 60_000)
# Extract text from result
text = Hermolaos.get_text(result)
# Extract image from result (returns decoded binary)
{:ok, image_data} = Hermolaos.get_image(result)
File.write!("output.png", image_data)
```
### Resources
```elixir
# List available resources
{:ok, %{resources: resources}} = Hermolaos.list_resources(conn)
# Read a specific resource
{:ok, %{contents: contents}} = Hermolaos.read_resource(conn, "file:///path/to/file")
```
### Prompts
```elixir
# List available prompts
{:ok, %{prompts: prompts}} = Hermolaos.list_prompts(conn)
# Get a prompt with arguments
{:ok, %{messages: messages}} = Hermolaos.get_prompt(conn, "prompt_name", %{arg: "value"})
```
## Connection Options
### Stdio Transport
```elixir
Hermolaos.connect(:stdio,
command: "path/to/server", # Required: executable path
args: ["--flag", "value"], # Optional: command line arguments
env: %{"VAR" => "value"}, # Optional: environment variables
timeout: 30_000 # Optional: request timeout (default: 30s)
)
```
### HTTP Transport
```elixir
Hermolaos.connect(:http,
url: "http://localhost:3000/mcp", # Required: server URL
headers: [{"authorization", "Bearer token"}], # Optional: custom headers
timeout: 30_000 # Optional: request timeout
)
```
### Authentication
For MCP servers requiring authentication, pass headers with your credentials:
```elixir
# Bearer token authentication
{:ok, conn} = Hermolaos.connect(:http,
url: "https://api.example.com/mcp",
headers: [{"authorization", "Bearer your-jwt-token"}]
)
# API key authentication
{:ok, conn} = Hermolaos.connect(:http,
url: "https://api.example.com/mcp",
headers: [{"x-api-key", "your-api-key"}]
)
# Multiple headers
{:ok, conn} = Hermolaos.connect(:http,
url: "https://api.example.com/mcp",
headers: [
{"authorization", "Bearer token"},
{"x-api-key", "key"},
{"x-custom-header", "value"}
]
)
```
## Connection Pooling
For high-throughput scenarios, use the built-in connection pool:
```elixir
# Start a pool with multiple connections
{:ok, pool} = Hermolaos.Pool.start_link(
name: MyApp.MCPPool,
size: 4,
connection_opts: [
transport: :stdio,
command: "my-server"
],
strategy: :round_robin # or :random, :least_busy
)
# Use checkout/checkin pattern
{:ok, conn} = Hermolaos.Pool.checkout(MyApp.MCPPool)
result = Hermolaos.call_tool(conn, "my_tool", %{})
Hermolaos.Pool.checkin(MyApp.MCPPool, conn)
# Or use transaction for automatic checkin
result = Hermolaos.Pool.transaction(MyApp.MCPPool, fn conn ->
Hermolaos.call_tool(conn, "my_tool", %{})
end)
```
## Notification Handling
Handle server notifications with custom handlers:
```elixir
defmodule MyApp.MCPHandler do
@behaviour Hermolaos.Client.NotificationHandler
@impl true
def handle_notification({:notification, "notifications/tools/list_changed", _}, state) do
IO.puts("Tools list changed!")
{:ok, state}
end
def handle_notification(_event, state), do: {:ok, state}
end
# Use custom handler
{:ok, conn} = Hermolaos.connect(:stdio,
command: "server",
notification_handler: {MyApp.MCPHandler, %{}}
)
```
## Error Handling
Errors are returned as `{:error, %Hermolaos.Error{}}`:
```elixir
case Hermolaos.call_tool(conn, "unknown_tool", %{}) do
{:ok, result} ->
# Handle success
result
{:error, %Hermolaos.Error{code: -32601, message: message}} ->
# Method not found
Logger.error("Tool not found: #{message}")
{:error, %Hermolaos.Error{code: -32001}} ->
# Request timeout
Logger.error("Request timed out")
{:error, error} ->
# Other error
Logger.error("Error: #{inspect(error)}")
end
```
## Example: Playwright Browser Automation
Hermolaos works with browser automation MCP servers like [Playwright MCP](https://github.com/microsoft/playwright-mcp):
```elixir
# Connect to Playwright MCP server
{:ok, conn} = Hermolaos.connect(:stdio,
command: "npx",
args: ["@playwright/mcp@latest"]
)
# Navigate to a page
Hermolaos.call_tool(conn, "browser_navigate", %{"url" => "https://example.com"})
# Get page snapshot (accessibility tree with element refs)
{:ok, snap} = Hermolaos.call_tool(conn, "browser_snapshot", %{})
IO.puts(Hermolaos.get_text(snap))
# Click an element (use ref from snapshot)
Hermolaos.call_tool(conn, "browser_click", %{"element" => "More information", "ref" => "e5"})
# Take a screenshot
{:ok, result} = Hermolaos.call_tool(conn, "browser_take_screenshot", %{})
{:ok, image} = Hermolaos.get_image(result)
File.write!("screenshot.png", image)
# Close and disconnect
Hermolaos.call_tool(conn, "browser_close", %{})
Hermolaos.disconnect(conn)
```
## Architecture
See [docs/architecture.md](docs/architecture.md) for detailed architecture documentation.
## Design Decisions
See [docs/design_decisions.md](docs/design_decisions.md) for rationale behind key design choices.
## Testing
```bash
# Run all tests
mix test
# Run with coverage
mix test --cover
# Run Playwright integration tests (requires Node.js)
mix test --include playwright
```
### Running Playwright Tests in Docker (Headless)
For CI/CD or headless environments, you can run Playwright tests in Docker:
```dockerfile
# Dockerfile.test
FROM mcr.microsoft.com/playwright:v1.40.0-jammy
# Install Erlang and Elixir
RUN apt-get update && apt-get install -y \
erlang \
elixir \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
RUN mix local.hex --force && mix local.rebar --force
RUN mix deps.get
RUN mix compile
# Run tests with Playwright
CMD ["mix", "test", "--include", "playwright"]
```
Or use docker-compose:
```yaml
# docker-compose.test.yml
version: '3.8'
services:
test:
build:
context: .
dockerfile: Dockerfile.test
environment:
- MIX_ENV=test
- PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
```
Run with:
```bash
docker-compose -f docker-compose.test.yml up --build
```
**Note:** The `mcr.microsoft.com/playwright` image includes all browser dependencies pre-installed for headless execution.
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b feature/my-feature`)
3. Commit your changes (`git commit -am 'Add my feature'`)
4. Push to the branch (`git push origin feature/my-feature`)
5. Create a Pull Request
## License
Apache License 2.0 - see [LICENSE](LICENSE) file for details.
## References
- [MCP Specification](https://spec.modelcontextprotocol.io)
- [MCP Documentation](https://modelcontextprotocol.io/docs)
- [JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification)