ConduitMcp.Transport.SSE (ConduitMCP v0.11.0)

Copy Markdown View Source

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-Id is defined by the Streamable HTTP transport in the MCP specification; SSE predates it and has no session concept, so :session is not an option here, and there is no DELETE / to end one (DELETE falls through to the 404 catch-all).
  • A long-lived GET stream. GET /sse holds one process, socket and Plug.Conn for the life of the connection, bounded by :max_connections and :max_connection_lifetime. It carries the endpoint event and then only :\r\n keep-alive comments.
  • The modern era is served, but not designed for. POST /message runs the shared pipeline, so a 2026-07-28 request is classified, its headers validated, and answered exactly as by StreamableHTTP's POST / (including a streamed tools/call response). The 2026-07-28 specification defines no HTTP+SSE transport, though, so modern clients should use ConduitMcp.Transport.StreamableHTTP; GET /sse is legacy only. A legacy request whose MCP-Protocol-Version header names an unsupported version is answered 400 here too.

Options

  • :server_module (required) - The MCP server module to route requests to
  • :cors_origin — value for access-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_origin is set). Unlike ConduitMcp.Transport.StreamableHTTP, no DELETE: 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_origin is set). Mcp-Param-* names must be listed here explicitly: access-control-allow-headers has no wildcard that holds for credentialed requests. Whenever :cors_origin is set, access-control-expose-headers: mcp-session-id, mcp-protocol-version is emitted too.
  • :auth - Authentication plug configuration (optional). Supports every strategy ConduitMcp.Plugs.Auth does, plus :oauth.
  • :instructions - instructions returned by initialize and server/discover; overrides the server module's own (use ConduitMcp.Server, instructions: or the Endpoint's :instructions)
  • :base_url - Public base URL advertised in the SSE endpoint event (e.g. "https://mcp.example.com"). Defaults to deriving it from the request's Host header (sanitized). Set this when running behind a proxy.
  • :allowed_origins - allowlist for the Origin header. Accepts a list of strings, a bare string, a Regex, or "*"; any other value raises ArgumentError at init/1. A Regex is matched unanchored, so anchor it: ~r/\Ahttps:\/\/example\.com\z/. Unset fails closed: any request carrying an Origin is rejected with 403. Requests without an Origin always pass. See ConduitMcp.Plugs.OriginValidation.
  • :keep_alive_interval - milliseconds between :\r\n keep-alive comments, on GET /sse and 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 and subscriptions/listen streams; see ConduitMcp.Transport.StreamableHTTP.
  • :max_connection_lifetime - milliseconds after which an SSE stream is closed (default: 1 hour). A stream pins a process, a socket and a Plug.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 raises ArgumentError at init/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}