# `Snodo.Transport.StreamableHTTP.Server`
[🔗](https://github.com/joshrotenberg/snodo/blob/v0.4.1/lib/snodo/transport/streamable_http/server.ex#L1)

Small dependency-free HTTP/1.1 listener for `Snodo.Transport.StreamableHTTP`.

It is intentionally a binding, not a web framework. Each accepted connection
carries one request and closes after one response. Header admission happens in
the connection process; admitted MCP work runs through the reusable bounded
`Snodo.Server.Executor`. A peer disconnect while work is pending cancels that
execution and tears down its reply owner.

`:request_timeout` bounds queue wait plus execution after HTTP admission; the
transport's default ceiling is 30 seconds, including with an application-owned
executor. Progress does not extend the deadline. Explicit `:infinity` disables
this ceiling. Body reads and socket writes have their own transport bounds.

Connection and stream bounds, each a positive integer:

  * `:max_connections` - the most connections open at once, default 1,024. A
    connection accepted at the limit is closed without being read.
  * `:head_timeout` - milliseconds from accept until the request head must be
    complete, default 10,000. `:read_timeout` (default 5,000) still bounds
    each read; this deadline bounds the whole head, so a client that sends it
    a byte at a time is closed when the deadline passes.
  * `:request_gate_timeout` - milliseconds allowed for a request gate to
    check the head, default 10,000. A timed-out gate gets a 504 response.
  * `:body_timeout` - milliseconds from the end of the head, or from gate
    admission when a gate is configured, until the request body must be
    read, default 10,000. Each body read returns what has arrived and waits
    at most `:read_timeout`; this deadline bounds the whole body.
  * `:max_subscriptions` - the most `subscriptions/listen` streams open at
    once, default 256. The executor holds one count per listener and returns
    a slot when the connection serving that stream exits. A stream over the
    limit is closed at its source and the request gets 503.
  * `:drain_timeout` - milliseconds the listener waits on shutdown for open
    connections to finish, default 5,000.

## Shutdown

The listener drains when it terminates, including when its supervisor stops
it:

  1. The listening socket closes, so new connections are refused.
  2. A connection whose request head or body is still arriving gets 503 and
     closes. A request read in full before the drain began is admitted and
     runs to its response.
  3. Each open `subscriptions/listen` stream gets its successful completion
     response, its source is closed with reason `:shutdown`, and the
     connection closes.
  4. Connections still open when `:drain_timeout` passes are killed, which
     cancels their executor work and closes their subscription sources.
  5. An executor the listener started is stopped. An application-owned
     executor keeps running.

`child_spec/1` sets the child's `:shutdown` to `:drain_timeout` plus 5,000
ms, the default worker shutdown, so the supervisor does not kill the
listener during the drain. A child spec that overrides `:shutdown` must keep
it above `:drain_timeout`. A listener that is killed does not drain.

Applications that already run Plug, Bandit, or Cowboy can translate their
request into `Snodo.Transport.StreamableHTTP.Request` and use the pure adapter
directly instead of starting this listener.

The listener has no built-in authentication. Set `:request_gate` to a
`{module, options}` implementing `Snodo.Transport.StreamableHTTP.RequestGate`
to serve discovery routes and authenticate requests before reading their
bodies. A verified identity returned by the gate is available to
`Snodo.Authorization`. `snodo_oauth` provides an OAuth resource-server gate.

The listener serves plaintext HTTP. It warns when bound outside loopback,
including when a request gate is configured. Bind it privately behind a TLS
reverse proxy, or run `Snodo.Transport.Plug` in an HTTP server with TLS.

# `address`

```elixir
@spec address(GenServer.server()) ::
  {:inet.ip_address(), :inet.port_number(), String.t()}
```

Returns the bound IP, actual port, and configured MCP endpoint path.

# `child_spec`

Returns a worker child spec whose `:shutdown` exceeds `:drain_timeout` by
5,000 ms. An invalid `:drain_timeout` is rejected when the listener starts.

# `url`

```elixir
@spec url(GenServer.server()) :: String.t()
```

Returns a URL for a listener bound to a local IPv4 or IPv6 address.

---

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