Current section

Files

Jump to
hermes_mcp lib hermes mcp error.ex
Raw

lib/hermes/mcp/error.ex

defmodule Hermes.MCP.Error do
@moduledoc """
Represents errors in the MCP protocol.
This module defines standardized error types based on the JSON-RPC 2.0
error codes, with additional MCP-specific error reasons.
## Error Structure
Each error includes:
- `code`: Numeric error code (following JSON-RPC 2.0 conventions)
- `reason`: Atom representing the error type (e.g., `:parse_error`, `:timeout`)
- `data`: Additional context or metadata about the error
## Error Categories
Errors are categorized into:
- Standard JSON-RPC errors (parse errors, invalid requests, etc.)
- Transport errors (connection issues, timeouts)
- Client errors (request handling issues)
- Server errors (capability issues, domain errors)
## Examples
```elixir
# Creating standard RPC errors
Hermes.MCP.Error.parse_error()
# Creating transport errors
Hermes.MCP.Error.transport_error(:connection_refused)
# Creating client errors
Hermes.MCP.Error.client_error(:request_timeout, %{elapsed_ms: 30000})
# Converting from JSON-RPC errors
Hermes.MCP.Error.from_json_rpc(%{"code" => -32700, "message" => "Parse error"})
```
"""
@type t :: %__MODULE__{
code: integer(),
reason: atom(),
data: map()
}
defstruct [:code, :reason, data: %{}]
# Standard JSON-RPC error codes
@parse_error -32_700
@invalid_request -32_600
@method_not_found -32_601
@invalid_params -32_602
@internal_error -32_603
@doc """
Creates a parse error.
Used when the server is unable to parse the JSON received.
## Examples
iex> Hermes.MCP.Error.parse_error()
%Hermes.MCP.Error{code: -32700, reason: :parse_error, data: %{}}
"""
@spec parse_error(map()) :: t()
def parse_error(data \\ %{}) do
%__MODULE__{
code: @parse_error,
reason: :parse_error,
data: data
}
end
@doc """
Creates an invalid request error.
Used when the JSON sent is not a valid request object.
## Examples
iex> Hermes.MCP.Error.invalid_request()
%Hermes.MCP.Error{code: -32600, reason: :invalid_request, data: %{}}
"""
@spec invalid_request(map()) :: t()
def invalid_request(data \\ %{}) do
%__MODULE__{
code: @invalid_request,
reason: :invalid_request,
data: data
}
end
@doc """
Creates a method not found error.
Used when the requested method does not exist or is not available.
## Examples
iex> Hermes.MCP.Error.method_not_found(%{method: "unknown_method"})
%Hermes.MCP.Error{code: -32601, reason: :method_not_found, data: %{method: "unknown_method"}}
"""
@spec method_not_found(map()) :: t()
def method_not_found(data \\ %{}) do
%__MODULE__{
code: @method_not_found,
reason: :method_not_found,
data: data
}
end
@doc """
Creates an invalid params error.
Used when the parameters provided are invalid for the requested method.
## Examples
iex> Hermes.MCP.Error.invalid_params(%{param: "name"})
%Hermes.MCP.Error{code: -32602, reason: :invalid_params, data: %{param: "name"}}
"""
@spec invalid_params(map()) :: t()
def invalid_params(data \\ %{}) do
%__MODULE__{
code: @invalid_params,
reason: :invalid_params,
data: data
}
end
@doc """
Creates an internal error.
Used when an internal server error occurs.
## Examples
iex> Hermes.MCP.Error.internal_error()
%Hermes.MCP.Error{code: -32603, reason: :internal_error, data: %{}}
"""
@spec internal_error(map()) :: t()
def internal_error(data \\ %{}) do
%__MODULE__{
code: @internal_error,
reason: :internal_error,
data: data
}
end
@doc """
Creates a transport-related error.
Used for communication/network errors between client and server.
## Parameters
* `reason` - Atom representing the specific transport error
* `data` - Additional error context or metadata
## Examples
iex> Hermes.MCP.Error.transport_error(:connection_refused)
%Hermes.MCP.Error{code: -32000, reason: :connection_refused, data: %{type: :transport}}
"""
@spec transport_error(atom(), map()) :: t()
def transport_error(reason, data \\ %{}) when is_atom(reason) do
%__MODULE__{
# Server error range
code: -32_000,
reason: reason,
data: Map.put(data, :type, :transport)
}
end
@doc """
Creates a client-related error.
Used for errors that occur within the client implementation.
## Parameters
* `reason` - Atom representing the specific client error
* `data` - Additional error context or metadata
## Examples
iex> Hermes.MCP.Error.client_error(:request_timeout, %{elapsed_ms: 30000})
%Hermes.MCP.Error{code: -32000, reason: :request_timeout, data: %{type: :client, elapsed_ms: 30000}}
"""
@spec client_error(atom(), map()) :: t()
def client_error(reason, data \\ %{}) when is_atom(reason) do
%__MODULE__{
# Server error range
code: -32_000,
reason: reason,
data: Map.put(data, :type, :client)
}
end
@doc """
Creates a domain-level error from an MCP server response.
Used for application-level errors returned as valid JSON-RPC responses with `isError: true`.
## Examples
iex> Hermes.MCP.Error.domain_error(%{"message" => "Resource not found"})
%Hermes.MCP.Error{code: -32000, reason: :domain_error, data: %{"message" => "Resource not found"}}
"""
@spec domain_error(map()) :: t()
def domain_error(data) when is_map(data) do
%__MODULE__{
code: -32_000,
reason: :domain_error,
data: data
}
end
@doc """
Converts from a JSON-RPC error object to a Hermes.MCP.Error struct.
## Parameters
* `error` - A map containing the JSON-RPC error fields
## Examples
iex> Hermes.MCP.Error.from_json_rpc(%{"code" => -32700, "message" => "Parse error"})
%Hermes.MCP.Error{code: -32700, reason: :parse_error, data: %{original_message: "Parse error"}}
"""
@spec from_json_rpc(map()) :: t()
def from_json_rpc(%{"code" => code} = error) do
# Store the original message in data for debugging purposes
message = Map.get(error, "message", "")
data = Map.get(error, "data", %{})
data = if message == "", do: data, else: Map.put(data, :original_message, message)
reason = reason_for_code(code)
%__MODULE__{
code: code,
reason: reason,
data: data
}
end
@doc """
Converts an MCP error to a tuple of the form `{:error, error}`.
This is useful for returning errors in functions that follow the
`{:ok, result} | {:error, reason}` pattern.
## Examples
iex> error = Hermes.MCP.Error.parse_error()
iex> Hermes.MCP.Error.to_tuple(error)
{:error, %Hermes.MCP.Error{code: -32700, reason: :parse_error, data: %{}}}
"""
@spec to_tuple(t()) :: {:error, t()}
def to_tuple(%__MODULE__{} = error), do: {:error, error}
# Private helper functions
# Maps JSON-RPC error codes to Elixir atoms
defp reason_for_code(@parse_error), do: :parse_error
defp reason_for_code(@invalid_request), do: :invalid_request
defp reason_for_code(@method_not_found), do: :method_not_found
defp reason_for_code(@invalid_params), do: :invalid_params
defp reason_for_code(@internal_error), do: :internal_error
defp reason_for_code(_), do: :server_error
end
defimpl Inspect, for: Hermes.MCP.Error do
def inspect(%{reason: reason, data: data}, _opts) do
details = if Enum.empty?(data), do: "", else: " #{Kernel.inspect(data, pretty: true)}"
"#MCP.Error<#{reason}#{details}>"
end
end