Error Handling

This guide explains how errors are handled in the Hermes MCP library.

Error Types

Hermes MCP distinguishes between three types of errors:

  1. Protocol errors (JSON-RPC errors): Standard errors defined by the JSON-RPC 2.0 specification
  2. Domain errors: Application-level errors reported by the server with isError: true
  3. Client-side errors: Errors generated by the client before reaching the server

Error Representation

Protocol and client-side errors in Hermes MCP are represented as {:error, %Hermes.MCP.Error{}} tuples, where Error is a struct with:

  • code: The numeric error code, as an integer
  • reason: The atom reason (e.g., :parse_error, :method_not_found)
  • data: Additional error context, as a map

Domain errors (with isError: true) are returned as {:ok, %Hermes.MCP.Response{}} tuples, where the response includes:

  • result: The original result from the server (including the isError field), as a map
  • id: The request ID, generated from Hermes.MCP.ID.generate_request_id/0
  • is_error: A boolean flag indicating if this is a domain error or "success"

Protocol Errors

Protocol errors (JSON-RPC errors) use the standard JSON-RPC error codes with specific reason atoms:

{:error, %Hermes.MCP.Error{
  code: -32601,
  reason: :method_not_found,
  data: %{original_message: "Method not found"}
}}

Standard JSON-RPC error codes:

  • -32700: Parse error (:parse_error)
  • -32600: Invalid request (:invalid_request)
  • -32601: Method not found (:method_not_found)
  • -32602: Invalid params (:invalid_params)
  • -32603: Internal error (:internal_error)
  • -32000: Server error (:server_error)

Domain Errors

Domain errors (application-level errors) are valid JSON-RPC responses with isError: true and are represented as:

{:ok, %Hermes.MCP.Response{
  result: %{
    "isError" => true,
    "reason" => "tool_not_found",
    "message" => "Tool 'unknown' not found",
    "details" => %{"toolName" => "unknown"}
  },
  id: "req_123",
  is_error: true
}}

Client-Side Errors

Client-specific errors follow the same error struct pattern with appropriate reason atoms:

# Transport error
{:error, %Hermes.MCP.Error{
  code: -32000,
  reason: :connection_refused,
  data: %{type: :transport}
}}

# Capability error
{:error, %Hermes.MCP.Error{
  code: -32601,
  reason: :method_not_found,
  data: %{method: "resources/list"}
}}

# Timeout error
{:error, %Hermes.MCP.Error{
  code: -32000,
  reason: :request_timeout,
  data: %{type: :client, message: "Request timed out after 30000ms"}
}}

> **Note:** Each client API call accepts a `:timeout` option which can be used to set a custom timeout for individual operations. The default timeout for all operations is 30 seconds.

Handling Errors

You can pattern match on the response structure to handle different error types:

case Hermes.Client.call_tool("search", %{query: "example"}) do
  {:ok, %Hermes.MCP.Response{is_error: false, result: result}} ->
    # Handle success
    handle_success(result)

  {:ok, %Hermes.MCP.Response{is_error: true, result: result}} ->
    # Handle domain/application error
    case result do
      %{"reason" => "not_found"} ->
        Logger.warning("Resource not found: #{result["message"]}")

      %{"reason" => "permission_denied"} ->
        Logger.error("Permission denied: #{result["message"]}")

      _ ->
        Logger.error("Other domain error: #{inspect(result)}")
    end

  {:error, %Hermes.MCP.Error{reason: :method_not_found}} ->
    # Handle unsupported method
    Logger.warning("Method not supported by this server")

  {:error, %Hermes.MCP.Error{reason: reason}} when reason in [:connection_refused, :timeout] ->
    # Handle transport errors
    Logger.error("Transport error: #{reason}")

  {:error, %Hermes.MCP.Error{} = error} ->
    # Handle any other error
    Logger.error("Unexpected error: #{inspect(error)}")
end

Error Inspection

For better debugging, errors implement a custom Inspect protocol:

#MCP.Error<method_not_found %{method: "unknown_method"}>