AttestoMCP.Server.SessionStore behaviour (attesto_mcp_server v2.8.0)

Copy Markdown View Source

Durable session storage contract for the session-bound MCP revisions.

Keys are {namespace, session_id} data tuples. Adapters must never convert either component to an atom. update/3, update_ttl/3, and cleanup_expired/1 must be atomic with respect to one another. Records are the versioned JSON-compatible maps documented by AttestoMCP.Server.Session.to_record/1; unknown fields must be preserved by an adapter and ignored by the server. Records with a future format_version are opaque to an older node and must remain untouched by ordinary loads, updates, touches, and expiry cleanup. A preserved future record returned by update/3 or update_ttl/3 is reported as {:ok, opaque_record} so callers can distinguish it from an absent row. update_ttl/3 must delete an already expired record with a version understood by the adapter and return :not_found rather than renewing it. On success it must return the updated record. If the supplied timestamp is older than the record's current "last_seen_ms", adapters must retain the newer value; a successful touch must never rewind session activity. Adapters may implement count_active/1 or namespace-aware count_active/2 to let server stats count sessions without materializing bounded record pages; otherwise the server falls back to list_active/1. Adapters should implement count_active_for_principal/2 or its namespace-aware arity-three form so the server can enforce its per-principal session limit without materializing all active records. Shared adapters should also implement save_below_principal_limit/4 as one atomic count-and-save operation so concurrent server processes cannot exceed that limit. They may also implement list_active_keys/4 to provide bounded, cursor-based operator visibility without loading principal bindings or complete session records. The returned keys must be scoped to the requested namespace, ordered by session ID in ascending binary/UTF-8 order, and accompanied by the last returned ID as next_cursor only when more keys remain. For externally supplied keys, load/2, update/3, and update_ttl/3 should treat malformed or unrepresentable keys as absent, while delete/2 should remain idempotent; valid keys from another configured namespace must still return the namespace-mismatch error. cleanup_expired/1 must return no more than 1,000 valid keys per call; adapters with more expired records must clean them in bounded batches across later calls.

Summary

Types

A configured adapter module and its opaque store handle.

Types

active_key_page()

@type active_key_page() :: %{keys: [key()], next_cursor: String.t() | nil}

adapter()

@type adapter() :: {module(), store()}

A configured adapter module and its opaque store handle.

key()

@type key() :: {String.t(), String.t()}

session_record()

@type session_record() :: map()

store()

@type store() :: term()

update_fun()

@type update_fun() :: (session_record() ->
                   {:ok, session_record()} | :delete | {:error, term()})

Callbacks

cleanup_expired(store)

@callback cleanup_expired(store()) :: {:ok, [key()]} | {:error, term()}

count_active(store)

(optional)
@callback count_active(store()) :: {:ok, non_neg_integer()} | {:error, term()}

count_active(store, t)

(optional)
@callback count_active(store(), String.t()) :: {:ok, non_neg_integer()} | {:error, term()}

count_active_for_principal(store, term)

(optional)
@callback count_active_for_principal(store(), term()) ::
  {:ok, non_neg_integer()} | {:error, term()}

count_active_for_principal(store, t, term)

(optional)
@callback count_active_for_principal(store(), String.t(), term()) ::
  {:ok, non_neg_integer()} | {:error, term()}

delete(store, key)

@callback delete(store(), key()) :: :ok | {:error, term()}

list_active(store)

@callback list_active(store()) :: {:ok, [{key(), session_record()}]} | {:error, term()}

list_active_keys(store, t, arg3, pos_integer)

(optional)
@callback list_active_keys(store(), String.t(), String.t() | nil, pos_integer()) ::
  {:ok, active_key_page()} | {:error, term()}

load(store, key)

@callback load(store(), key()) :: {:ok, session_record()} | :not_found | {:error, term()}

save(store, key, session_record)

@callback save(store(), key(), session_record()) :: :ok | {:error, term()}

save_below_principal_limit(store, key, session_record, pos_integer)

(optional)
@callback save_below_principal_limit(store(), key(), session_record(), pos_integer()) ::
  :ok | :limit_reached | {:error, term()}

update(store, key, update_fun)

@callback update(store(), key(), update_fun()) ::
  {:ok, session_record()} | :not_found | {:error, term()}

update_ttl(store, key, non_neg_integer)

@callback update_ttl(store(), key(), non_neg_integer()) ::
  {:ok, session_record()} | :not_found | {:error, term()}