Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions lib/anubis/mcp/error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ defmodule Anubis.MCP.Error do
- **Resource Errors**: MCP-specific resource handling errors
- **Execution Errors**: Tool and operation execution failures

## Error code ranges

MCP partitions the JSON-RPC implementation-defined range: `-32000` to
`-32019` holds codes allocated before the policy existed (such as `-32002`
for a missing resource, and the generic `-32000` used by transport and
execution errors), while `-32020` to `-32099` is reserved for codes the
specification defines.

## Examples

# Protocol errors
Expand All @@ -25,6 +33,10 @@ defmodule Anubis.MCP.Error do
# Resource errors
Anubis.MCP.Error.resource(:not_found, %{uri: "file:///missing.txt"})

# Reserved errors whose payload the specification pins down
Anubis.MCP.Error.unsupported_protocol_version("1900-01-01", ["2026-07-28"])
Anubis.MCP.Error.missing_required_client_capability(%{"elicitation" => %{}})

# Execution errors with custom messages
Anubis.MCP.Error.execution("Database connection failed", %{retries: 3})

Expand Down Expand Up @@ -54,6 +66,10 @@ defmodule Anubis.MCP.Error do
# MCP-specific error codes
@resource_not_found -32_002

@header_mismatch -32_020
@missing_required_client_capability -32_021
@unsupported_protocol_version -32_022

# Generic server error code for custom errors
@server_error -32_000

Expand All @@ -65,6 +81,9 @@ defmodule Anubis.MCP.Error do
invalid_params: "Invalid params",
internal_error: "Internal error",
resource_not_found: "Resource not found",
header_mismatch: "Header mismatch",
missing_required_client_capability: "Missing required client capability",
unsupported_protocol_version: "Unsupported protocol version",
server_error: "Server error"
}

Expand All @@ -80,6 +99,11 @@ defmodule Anubis.MCP.Error do
- `:method_not_found` - The method does not exist
- `:invalid_params` - Invalid method parameters
- `:internal_error` - Internal JSON-RPC error
- `:header_mismatch` - Transport headers disagree with the request body
- `:missing_required_client_capability` - The request needs a capability the
client did not declare
- `:unsupported_protocol_version` - The requested protocol version is not
implemented

## Examples

Expand All @@ -88,6 +112,9 @@ defmodule Anubis.MCP.Error do

iex> Anubis.MCP.Error.protocol(:method_not_found, %{method: "foo"})
%Anubis.MCP.Error{code: -32601, reason: :method_not_found, message: "Method not found", data: %{method: "foo"}}

iex> Anubis.MCP.Error.protocol(:unsupported_protocol_version, %{supported: ["2026-07-28"], requested: "1900-01-01"})
%Anubis.MCP.Error{code: -32022, reason: :unsupported_protocol_version, message: "Unsupported protocol version", data: %{supported: ["2026-07-28"], requested: "1900-01-01"}}
"""
@spec protocol(atom(), map()) :: t()
def protocol(reason, data \\ %{})
Expand Down Expand Up @@ -137,6 +164,33 @@ defmodule Anubis.MCP.Error do
}
end

def protocol(:header_mismatch, data) do
%__MODULE__{
code: @header_mismatch,
reason: :header_mismatch,
message: @error_messages.header_mismatch,
data: data
}
end

def protocol(:missing_required_client_capability, data) do
%__MODULE__{
code: @missing_required_client_capability,
reason: :missing_required_client_capability,
message: @error_messages.missing_required_client_capability,
data: data
}
end

def protocol(:unsupported_protocol_version, data) do
%__MODULE__{
code: @unsupported_protocol_version,
reason: :unsupported_protocol_version,
message: @error_messages.unsupported_protocol_version,
data: data
}
end

@doc """
Creates a transport-level error.

Expand All @@ -162,6 +216,41 @@ defmodule Anubis.MCP.Error do
}
end

@doc """
Creates an `UnsupportedProtocolVersion` error for a version this peer does
not implement.

The specification requires the `data` payload to carry both the versions
this peer supports and the version that was requested, so prefer this over
`protocol/2` to build the payload for you.

## Examples

iex> Anubis.MCP.Error.unsupported_protocol_version("1900-01-01", ["2026-07-28"])
%Anubis.MCP.Error{code: -32022, reason: :unsupported_protocol_version, message: "Unsupported protocol version", data: %{supported: ["2026-07-28"], requested: "1900-01-01"}}
"""
@spec unsupported_protocol_version(String.t(), [String.t()]) :: t()
def unsupported_protocol_version(requested, supported) when is_binary(requested) and is_list(supported) do
protocol(:unsupported_protocol_version, %{supported: supported, requested: requested})
end

@doc """
Creates a `MissingRequiredClientCapability` error for a request that needs a
capability the client did not declare.

`capabilities` is a `ClientCapabilities` map — the capabilities required to
process the request, not a list of their names.

## Examples

iex> Anubis.MCP.Error.missing_required_client_capability(%{"elicitation" => %{}})
%Anubis.MCP.Error{code: -32021, reason: :missing_required_client_capability, message: "Missing required client capability", data: %{requiredCapabilities: %{"elicitation" => %{}}}}
"""
@spec missing_required_client_capability(map()) :: t()
def missing_required_client_capability(capabilities) when is_map(capabilities) do
protocol(:missing_required_client_capability, %{requiredCapabilities: capabilities})
end

@doc """
Creates a resource-specific error.

Expand Down Expand Up @@ -296,6 +385,9 @@ defmodule Anubis.MCP.Error do
defp reason_from_code(@invalid_params), do: :invalid_params
defp reason_from_code(@internal_error), do: :internal_error
defp reason_from_code(@resource_not_found), do: :resource_not_found
defp reason_from_code(@header_mismatch), do: :header_mismatch
defp reason_from_code(@missing_required_client_capability), do: :missing_required_client_capability
defp reason_from_code(@unsupported_protocol_version), do: :unsupported_protocol_version
defp reason_from_code(_), do: :server_error

defp default_message(reason) do
Expand Down
33 changes: 29 additions & 4 deletions lib/anubis/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ defmodule Anubis.Protocol do

@type version :: String.t()
@type feature :: atom()
@type era :: Anubis.Protocol.Behaviour.era()

@doc """
Returns all supported protocol versions.
Expand All @@ -25,29 +26,53 @@ defmodule Anubis.Protocol do
defdelegate supported_versions(), to: Registry

@doc """
Returns the latest supported protocol version.
Returns the supported protocol versions belonging to an era.
"""
@spec supported_versions(era()) :: [version()]
defdelegate supported_versions(era), to: Registry, as: :versions_for_era

@doc """
Returns the era a protocol version belongs to.
"""
@spec era(version()) :: {:ok, era()} | :error
defdelegate era(version), to: Registry

@doc """
Returns the latest protocol version reachable through the `initialize`
handshake.
"""
@spec latest_version() :: version()
defdelegate latest_version(), to: Registry

@doc """
Returns the latest supported protocol version of an era.
"""
@spec latest_version(era()) :: version() | nil
defdelegate latest_version(era), to: Registry

@doc """
Returns the fallback protocol version for compatibility.
"""
@spec fallback_version() :: version()
defdelegate fallback_version(), to: Registry

@doc """
Validates if a protocol version is supported.
Validates that a protocol version can be negotiated with the `initialize`
handshake.

Only `:legacy` versions qualify. A `:stateless` version is registered and
valid, but it is not reachable through the handshake, so accepting it here
would report success for a connection that cannot be established.
"""
@spec validate_version(version()) :: :ok | {:error, Error.t()}
def validate_version(version) do
if Registry.supported?(version) do
if era(version) == {:ok, :legacy} do
:ok
else
{:error,
Error.protocol(:invalid_params, %{
version: version,
supported: supported_versions()
supported: supported_versions(:legacy)
})}
end
end
Expand Down
Loading