Horde.Registry-backed session registry for the Anubis MCP transports.
Why not Anubis.Server.Registry.PG
The bundled Anubis.Server.Registry.PG adapter does not implement the
optional session_name/2 callback. When the transport resolves a session
name, the fallback builds a dynamic atom from the client-supplied
mcp-session-id:
:"#{registry_name}.session.#{session_id}"Atoms are never garbage collected, so a client that walks distinct
mcp-session-id values can exhaust the BEAM atom table and crash the node - a
remote, unauthenticated denial of service. :pg also gives no cluster-wide
name uniqueness, only process-group membership.
This adapter implements session_name/2, returning a :via tuple:
{:via, Horde.Registry, {registry_name, session_id}}Because Anubis passes that name to the session GenServer.start_link/3, the
session auto-registers in the CRDT-backed Horde.Registry on start. The
session_id is kept as ETS/CRDT data, never converted to an atom, so the
atom-exhaustion vector is closed. Horde gives cluster-wide name uniqueness and
resolves a registered name to the owning (possibly remote) pid, so a request
that lands on any node routes transparently to the node holding the session.
Anubis starts the session DynamicSupervisor itself, so this adapter only
starts the Horde.Registry; cross-node request routing is handled by
distributed Erlang GenServer.call/3 against the pid that
Horde.Registry.lookup/2 returns, exactly as the :pg adapter relied on.
Wiring
{MyApp.MCP.Server,
transport: :streamable_http,
registry: {AttestoMCP.Anubis.Registry.Horde, []},
# ...
}Cluster membership uses members: :auto, so every node running the same
Anubis registry name discovers its peers (pair it with a clustering library
such as libcluster so the nodes connect).
Compile-guarded on Anubis.Server.Registry and Horde.Registry.
Summary
Functions
Starts the Horde.Registry for the given Anubis registry name.
Looks the session pid up in the cluster-wide Horde.Registry.
No-op: the session registers itself via the :via tuple from session_name/2
when it starts.
Returns the :via tuple Anubis uses as the session GenServer name.
No-op: Horde.Registry removes the entry automatically when the owning
session process exits.
Functions
Starts the Horde.Registry for the given Anubis registry name.
Anubis injects :name (a compile-time bounded atom such as
:"Anubis.MyApp.MCP.Server.registry") into opts. keys: :unique is
required for :via registration; members: :auto lets Horde discover peers
running the same registry name across the connected cluster.
Looks the session pid up in the cluster-wide Horde.Registry.
No-op: the session registers itself via the :via tuple from session_name/2
when it starts.
Returns the :via tuple Anubis uses as the session GenServer name.
Returning a :via tuple (rather than an atom) means the session registers
itself in Horde.Registry on start_link, so register_session/3 is a
no-op and no atom is ever derived from the session_id.
No-op: Horde.Registry removes the entry automatically when the owning
session process exits.
Horde.Registry.unregister/2 only unregisters names owned by the calling
process; here the caller is the transport, not the session, so an explicit
unregister would be a misleading no-op. Anubis terminates sessions through the
session DynamicSupervisor, and the resulting process exit drops the Horde
entry. Anubis never invokes this callback itself; it exists only to satisfy
the behaviour.