Packages
Model Context Protocol (MCP) implementation in Elixir with Phoenix integration
Current section
Files
Jump to
Current section
Files
lib/backplane/mcp_protocol/sse/streaming.ex
if Code.ensure_loaded?(Plug) do
defmodule Backplane.McpProtocol.SSE.Streaming do
@moduledoc false
use Backplane.McpProtocol.Logging
alias Backplane.McpProtocol.SSE.Event
@type conn :: Plug.Conn.t()
@type transport :: GenServer.server()
@type session_id :: String.t()
@doc """
Starts the SSE streaming loop for a connection.
This function takes control of the connection and enters a receive loop,
streaming messages to the client as they arrive.
## Parameters
- `conn` - The Plug.Conn that has been prepared for chunked response
- `transport` - The transport process
- `session_id` - The session identifier
- `opts` - Options including:
- `:initial_event_id` - Starting event ID (default: 0)
- `:on_close` - Function to call when connection closes
## Messages handled
- `{:sse_message, binary}` - Message to send to client
- `:close_sse` - Close the connection gracefully
"""
@spec start(conn, transport, session_id, keyword()) :: conn
def start(conn, transport, session_id, opts \\ []) do
initial_event_id = Keyword.get(opts, :initial_event_id, 0)
on_close = Keyword.get(opts, :on_close, fn -> :ok end)
try do
loop(conn, transport, session_id, initial_event_id)
after
on_close.()
end
end
@doc """
Prepares a connection for SSE streaming.
Sets appropriate headers and starts chunked response.
"""
@spec prepare_connection(conn) :: conn
def prepare_connection(conn) do
conn
|> Plug.Conn.put_resp_content_type("text/event-stream")
|> Plug.Conn.put_resp_header("cache-control", "no-cache")
|> Plug.Conn.put_resp_header("x-accel-buffering", "no")
|> Plug.Conn.send_chunked(200)
end
@doc """
Sends a single SSE event.
This is useful for sending events outside of the main loop.
"""
@spec send_event(conn, binary(), non_neg_integer()) ::
{:ok, conn} | {:error, term()}
def send_event(conn, data, event_id) when is_binary(data) do
event = %Event{
id: to_string(event_id),
event: "message",
data: data
}
case Plug.Conn.chunk(conn, Event.encode(event)) do
{:ok, conn} -> {:ok, conn}
{:error, reason} -> {:error, reason}
end
end
# Private functions
defp loop(conn, transport, session_id, event_counter) do
receive do
:sse_keepalive ->
case keep_alive(conn) do
{:ok, conn} ->
loop(conn, transport, session_id, event_counter + 1)
{:error, reason} ->
Logging.transport_event("sse_keepalive_failed", %{session_id: session_id, reason: reason}, level: :warning)
conn
end
{:sse_message, message} when is_binary(message) ->
case send_event(conn, message, event_counter) do
{:ok, conn} ->
loop(conn, transport, session_id, event_counter + 1)
{:error, reason} ->
Logging.transport_event(
"sse_send_failed",
%{session_id: session_id, reason: reason},
level: :warning
)
conn
end
{:sse_message, message, {from, ref}} when is_binary(message) ->
case send_event(conn, message, event_counter) do
{:ok, conn} ->
send(from, {ref, :ok})
loop(conn, transport, session_id, event_counter + 1)
{:error, reason} ->
Logging.transport_event(
"sse_send_failed",
%{session_id: session_id, reason: reason},
level: :warning
)
send(from, {ref, {:error, reason}})
conn
end
:close_sse ->
Logging.transport_event("sse_closing", %{session_id: session_id})
Plug.Conn.halt(conn)
{:plug_conn, :sent} ->
# Ignore Plug internal messages
loop(conn, transport, session_id, event_counter)
msg ->
Logging.transport_event(
"sse_unknown_message",
%{
session_id: session_id,
message: inspect(msg)
},
level: :warning
)
loop(conn, transport, session_id, event_counter)
end
end
defp keep_alive(conn) do
Plug.Conn.chunk(conn, ": keepalive\n\n")
end
end
end