Raxol.MCP.Protocol (Raxol MCP v2.8.0)

Copy Markdown View Source

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

error_response()

@type error_response() :: %{
  jsonrpc: String.t(),
  id: pos_integer() | nil,
  error: %{code: integer(), message: String.t()}
}

notification()

@type notification() :: %{jsonrpc: String.t(), method: String.t(), params: map()}

request()

@type request() :: %{
  jsonrpc: String.t(),
  id: pos_integer(),
  method: String.t(),
  params: map()
}

response()

@type response() :: %{jsonrpc: String.t(), id: pos_integer(), result: term()}

Functions

decode(json)

@spec decode(String.t()) :: {:ok, map()} | {:error, term()}

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(message)

@spec encode(map()) :: {:ok, iodata()} | {:error, term()}

Encode a message to a JSON string with newline delimiter.

encode!(message)

@spec encode!(map()) :: iodata()

Encode a message, raising on failure.

Returns iodata (JSON + newline).

error?(arg1)

@spec error?(map()) :: boolean()

Check if a decoded message is an error response.

error_response(id, code, message, data \\ nil)

@spec error_response(pos_integer() | nil, integer(), String.t(), term()) ::
  error_response()

Build a JSON-RPC error response.

internal_error()

@spec internal_error() :: integer()

JSON-RPC internal error code (-32603).

invalid_params()

@spec invalid_params() :: integer()

JSON-RPC invalid params code (-32602).

invalid_request()

@spec invalid_request() :: integer()

JSON-RPC invalid request code (-32600).

legacy_version()

@spec legacy_version() :: String.t()

The newest stateful revision, offered to a legacy-era origin.

mcp_protocol_version()

@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.

method_not_found()

@spec method_not_found() :: integer()

JSON-RPC method not found code (-32601).

modern_version()

@spec modern_version() :: String.t()

The stateless revision: no handshake, no session, server/discover required.

negotiate(version)

@spec negotiate(term()) ::
  {:ok, String.t()} | {:error, {:unsupported_version, term()}}

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.

notification(method, params \\ %{})

@spec notification(String.t(), map()) :: notification()

Build a JSON-RPC notification (no id, no response expected).

notification?(msg)

@spec notification?(map()) :: boolean()

Check if a decoded message is a notification (no id).

parse_error()

@spec parse_error() :: integer()

JSON-RPC parse error code (-32700).

request(id, method, params \\ %{})

@spec request(pos_integer(), String.t(), map()) :: request()

Build a JSON-RPC request.

request?(arg1)

@spec request?(map()) :: boolean()

Check if a decoded message is a request (has id + method).

response(id, result)

@spec response(pos_integer(), term()) :: response()

Build a JSON-RPC success response.

response?(arg1)

@spec response?(map()) :: boolean()

Check if a decoded message is a response (has id + result or error).

supported_versions()

@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.