Anubis.Server.Registry behaviour (anubis_mcp v1.14.0)

Copy Markdown

Behaviour for pluggable session registries and deterministic naming utilities.

The registry is responsible for mapping session IDs to PIDs. Different transports have different needs:

  • STDIO: single session, no registry needed (Registry.None)
  • HTTP: multiple sessions, need lookup by session ID (Registry.Local)

Naming Utilities

The module also provides deterministic atom naming for internal processes (transports, supervisors, task stores). These are keyed off the server module, which is compile-time bounded, so they cannot exhaust the atom table.

Session processes are different: their ids come from the client-controlled mcp-session-id header. resolve_session_name/3 therefore names sessions via an Elixir Registry keyed by the session-id string (a :via tuple) rather than minting one atom per session id.

Summary

Callbacks

Returns the GenServer name for a session. Override this to return a :via tuple (e.g. {:via, Horde.Registry, {name, session_id}}) when using a distributed registry that auto-registers processes on start_link. The default returns a plain atom.

Functions

Default atom name for a server's Streamable HTTP SSE event store process. Adapters may override via the optional resolve_name/2 callback to return a :via tuple for distributed deployments.

Name of the per-server Registry used to name session processes.

Resolves the session GenServer name via the registry adapter.

Deterministic atom name for a session process.

Default atom name for a server's Anubis.Server.TaskStore process. Adapters may override via the optional resolve_name/2 callback to return a :via tuple for distributed deployments.

Types

session_id()

@type session_id() :: String.t()

Callbacks

child_spec(keyword)

@callback child_spec(keyword()) :: Supervisor.child_spec() | :ignore

lookup_session(name, session_id)

@callback lookup_session(name :: term(), session_id()) ::
  {:ok, pid()} | {:error, :not_found}

register_session(name, session_id, pid)

@callback register_session(name :: term(), session_id(), pid()) :: :ok | {:error, term()}

session_name(registry_name, session_id)

(optional)
@callback session_name(registry_name :: term(), session_id()) :: GenServer.name()

Returns the GenServer name for a session. Override this to return a :via tuple (e.g. {:via, Horde.Registry, {name, session_id}}) when using a distributed registry that auto-registers processes on start_link. The default returns a plain atom.

When a :via tuple is returned, register_session/3 should be a no-op since registration happens automatically on process start.

unregister_session(name, session_id)

@callback unregister_session(name :: term(), session_id()) :: :ok

Functions

event_store_name(server)

@spec event_store_name(module()) :: atom()

Default atom name for a server's Streamable HTTP SSE event store process. Adapters may override via the optional resolve_name/2 callback to return a :via tuple for distributed deployments.

Examples

iex> Anubis.Server.Registry.event_store_name(MyApp.Server)
:"Anubis.Elixir.MyApp.Server.event_store"

naming_registry_name(registry_name)

@spec naming_registry_name(atom()) :: atom()

Name of the per-server Registry used to name session processes.

Session ids are client-controlled (the mcp-session-id header), so naming session processes with :"#{registry_name}.session.#{session_id}" would mint a fresh atom per session id. Atoms are never garbage collected, so an attacker feeding distinct session ids could exhaust the atom table and crash the VM.

Instead we route the default naming through an Elixir Registry keyed by the session-id string. registry_name is a compile-time bounded server atom, so deriving this name from it is safe.

registry_name(server)

@spec registry_name(module()) :: atom()

resolve_session_name(registry_mod, registry_name, session_id)

@spec resolve_session_name(module(), term(), session_id()) :: GenServer.name()

Resolves the session GenServer name via the registry adapter.

Falls back to the default :via naming if the adapter does not implement the optional session_name/2 callback.

session_name(server, session_id)

@spec session_name(module(), String.t()) :: atom()

Deterministic atom name for a session process.

Warning

This mints an atom per session_id. Only call it with compile-time bounded or otherwise trusted session ids (e.g. in tests). It must not be used on the request path with client-supplied session ids, since atoms are never garbage collected and an attacker could exhaust the atom table. The runtime session naming path goes through resolve_session_name/3, which returns a :via Registry name keyed by the session-id string instead.

session_supervisor_name(server)

@spec session_supervisor_name(module()) :: atom()

stdio_session_name(server)

@spec stdio_session_name(module()) :: atom()

supervisor_name(server)

@spec supervisor_name(module()) :: atom()

task_store_name(server)

@spec task_store_name(module()) :: atom()

Default atom name for a server's Anubis.Server.TaskStore process. Adapters may override via the optional resolve_name/2 callback to return a :via tuple for distributed deployments.

task_supervisor_name(server)

@spec task_supervisor_name(module()) :: atom()

transport_name(server, type)

@spec transport_name(module(), atom()) :: atom()