OTP application for ConduitMCP.
Request handling itself is stateless — each HTTP request runs in its own Bandit process — but ConduitMCP is not a library without processes. It starts a small supervision tree whose job is to own the long-lived ETS tables that must outlive short-lived request processes, plus the janitors that keep them bounded:
ConduitMcp.Cancellation.Owner— always started.ConduitMcp.Cancellation.InFlightOwner— always started; owns the in-flight table (one row per request being handled) that gates which cancellations are recorded.ConduitMcp.Cancellation.Janitor— always started; sweeps cancellation rows whose request never cleared them, and in-flight rows whose process died without untracking. Disable withconfig :conduit_mcp, :cancellation_janitor, false.ConduitMcp.Session.EtsStore.Owner— always started.ConduitMcp.Transport.SSE.Owner— always started; owns the slot table (one row per live stream) behindTransport.SSE's:max_connections.ConduitMcp.Transport.SSEStream.Owner— always started; owns the slot table (one row per open stream) behind the HTTP transports':max_streamscap on streamed POST responses.ConduitMcp.Subscriptions— always started; aRegistry(duplicate keys) of thesubscriptions/listenstreams served by this node, keyed by the notification topics they deliver. The registry's own processes only clean up after streams that die; publishing and registering go straight to its ETS tables from the calling process.ConduitMcp.Subscriptions.Admission— always started; the livesubscriptions/listenstreams of each principal behind:max_listens_per_principal. Unlike the table owners it is called: admitting a stream (once per listen) counts, supersedes and records in one serialized step. It runs no user code.ConduitMcp.Session.Janitor— always started againstConduitMcp.Session.EtsStore, under the nameConduitMcp.Session.Janitor.Defaultso it cannot collide with a janitor you start yourself. Disable withconfig :conduit_mcp, :session_janitor, false, or tune it withconfig :conduit_mcp, :session_janitor, ttl: ..., interval: ....ConduitMcp.Tasks.EtsStore.Owner— started only when the default in-memory tasks store is in use (skipped for a custom:tasks_store).ConduitMcp.Ext.Tasks.EtsStore.Ownerand the Ext.Tasks janitor (aConduitMcp.Session.JanitornamedConduitMcp.Ext.Tasks.Janitor) — only whenconfig :conduit_mcp, :ext_tasksenables the experimental tasks extension; the owner only for the default store, the janitor unlessjanitor: false. SeeConduitMcp.Ext.Tasks.ConduitMcp.OAuth.KeyProvider.JWKS.Owner— started when the JWKS key provider is compiled in (i.e. whenReqis available).
Every table above is :public; the owners create the table and then idle.
Reads and writes go directly to ETS from the calling process — the owners
are not handle_call gateways and must not become ones.
(ConduitMcp.Subscriptions.Admission owns no table; see its moduledoc for
why a process answering calls is right there.)
It also starts ConduitMcp.Client.Registry, a passive unique Registry
(partitioned by scheduler) in which every ConduitMcp.Client registers
its processes under tuple keys of its :name. The application starts no
client processes: you supervise {ConduitMcp.Client, opts} yourself.
start/2 also seeds the validation config into :persistent_term for O(1)
reads on every request; when config :conduit_mcp, :mrtr_keys is set,
validates that keyring with ConduitMcp.MRTR.State.validate_keys!/1 so a
malformed key fails at boot rather than on the first MRTR request; and
validates :ext_tasks (ConduitMcp.Ext.Tasks.validate_config!/1) and the
:cache_ttl_ms / :cache_scope defaults
(ConduitMcp.CacheHints.validate_config!/0).