mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 10:55:57 +00:00
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>
80 lines
4.6 KiB
Markdown
80 lines
4.6 KiB
Markdown
# 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.
|