Current section

Files

Jump to
ragex lib ragex mcp protocol.ex
Raw

lib/ragex/mcp/protocol.ex

defmodule Ragex.MCP.Protocol do
@moduledoc """
Implements the Model Context Protocol (MCP) JSON-RPC 2.0 protocol.
Handles encoding/decoding of MCP messages and protocol-level validation.
"""
@type method :: String.t()
@type params :: map() | list() | nil
@type id :: String.t() | integer() | nil
@type request :: %{
jsonrpc: String.t(),
method: method(),
params: params(),
id: id()
}
@type response :: %{
jsonrpc: String.t(),
result: any(),
id: id()
}
@type error_response :: %{
jsonrpc: String.t(),
error: %{
code: integer(),
message: String.t(),
data: any()
},
id: id()
}
@type notification :: %{
jsonrpc: String.t(),
method: method(),
params: params()
}
# 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 """
Decodes a JSON-RPC message from a string.
"""
@spec decode(String.t()) :: {:ok, request() | notification()} | {:error, term()}
def decode(json_string) do
message = :json.decode(json_string)
case message do
%{"jsonrpc" => "2.0"} ->
{:ok, message}
_ ->
{:error, :invalid_jsonrpc_version}
end
rescue
e -> {:error, {:parse_error, e}}
end
@doc """
Encodes a response to JSON string.
"""
@spec encode(response() | error_response() | notification()) ::
{:ok, String.t()} | {:error, term()}
def encode(message) do
json = :json.encode(message) |> IO.iodata_to_binary()
{:ok, json}
rescue
e -> {:error, e}
end
@doc """
Creates a successful response message.
"""
@spec success_response(any(), id()) :: response()
def success_response(result, id) do
%{
jsonrpc: "2.0",
result: result,
id: id
}
end
@doc """
Creates an error response message.
"""
@spec error_response(integer(), String.t(), any(), id()) :: error_response()
def error_response(code, message, data \\ nil, id) do
error = %{
code: code,
message: message
}
error = if data, do: Map.put(error, :data, data), else: error
%{
jsonrpc: "2.0",
error: error,
id: id
}
end
@doc """
Creates a notification message.
"""
@spec notification(method(), params()) :: notification()
def notification(method, params \\ nil) do
message = %{
jsonrpc: "2.0",
method: method
}
if params, do: Map.put(message, :params, params), else: message
end
@doc """
Standard error codes and helpers.
"""
def parse_error(id), do: error_response(@parse_error, "Parse error", nil, id)
def invalid_request(id), do: error_response(@invalid_request, "Invalid request", nil, id)
def method_not_found(method, id),
do: error_response(@method_not_found, "Method not found: #{method}", nil, id)
def invalid_params(message, id),
do: error_response(@invalid_params, "Invalid params: #{message}", nil, id)
def internal_error(message, id),
do: error_response(@internal_error, "Internal error: #{message}", nil, id)
@doc """
Validates if a message is a request (has an id).
"""
@spec request?(map()) :: boolean()
def request?(%{"id" => _}), do: true
def request?(_), do: false
@doc """
Validates if a message is a notification (no id).
"""
@spec notification?(map()) :: boolean()
def notification?(message), do: not request?(message)
end