# `ConduitMcp.MRTR.State`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/mrtr/state.ex#L1)

Seals and opens the MRTR `requestState` token.

The spec requires servers to treat `requestState` as attacker-controlled.
`seal/3` encrypts the handler's state term with AES-256-GCM (a fresh 96-bit
IV per seal) and binds it to the request that produced it; `open/3`
returns the term only when every binding still holds.

## Configuration

MRTR requires a keyring:

    config :conduit_mcp, mrtr_keys: [{"2026-10", key32}, {"2026-07", old_key32}]

Each entry is `{kid, key}`: `kid` is a non-empty binary without `"."`, `key`
exactly 32 random bytes (e.g. `:crypto.strong_rand_bytes(32)`, loaded from a
secret at runtime). `seal/3` always uses the **first** key; `open/3` selects
the key named by the token's `kid`. To rotate, put the new key first and
keep the old one second until in-flight tokens (at most 5 minutes old) have
expired, then remove it. Rotate well before 2³² seals per key, the GCM
random-IV bound. Keys are read at runtime; when configured they are
validated at application boot (`validate_keys!/1`). Without `:mrtr_keys`,
`seal/3` raises and `open/3` returns `:error`.

## Bindings

The binding passed to both calls is
`%{version: v, method: m, params: p, principal: id, server: module}`:

  * **AAD** — the protocol `version`, `method`, `server` (the Server or
    Endpoint module that handles the request), and `kid`. `:mrtr_keys` is
    application-global, so without `server` a token sealed by one endpoint
    would open at another endpoint in the same node for the same tool,
    arguments and principal.
  * **Payload** — the `principal` id (use `ConduitMcp.Principal.id/1`, the
    claim-prefixed id, or `nil` when unauthenticated), an expiry (5 minutes,
    60 seconds when `principal` is nil), a SHA-256 digest of `method` and
    the *salient* params, and the state term.

Salient params are the request `params` minus `"_meta"`,
`"inputResponses"`, and `"requestState"` — the fields that legitimately
change between rounds — canonicalized as JSON with sorted object keys. A
token presented on a different tool, arguments, URI, or prompt is rejected.

Replay inside the expiry window by the same principal on the same request
is bounded, not prevented; single-use redemption needs server-side storage.

## Token format

`kid <> "." <> Base.url_encode64(iv <> tag <> ciphertext, padding: false)`.
Tokens longer than 64 KiB are rejected before any decoding. The payload is
JSON; `:erlang.binary_to_term/1` is never used.

## State terms

The state must be JSON-encodable: `nil`, booleans, numbers, UTF-8 strings,
atoms (encoded as strings), lists, and maps with string or atom keys.
Tuples, pids, references, funs, ports, and non-UTF-8 binaries raise
`ArgumentError` at seal time. `open/3` returns the decoded JSON, so atom
keys and values come back as strings.

# `binding`

```elixir
@type binding() :: %{
  version: String.t(),
  method: String.t(),
  params: map() | nil,
  principal: String.t() | nil,
  server: module()
}
```

What a token is bound to; `principal` is a derived principal id or nil.

# `open`

```elixir
@spec open(term(), binding(), keyword()) :: {:ok, term()} | :error
```

Opens a token produced by `seal/3`, returning `{:ok, state}`.

Returns `:error` — the same value whatever the cause — for an oversize,
truncated, malformed or tampered token, an unknown `kid`, a different
principal, server module, version, method or salient params, or an expired token.

Options: `:now`, as in `seal/3`.

# `seal`

```elixir
@spec seal(term(), binding(), keyword()) :: String.t()
```

Seals `state` into a `requestState` token bound to `binding`.

Options: `:now` — current time in milliseconds since the epoch (defaults to
`System.system_time(:millisecond)`).

Raises `ArgumentError` when `state` is not JSON-encodable or `:mrtr_keys` is
not configured.

# `validate_keys!`

```elixir
@spec validate_keys!(term()) :: :ok
```

Validates a `:mrtr_keys` keyring, raising `ArgumentError` naming the config
key (never the key bytes) when it is malformed.

`ConduitMcp.Application` calls this at boot when `:mrtr_keys` is set.

---

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