# Streamable HTTP transport for the MCP server

Issue: https://github.com/morluto/rea/issues/4933

## Outcome

`rea mcp --http [--host H] [--port N]` serves the existing MCP tool catalog over
the pinned SDK's streamable HTTP transport instead of stdio, so an agent
environment runtime or any HTTP MCP client can reach REA on a local URL. The
listener is opt-in: bare `rea mcp`, `rea mcp doctor`, `rea mcp add` and every
client registration keep stdio.

## Requirements

- Route `mcp --http ...` and `--mcp --http ...` to the MCP runtime in the
  dispatcher without changing bare `mcp`, `mcp doctor` or `mcp add`.
- Resolve host and port from the flags, then `MCP_HOST`/`MCP_PORT` from the
  environment snapshot, then `127.0.0.1` and port `0`; announce the bound URL as
  one pino JSON line on stderr at every log level and leave stdout unused.
- Use `WebStandardStreamableHTTPServerTransport` in stateful mode around one
  server instance built once, after the listener has bound. `DELETE` ends the session on its transport without
  running the instance's shutdown hooks, so the same instance keeps its
  providers and serves the next client on a fresh transport; the hooks run when
  the listener closes.
- Let a new client's `initialize` replace a session that is still open, closing
  the earlier transport the same way, so a client that left without `DELETE`
  cannot block the listener; the replaced client's later requests are refused
  (`404` while the replacing session is open).
- Bridge node:http to web Request/Response in `src/server/http/`, streaming
  response chunks as they are produced and turning a client disconnect into a
  request abort and body cancellation.
- Classify a loopback bind by the actual bound IP (`127.0.0.0/8`, `::1`, or
  IPv4-mapped loopback), without using the configured DNS name as evidence; validate
  the `Host` and `Origin` headers with the SDK loopback allowlist plus the
  configured name and the bound address on such binds, and relax both for
  other binds and log that relaxation. Refuse scoped IPv6 literals before
  binding, close the listener if its URL cannot be formed, and forward
  listener errors to `onerror` only after a successful bind.
- Let the caller own instance cleanup (`closeInstance: false`) when its factory
  owner already closes every instance, as the MCP runtime does.
- Let only an `initialize` that carries the streamable headers and passes those
  checks replace an open session; refuse requests once the slot is closed, join
  an arming session during shutdown, and keep close stages retryable.
- End the HTTP listener on SIGINT/SIGTERM only, never on stdin EOF, and exit with
  the signal status (`130`/`143`) after cleanup settles; latch a signal that
  arrives during startup so it is acted on as soon as the lifecycle owner exists,
  and exit at once on a second one.
- Export `serveStreamableHttp({ host, port, createMcpServer, routes, logger })`
  returning `{ url, close }` so an agent environment server can add plain routes
  beside `/mcp` without a second bridge.
- Keep listener failure causes out of stderr behind a fixed message while the
  typed listen error carries the system cause (such as `EADDRINUSE`) into the
  error-level log line.

## Acceptance and limits

Adapters tests cover the bridge (method, URL, headers, body, GET without body,
incremental streaming, abort and cancel) and the listener (listening line, one
factory call across three sessions, replacement of an open session by the next
`initialize`, two `initialize` requests racing to replace one session while the
instance's shutdown hook stays installed, a working tool call after `DELETE`,
the instance's shutdown hook firing only when the listener closes, plain
routes, 404, Host refusal, port release, typed bind failure naming
`EADDRINUSE`). The dispatcher test covers the new routes and keeps `mcp add
--http` with the CLI. An mcp-boundary test spawns the built CLI and drives
initialize, tools/list, `binary_session` and `DELETE` with the SDK streamable
HTTP client, then proves `binary_session` still answers in the next session and
in a session that replaced one left open; a process-boundary test asserts
`143`/`130` exits and a released port. Concurrent clients are not supported:
the newest `initialize` owns the server. The non-loopback relaxation is covered
only through its pure allowlist helper; no wide bind runs in tests. Response
budgets keep their stdio-derived floor; registrations, `rea mcp doctor` and
`server.json` stay stdio.

## Deferred issue criteria

Serving an environment manifest with replayed records, its environment card and
the exported bundle are later increments of #4933. This PR must reference rather
than close the issue.
