Optional Plug binding for the stateless Streamable HTTP adapter.
The application supplies an immutable :runtime and a supervised :executor.
This Plug starts no listener or global runtime. Install it before body parsers,
after the application's authentication Plug. Only conn.assigns[:mcp_auth]
supplies Snodo.Context.auth; request headers and JSON never supply identity.
Requests are admitted before entering the bounded executor. Valid cancellation
notifications bypass that queue. Cross-request cancellation is opt-in through
the trusted :mcp_cancellation_scope assign, combined with the complete auth
value; applications must distinguish authenticated client instances in this
scope. Anonymous requests cannot cancel each other by guessing request IDs.
Portable Plug APIs only reveal a disconnect when a write fails. So that a
client disconnect cancels a silent handler, as the 2026-07-28 cancellation
rules require, a request still running after :disconnect_probe_ms (default
5,000) turns its response into an event stream and sends a keepalive comment
every interval; a failed write cancels the work, and the result arrives as
the stream's final event. :infinity keeps plain JSON responses. A finite
total :request_timeout bounds pending work and queue wait.
At most :max_subscriptions (default 256) subscriptions/listen streams are
open at once. init/1 can run at compile time, so the count is held in the
executor, which lives as long as the runtime; Plugs that share an executor
share the count. A stream over the limit is closed at its source and the
request gets 503.
The request body must arrive within :body_timeout (default 10,000 ms from
when this Plug starts reading it) or the request gets 408. The Plug reads the
body in slices of at most 64 KiB and checks the deadline between them; each
read waits at most :read_timeout (default 5,000) and never past the
deadline. Under Bandit a Content-Length body is one socket read per slice,
so the deadline bounds the whole body. A request that declares
Transfer-Encoding gets 411 without its body being read: an adapter returns
from a chunked read only once it has the requested bytes, however many
socket reads that takes, so no deadline checked between reads can bound it.
Over HTTP/2, Bandit returns from a read after a slice of DATA, at the end of
the stream, or after :read_timeout without a frame, and each DATA frame
restarts that wait, so a client that keeps sending small or empty frames can
hold the request process past the deadline. After a body error over HTTP/1.x
the response closes the connection, so the adapter does not read the rest of
the body.