# `Hermolaos`
[🔗](https://github.com/nyo16/hermolaos/blob/v0.5.0/lib/hermolaos.ex#L1)

MCP (Model Context Protocol) client for Elixir.

Hermolaos provides a complete implementation of the Model Context Protocol,
enabling Elixir applications to connect to MCP servers and access their
tools, resources, and prompts.

## Quick Start

    # Connect to a local MCP server via stdio
    {:ok, client} = Hermolaos.connect(:stdio,
      command: "npx",
      args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
    )

    # List available tools
    {:ok, tools} = Hermolaos.list_tools(client)

    # Call a tool
    {:ok, result} = Hermolaos.call_tool(client, "read_file", %{"path" => "/tmp/test.txt"})

    # Disconnect when done
    :ok = Hermolaos.disconnect(client)

## Transports

Hermolaos supports two transport mechanisms:

### Stdio Transport

Launches an MCP server as a subprocess and communicates via stdin/stdout.

    {:ok, client} = Hermolaos.connect(:stdio,
      command: "/path/to/server",
      args: ["--arg1", "value"],
      env: [{"DEBUG", "1"}]
    )

### HTTP Transport

Connects to a remote MCP server via HTTP/SSE.

    {:ok, client} = Hermolaos.connect(:http,
      url: "http://localhost:3000/mcp",
      headers: [{"authorization", "Bearer token"}]
    )

## Error Handling

All operations return `{:ok, result}` or `{:error, reason}`. Errors
are typically `Hermolaos.Protocol.Errors` structs with error codes.

    case Hermolaos.call_tool(client, "unknown_tool", %{}) do
      {:ok, result} -> handle_result(result)
      {:error, %Hermolaos.Protocol.Errors{code: -32601}} -> IO.puts("Tool not found")
      {:error, reason} -> IO.puts("Error: #{inspect(reason)}")
    end

## Notification Handling

To receive server notifications, configure a notification handler:

    defmodule MyHandler do
      @behaviour Hermolaos.Client.NotificationHandler

      @impl true
      def handle_notification({:notification, method, params}, state) do
        IO.puts("Got notification: #{method}")
        {:ok, state}
      end
    end

    {:ok, client} = Hermolaos.connect(:stdio,
      command: "my-server",
      notification_handler: {MyHandler, %{}}
    )

# `client`

```elixir
@type client() :: Hermolaos.Client.Connection.t()
```

# `transport`

```elixir
@type transport() :: :stdio | :http
```

# `call_tool`

```elixir
@spec call_tool(client(), String.t(), map(), keyword()) ::
  {:ok, map()} | {:error, term()}
```

Calls a tool with the given arguments.

## Parameters

- `client` - The MCP client
- `name` - Tool name
- `arguments` - Tool arguments map
- `opts` - Options:
  - `:timeout` - Request timeout override

## Returns

- `{:ok, %{content: [...], isError: false}}` - Success (atom keys)
- `{:ok, %{content: [...], isError: true}}` - Tool error
- `{:error, reason}` - Protocol error

## Examples

    {:ok, result} = Hermolaos.call_tool(client, "read_file", %{"path" => "/tmp/test.txt"})

    case result do
      %{isError: false, content: content} ->
        for item <- content do
          case item do
            %{type: "text", text: text} -> IO.puts(text)
            %{type: "image", data: data} -> File.write!("image.png", Base.decode64!(data))
          end
        end

      %{isError: true, content: content} ->
        IO.puts("Tool error: #{inspect(content)}")
    end

# `cancel`

```elixir
@spec cancel(client(), integer() | String.t(), String.t() | nil) ::
  :ok | {:error, term()}
```

Sends a cancellation notification for a pending request.

## Examples

    :ok = Hermolaos.cancel(client, request_id)

# `complete`

```elixir
@spec complete(client(), map(), map(), keyword()) :: {:ok, map()} | {:error, term()}
```

Requests argument completion suggestions.

## Parameters

- `client` - The MCP client
- `ref` - Reference object (prompt or resource)
- `argument` - Argument to complete

## Examples

    ref = %{"type" => "ref/prompt", "name" => "code_review"}
    argument = %{"name" => "language", "value" => "eli"}
    {:ok, completions} = Hermolaos.complete(client, ref, argument)

# `connect`

```elixir
@spec connect(
  transport(),
  keyword()
) :: {:ok, client()} | {:error, term()}
```

Connects to an MCP server.

## Parameters

- `transport` - Either `:stdio` or `:http`
- `opts` - Transport-specific options

## Stdio Options

- `:command` - Command to execute (required)
- `:args` - Command arguments (default: `[]`)
- `:env` - Environment variables as `[{name, value}]` (default: `[]`)
- `:cd` - Working directory (optional)

## HTTP Options

- `:url` - Server endpoint URL (required)
- `:headers` - Additional HTTP headers (default: `[]`)
- `:req_options` - Options passed to Req (default: `[]`)

## Common Options

- `:client_info` - Client identification map (default: Hermolaos info)
- `:capabilities` - Client capabilities (default: standard)
- `:notification_handler` - Module or `{module, state}` for notifications
- `:timeout` - Default request timeout in ms (default: 30000)
- `:name` - GenServer name (optional)

## Examples

    # Stdio with npx
    {:ok, client} = Hermolaos.connect(:stdio,
      command: "npx",
      args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
    )

    # HTTP with authentication
    {:ok, client} = Hermolaos.connect(:http,
      url: "https://api.example.com/mcp",
      headers: [{"authorization", "Bearer token123"}]
    )

    # Named connection
    {:ok, _} = Hermolaos.connect(:stdio,
      command: "my-server",
      name: MyApp.MCPClient
    )
    # Later: Hermolaos.list_tools(MyApp.MCPClient)

# `disconnect`

```elixir
@spec disconnect(client()) :: :ok
```

Disconnects from the MCP server.

This gracefully closes the connection, cleaning up resources.

## Examples

    :ok = Hermolaos.disconnect(client)

# `get_audio`

```elixir
@spec get_audio(map()) :: {:ok, binary()} | :error
```

Extracts audio data from a tool call result.

Returns the base64-decoded binary audio data.

## Examples

    {:ok, result} = Hermolaos.call_tool(client, "text_to_speech", %{"text" => "hello"})
    case Hermolaos.get_audio(result) do
      {:ok, audio_data} -> File.write!("speech.mp3", audio_data)
      :error -> IO.puts("No audio in response")
    end

# `get_audios`

```elixir
@spec get_audios(map()) :: [binary()]
```

Extracts all audio clips from a tool call result.

Returns a list of base64-decoded binary audio data.

# `get_image`

```elixir
@spec get_image(map()) :: {:ok, binary()} | :error
```

Extracts image data from a tool call result.

Returns the base64-decoded binary image data.

## Examples

    {:ok, result} = Hermolaos.call_tool(client, "browser_take_screenshot", %{})
    case Hermolaos.get_image(result) do
      {:ok, image_data} -> File.write!("screenshot.png", image_data)
      :error -> IO.puts("No image in response")
    end

# `get_images`

```elixir
@spec get_images(map()) :: [binary()]
```

Extracts all images from a tool call result.

Returns a list of base64-decoded binary image data.

## Examples

    {:ok, result} = Hermolaos.call_tool(client, "get_images", %{})
    images = Hermolaos.get_images(result)
    Enum.with_index(images, fn data, i ->
      File.write!("image_#{i}.png", data)
    end)

# `get_prompt`

```elixir
@spec get_prompt(client(), String.t(), map(), keyword()) ::
  {:ok, map()} | {:error, term()}
```

Gets a prompt by name, optionally with argument values.

## Parameters

- `client` - The MCP client
- `name` - Prompt name
- `arguments` - Argument values map (default: `%{}`)
- `opts` - Options

## Examples

    {:ok, prompt} = Hermolaos.get_prompt(client, "code_review")
    {:ok, prompt} = Hermolaos.get_prompt(client, "summarize", %{"language" => "elixir"})

# `get_resource_links`

```elixir
@spec get_resource_links(map()) :: [map()]
```

Extracts resource links from a tool call result.

Returns a list of resource link maps with `:uri` and optional `:name`, `:mimeType`.

# `get_structured_content`

```elixir
@spec get_structured_content(map()) :: {:ok, map()} | :error
```

Extracts structured content from a tool call result.

Returns the `structuredContent` map if present, which contains typed data
matching the tool's `outputSchema`.

## Examples

    {:ok, result} = Hermolaos.call_tool(client, "structured_tool", %{})
    case Hermolaos.get_structured_content(result) do
      {:ok, data} -> IO.inspect(data)
      :error -> IO.puts("No structured content")
    end

# `get_text`

```elixir
@spec get_text(map()) :: String.t() | nil
```

Extracts text content from a tool call result.

Tool results contain a list of content items. This helper extracts
all text items and concatenates them.

## Examples

    {:ok, result} = Hermolaos.call_tool(client, "browser_snapshot", %{})
    text = Hermolaos.get_text(result)

# `instructions`

```elixir
@spec instructions(client()) ::
  {:ok, String.t()} | {:error, :not_initialized | :no_instructions}
```

Gets server instructions from the initialization response.

Instructions describe how to use the server and its capabilities.

## Examples

    case Hermolaos.instructions(client) do
      {:ok, instructions} -> IO.puts(instructions)
      {:error, :no_instructions} -> IO.puts("No instructions provided")
    end

# `list_prompts`

```elixir
@spec list_prompts(
  client(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Lists available prompts on the server.

## Options

- `:cursor` - Pagination cursor
- `:timeout` - Request timeout override

## Examples

    {:ok, %{prompts: prompts}} = Hermolaos.list_prompts(client)

# `list_resource_templates`

```elixir
@spec list_resource_templates(
  client(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Lists resource templates on the server.

## Options

- `:cursor` - Pagination cursor
- `:timeout` - Request timeout override

## Examples

    {:ok, %{resourceTemplates: templates}} = Hermolaos.list_resource_templates(client)

# `list_resources`

```elixir
@spec list_resources(
  client(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Lists available resources on the server.

## Options

- `:cursor` - Pagination cursor
- `:timeout` - Request timeout override

## Examples

    {:ok, %{resources: resources}} = Hermolaos.list_resources(client)

# `list_tools`

```elixir
@spec list_tools(
  client(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Lists available tools on the server.

## Options

- `:cursor` - Pagination cursor for subsequent requests
- `:timeout` - Request timeout override

## Returns

- `{:ok, %{tools: [...], nextCursor: ...}}` - Success (atom keys)
- `{:error, reason}` - Error

## Examples

    {:ok, %{tools: tools}} = Hermolaos.list_tools(client)
    for tool <- tools do
      IO.puts("Tool: #{tool.name} - #{tool.description}")
    end

# `ping`

```elixir
@spec ping(
  client(),
  keyword()
) :: {:ok, map()} | {:error, term()}
```

Sends a ping to check server liveness.

## Examples

    {:ok, %{}} = Hermolaos.ping(client)

# `read_resource`

```elixir
@spec read_resource(client(), String.t(), keyword()) ::
  {:ok, map()} | {:error, term()}
```

Reads a resource by URI.

## Parameters

- `client` - The MCP client
- `uri` - Resource URI
- `opts` - Options

## Returns

- `{:ok, %{contents: [...]}}` - Success (atom keys)

## Examples

    {:ok, %{contents: contents}} = Hermolaos.read_resource(client, "file:///project/README.md")
    for content <- contents do
      IO.puts(content.text)
    end

# `server_capabilities`

```elixir
@spec server_capabilities(client()) :: {:ok, map()} | {:error, :not_initialized}
```

Gets server capabilities from the initialization response.

## Examples

    {:ok, caps} = Hermolaos.server_capabilities(client)
    if Hermolaos.Protocol.Capabilities.supports?(caps, :tools) do
      # Server supports tools
    end

# `server_info`

```elixir
@spec server_info(client()) :: {:ok, map()} | {:error, :not_initialized}
```

Gets server information from the initialization response.

## Examples

    {:ok, %{"name" => "MyServer", "version" => "1.0.0"}} = Hermolaos.server_info(client)

# `set_log_level`

```elixir
@spec set_log_level(client(), String.t()) :: {:ok, map()} | {:error, term()}
```

Sets the server's logging level.

## Parameters

- `client` - The MCP client
- `level` - Log level (debug, info, notice, warning, error, critical, alert, emergency)

## Examples

    {:ok, _} = Hermolaos.set_log_level(client, "debug")

# `status`

```elixir
@spec status(client()) :: Hermolaos.Client.Connection.status()
```

Gets the current connection status.

## Returns

- `:disconnected` - Not connected
- `:connecting` - Transport starting
- `:initializing` - MCP handshake in progress
- `:ready` - Ready for requests

## Examples

    :ready = Hermolaos.status(client)

# `subscribe_resource`

```elixir
@spec subscribe_resource(client(), String.t()) :: {:ok, map()} | {:error, term()}
```

Subscribes to resource updates.

Requires server to support resource subscriptions.

## Examples

    :ok = Hermolaos.subscribe_resource(client, "file:///project/src/main.rs")

# `unsubscribe_resource`

```elixir
@spec unsubscribe_resource(client(), String.t()) :: {:ok, map()} | {:error, term()}
```

Unsubscribes from resource updates.

## Examples

    :ok = Hermolaos.unsubscribe_resource(client, "file:///project/src/main.rs")

---

*Consult [api-reference.md](api-reference.md) for complete listing*
