A response the transport may send as a Server-Sent Events stream instead
of a single JSON body: the {:stream, spec} reply of
ConduitMcp.Handler.dispatch/3.
The modern handler returns one of two kinds:
:response— a moderntools/callwhose_metacarries aprogressTokenorio.modelcontextprotocol/logLevel: the requests that can receivenotifications/progressornotifications/messagewhile the tool runs.:listen— a modernsubscriptions/listenwhose filter parsed: a long-lived stream of change notifications that has no response until the server ends it (ConduitMcp.Subscriptions).
Every other request is answered {:reply, response}.
A :response spec's run produces the final JSON-RPC response map. What
runs it decides how the response travels:
ConduitMcp.Transport.Shared.dispatch_post/2runs it in a task and, while it runs, relays the tool's notifications (ConduitMcp.Progress,ConduitMcp.Log) as SSE events ahead of the response. A client that disconnects cancels the request (ConduitMcp.Cancellation).run/1runs it in the calling process with no stream: notifications are dropped and the response is returned.ConduitMcp.Handler.handle_request/3uses it, so manual-mode callers keep getting a map.
Either way the request is tracked in flight for run's duration, so the
tool can poll ConduitMcp.Cancellation.cancelled?/1.
A :listen spec has no run: only a transport that can hold an SSE
stream open serves it (ConduitMcp.Transport.Shared.dispatch_post/2).
ConduitMcp.Handler.handle_request/3 answers it -32601.
Summary
Types
@type t() :: %ConduitMcp.StreamSpec{ conn: Plug.Conn.t() | map(), kind: :response | :listen, method: String.t() | nil, progress_token: String.t() | integer() | nil, request_id: ConduitMcp.Protocol.json_rpc_id() | nil, run: (Plug.Conn.t() | map() -> map()) | nil, subscription: ConduitMcp.Subscriptions.Filter.t() | nil }
:kind—:response: a finite response, preceded by notifications;:listen: asubscriptions/listenstream.:request_id— the JSON-RPC id being answered.:method— the request method.:progress_token— the request's_meta.progressToken, carried by everynotifications/progress;nilwhen the request has none.:conn— the connrunis called with (the request'sConduitMcp.Ctxis stored in it).:run—:responseonly:(Plug.Conn.t() -> map()), the work: returns the JSON-RPC response map. It must not raise; the modern handler builds it around the same guard as every other request.:subscription—:listenonly: the honouredConduitMcp.Subscriptions.Filter.