Default in-memory session store backed by a public, named ETS table.
Sessions are keyed by session_id (a random URL-safe base64 string
generated by ConduitMcp.Session.generate_id/0 on first initialize)
and store an arbitrary metadata map plus "created_at" (millisecond
Unix timestamp).
Ownership
The table is owned by a long-lived ConduitMcp.EtsOwner process
(ConduitMcp.Session.EtsStore.Owner) started from ConduitMcp.Application, so it
outlives the short-lived Bandit request process that happens to touch it
first. A table owned by a request process would die with that request,
taking every session with it — Session.get/2 would return
{:error, :not_found} for a session id the client had just been handed.
ensure_table/0 remains as a fallback for embedding contexts where the
:conduit_mcp application is not started (e.g. some test setups). It is
not the primary mechanism.
Concurrency
The table is :public, :set, read_concurrency: true, write_concurrency: :auto.
Reads do not block writes; writers serialize per key. Suitable for production
single-node workloads. For multi-node deployments, implement a
ConduitMcp.Session.Store against your distributed backend — see
guides/multi_node_sessions.md.
Bounds & cleanup
Two mechanisms keep the table bounded, and both are needed:
Row cap.
create/2refuses to grow the table pastconfig :conduit_mcp, :sessions_max_rows(default 100000,:infinitydisables it) and returns{:error, :session_limit_reached}. Sessions are created by unauthenticatedinitializerequests, so without a cap a supervised (i.e. immortal) table is an unbounded growth surface.Note this is a global bound, not a per-caller one: it refuses new sessions rather than evicting existing ones, so an unauthenticated flood cannot displace a live session, but it can deny new ones until the janitor ages the flood out.
ConduitMcp.Plugs.RateLimitis the per-caller bound; configure it on any public-facing transport.TTL sweep.
cleanup/1prunes rows older than a TTL.ConduitMcp.Session.Janitoris started byConduitMcp.Applicationagainst this store by default; disable it withconfig :conduit_mcp, :session_janitor, false, or tune it withconfig :conduit_mcp, :session_janitor, ttl: ..., interval: ....
Configuration
Reference the store from a transport's :session option:
{ConduitMcp.Transport.StreamableHTTP,
server_module: MyServer,
session: [store: ConduitMcp.Session.EtsStore, ttl: :timer.minutes(60)]}
Summary
Functions
Removes sessions older than ttl_ms milliseconds.
Creates a session, unless the table is already at its configured row cap.
Ensures the ETS table exists.
Functions
Removes sessions older than ttl_ms milliseconds.
Creates a session, unless the table is already at its configured row cap.
Returns :ok, or {:error, :session_limit_reached}.
Ensures the ETS table exists.
Normally a no-op: ConduitMcp.Session.EtsStore.Owner creates the table at
application start. Retained so the store still works when :conduit_mcp
is not running as an OTP application.