JSON-RPC 2.0 message encoding/decoding for the Model Context Protocol.
Handles both client-side (requests, notifications) and server-side (responses, error responses) message construction. All MCP communication flows through this module.
Summary
Functions
Decode a JSON string into a message map with atom keys for known fields.
Encode a message to a JSON string with newline delimiter.
Encode a message, raising on failure.
Check if a decoded message is an error response.
Build a JSON-RPC error response.
JSON-RPC internal error code (-32603).
JSON-RPC invalid params code (-32602).
JSON-RPC invalid request code (-32600).
The newest stateful revision, offered to a legacy-era origin.
The protocol version this package's SERVER advertises.
JSON-RPC method not found code (-32601).
The stateless revision: no handshake, no session, server/discover required.
Accept a peer's protocol revision, or name it as unsupported.
Build a JSON-RPC notification (no id, no response expected).
Check if a decoded message is a notification (no id).
JSON-RPC parse error code (-32700).
Build a JSON-RPC request.
Check if a decoded message is a request (has id + method).
Build a JSON-RPC success response.
Check if a decoded message is a response (has id + result or error).
Every revision the client side can speak, newest first.
Types
@type error_response() :: %{ jsonrpc: String.t(), id: pos_integer() | nil, error: %{code: integer(), message: String.t()} }
@type request() :: %{ jsonrpc: String.t(), id: pos_integer(), method: String.t(), params: map() }
@type response() :: %{jsonrpc: String.t(), id: pos_integer(), result: term()}
Functions
Decode a JSON string into a message map with atom keys for known fields.
A decode that is not a JSON OBJECT is {:error, {:unsupported_message, kind}}
rather than an exception. Jason.decode/1 answers any JSON value, and
normalize/1 fed one straight to Map.pop/2: a [{"id":1}] batch response
-- legal JSON-RPC 2.0 -- a bare 123, "hello" or true each raised
BadMapError inside whatever process read the line. For the client that is
handle_continue(:connect) (through the era probe's jsonrpc_error/1) and
handle_manager_info/2, so a payload from an arbitrary HTTPS origin or an
SSE frame took the client down its start_link link to the loader -- the
outcome client.ex keeps every connect failure out of {:stop, _} to
prevent.
Encode a message to a JSON string with newline delimiter.
Encode a message, raising on failure.
Returns iodata (JSON + newline).
Check if a decoded message is an error response.
@spec error_response(pos_integer() | nil, integer(), String.t(), term()) :: error_response()
Build a JSON-RPC error response.
@spec internal_error() :: integer()
JSON-RPC internal error code (-32603).
@spec invalid_params() :: integer()
JSON-RPC invalid params code (-32602).
@spec invalid_request() :: integer()
JSON-RPC invalid request code (-32600).
@spec legacy_version() :: String.t()
The newest stateful revision, offered to a legacy-era origin.
@spec mcp_protocol_version() :: String.t()
The protocol version this package's SERVER advertises.
Unchanged at 2024-11-05, deliberately. ADR-0037 negotiates a version per
client CONNECTION and lists what the raxol server advertises under what it
does not decide, so the two move independently.
@spec method_not_found() :: integer()
JSON-RPC method not found code (-32601).
@spec modern_version() :: String.t()
The stateless revision: no handshake, no session, server/discover required.
Accept a peer's protocol revision, or name it as unsupported.
The per-connection half of ADR-0037 decision 2: the revision in force is
whatever the peer answered initialize with, not a module constant.
@spec notification(String.t(), map()) :: notification()
Build a JSON-RPC notification (no id, no response expected).
Check if a decoded message is a notification (no id).
@spec parse_error() :: integer()
JSON-RPC parse error code (-32700).
@spec request(pos_integer(), String.t(), map()) :: request()
Build a JSON-RPC request.
Check if a decoded message is a request (has id + method).
@spec response(pos_integer(), term()) :: response()
Build a JSON-RPC success response.
Check if a decoded message is a response (has id + result or error).
@spec supported_versions() :: [String.t()]
Every revision the client side can speak, newest first.
Three revisions rather than one because the upstreams disagree: the current
specification (2026-07-28) is stateless, with no initialize and no
Mcp-Session-Id, while the servers ADR-0033 measured on 2026-08-31 are
stateful. A client that speaks only one of the two fails against the other,
which is why Raxol.MCP.Client.Transport.Http probes an origin's era rather
than assuming it.