Server-Sent Events (SSE) transport layer for MCP.
Legacy transport
SSE is the pre-2025-03-26 MCP transport. New servers should use
ConduitMcp.Transport.StreamableHTTP, which carries the same features
over a single endpoint and is the one the specification develops. This
transport is maintained for existing clients.
Provides two endpoints:
- GET /sse - Server-Sent Events stream for server-to-client messages
- POST /message - HTTP endpoint for client-to-server messages
Everything this transport shares with ConduitMcp.Transport.StreamableHTTP
— the plug pipeline, CORS headers, auth (including strategy: :oauth), rate
limiting, the JSON-RPC POST dispatch, GET /health, the RFC 9728 metadata
endpoint and the catch-alls — lives in ConduitMcp.Transport.Shared.
Differences from ConduitMcp.Transport.StreamableHTTP
- No sessions.
Mcp-Session-Idis defined by the Streamable HTTP transport in the MCP specification; SSE predates it and has no session concept, so:sessionis not an option here, and there is noDELETE /to end one (DELETEfalls through to the 404 catch-all). - A long-lived GET stream.
GET /sseholds one process, socket andPlug.Connfor the life of the connection, bounded by:max_connectionsand:max_connection_lifetime. It carries theendpointevent and then only:\r\nkeep-alive comments. - The modern era is served, but not designed for.
POST /messageruns the shared pipeline, so a2026-07-28request is classified, its headers validated, and answered exactly as by StreamableHTTP'sPOST /(including a streamedtools/callresponse). The2026-07-28specification defines no HTTP+SSE transport, though, so modern clients should useConduitMcp.Transport.StreamableHTTP;GET /sseis legacy only. A legacy request whoseMCP-Protocol-Versionheader names an unsupported version is answered 400 here too.
Options
:server_module(required) - The MCP server module to route requests to:cors_origin— value foraccess-control-allow-origin. Unset means no CORS headers are emitted at all, so a page on another origin cannot read the response. Set it (e.g."https://myapp.example", or"*") to opt in.:cors_methods- CORS allow-methods header (default: "GET, POST, OPTIONS"; only emitted when:cors_originis set). UnlikeConduitMcp.Transport.StreamableHTTP, noDELETE: there is no session to end.:cors_headers- CORS allow-headers header (default: "content-type, authorization, mcp-session-id, mcp-protocol-version, mcp-method, mcp-name"; only emitted when:cors_originis set).Mcp-Param-*names must be listed here explicitly:access-control-allow-headershas no wildcard that holds for credentialed requests. Whenever:cors_originis set,access-control-expose-headers: mcp-session-id, mcp-protocol-versionis emitted too.:auth- Authentication plug configuration (optional). Supports every strategyConduitMcp.Plugs.Authdoes, plus:oauth.:instructions- instructions returned byinitializeandserver/discover; overrides the server module's own (use ConduitMcp.Server, instructions:or the Endpoint's:instructions):base_url- Public base URL advertised in the SSEendpointevent (e.g."https://mcp.example.com"). Defaults to deriving it from the request'sHostheader (sanitized). Set this when running behind a proxy.:allowed_origins- allowlist for theOriginheader. Accepts a list of strings, a bare string, aRegex, or"*"; any other value raisesArgumentErroratinit/1. ARegexis matched unanchored, so anchor it:~r/\Ahttps:\/\/example\.com\z/. Unset fails closed: any request carrying anOriginis rejected with 403. Requests without anOriginalways pass. SeeConduitMcp.Plugs.OriginValidation.:keep_alive_interval- milliseconds between:\r\nkeep-alive comments, onGET /sseand on streamed POST responses (default: 15 000).:max_streams,:max_stream_lifetime,:stream_cancel_timeout,:max_listens_per_principal,:listen_idle_timeout- the bounds on streamed modern POST responses andsubscriptions/listenstreams; seeConduitMcp.Transport.StreamableHTTP.:max_connection_lifetime- milliseconds after which an SSE stream is closed (default: 1 hour). A stream pins a process, a socket and aPlug.Conn; without a lifetime a client that opens connections and never reads accumulates both indefinitely.:max_connections- maximum concurrent SSE streams, a positive integer; any other value raisesArgumentErroratinit/1. Further connections get HTTP 503 (default: 1 000).
Example
{Bandit,
plug: {ConduitMcp.Transport.SSE,
server_module: MyApp.MCPServer,
cors_origin: "https://myapp.com"},
port: 4001}With Authentication
{Bandit,
plug: {ConduitMcp.Transport.SSE,
server_module: MyApp.MCPServer,
auth: [
strategy: :bearer_token,
token: "my-secret-token"
]},
port: 4001}