Add an explicit Streamable HTTP entry for environment/gateway hosts while keeping stdio registrations and default behavior. Preserve the shared provider/session owner across DELETE and replacement; fence and drain tool/completion work before handover or teardown. Bound HTTP input and plain-response admission, cancel refused bodies, join whole exchanges and asynchronous response cleanup before provider hooks, gate startup readiness, and reject queued HTTP/1.1 pipelining while retaining sequential keep-alive. Validation: 8 static/build tasks, 83 adapter tests and registry metadata pass. A real installed gh binary produces seven equivalent CLI/stdio/HTTP reports with DELETE/replacement and clean signal shutdown. Broader local native-provider/ownership failures reproduce on clean main and are documented with limitations in the PR. Co-authored-by: Edgar Arakelyan <edgar.arakelyan@scale.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
4.6 KiB
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 baremcp,mcp doctorormcp add. - Resolve host and port from the flags, then
MCP_HOST/MCP_PORTfrom the environment snapshot, then127.0.0.1and port0; announce the bound URL as one pino JSON line on stderr at every log level and leave stdout unused. - Use
WebStandardStreamableHTTPServerTransportin stateful mode around one server instance built once, after the listener has bound.DELETEends 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
initializereplace a session that is still open, closing the earlier transport the same way, so a client that left withoutDELETEcannot block the listener; the replaced client's later requests are refused (404while 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 theHostandOriginheaders 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 toonerroronly 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
initializethat 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/mcpwithout 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.