# `Raxol.MCP.Protocol`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.8.0/lib/raxol/mcp/protocol.ex#L1)

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.

# `error_response`

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

# `notification`

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

# `request`

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

# `response`

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

# `decode`

```elixir
@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`

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

Encode a message to a JSON string with newline delimiter.

# `encode!`

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

Encode a message, raising on failure.

Returns iodata (JSON + newline).

# `error?`

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

Check if a decoded message is an error response.

# `error_response`

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

Build a JSON-RPC error response.

# `internal_error`

```elixir
@spec internal_error() :: integer()
```

JSON-RPC internal error code (-32603).

# `invalid_params`

```elixir
@spec invalid_params() :: integer()
```

JSON-RPC invalid params code (-32602).

# `invalid_request`

```elixir
@spec invalid_request() :: integer()
```

JSON-RPC invalid request code (-32600).

# `legacy_version`

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

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

# `mcp_protocol_version`

```elixir
@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`

```elixir
@spec method_not_found() :: integer()
```

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

# `modern_version`

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

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

# `negotiate`

```elixir
@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`

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

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

# `notification?`

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

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

# `parse_error`

```elixir
@spec parse_error() :: integer()
```

JSON-RPC parse error code (-32700).

# `request`

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

Build a JSON-RPC request.

# `request?`

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

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

# `response`

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

Build a JSON-RPC success response.

# `response?`

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

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

# `supported_versions`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
