Anubis.Server.RequestState (anubis_mcp v2.2.0)

Copy Markdown

The integrity-protected requestState of multi round-trip requests.

A handler that returns an Anubis.Server.InputRequired may attach any Erlang term as state. The client echoes the resulting string on its retry, so the specification treats it as attacker-controlled input: a server whose state influences what it does must protect its integrity and reject state that fails verification.

This module signs the term with HMAC-SHA256 and binds it to:

  • the authenticated principal (Anubis.Server.Frame.subject/1), so state issued to one user is refused for another;
  • the server module that issued it, so state cannot cross to another server sharing the application's secret;
  • the originating request, its method plus the tool or prompt name or the resource URI, so state cannot move to a different request;
  • a digest of that request's parameters (request_digest on the frame's context), so a retry cannot keep the state and change the arguments;
  • an expiry, :request_state_ttl milliseconds after it was issued (default: ten minutes).

A server that authenticates outside Anubis.Server.Authorization has no subject on the frame, and should carry its own principal inside the term and check it on retry.

These measures bound replay; they do not make a state single-use. A server that must consume a state at most once enforces that itself.

Configuration

config :anubis_mcp, request_state_secret: System.fetch_env!("MCP_REQUEST_STATE_SECRET")

The secret must be a binary of at least 32 bytes. Signing without one raises; verifying without one refuses the state as :invalid.

Summary

Functions

Signs term for the request binding identifies, as the principal on frame.

Verifies a state the client echoed and returns the term it carries.

Types

binding()

@type binding() :: {server :: module(), method :: String.t(), target :: String.t()}

Functions

sign(term, frame, binding)

@spec sign(term(), Anubis.Server.Frame.t(), binding()) :: String.t()

Signs term for the request binding identifies, as the principal on frame.

verify(token, frame, binding)

@spec verify(String.t(), Anubis.Server.Frame.t(), binding()) ::
  {:ok, term()} | {:error, :invalid | :mismatch | :expired}

Verifies a state the client echoed and returns the term it carries.

Fails with :invalid when the string is malformed or its signature does not match, :mismatch when it was issued to another principal or request, and :expired once its TTL has lapsed.