# `ConduitMcp.Transport.SSE`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/transport/sse.ex#L1)

Server-Sent Events (SSE) transport layer for MCP.

> #### Legacy transport {: .warning}
>
> 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}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
