diff --git a/specs/port/trios-railway/crates/trios-railway-mcp/src/main.t27 b/specs/port/trios-railway/crates/trios-railway-mcp/src/main.t27 new file mode 100644 index 0000000000..afefc3f390 --- /dev/null +++ b/specs/port/trios-railway/crates/trios-railway-mcp/src/main.t27 @@ -0,0 +1,143 @@ +// trios-railway-mcp — the public MCP server with dual transport. +// Port of gHashTag/trios-railway:crates/trios-railway-mcp/src/main.rs +// (at 8b229e9489ee, 114 lines) — Roadmap Stage 6, goal #4548, target +// t27c gen-rust. +// +// The original serves MCP (Model Context Protocol) over BOTH the modern +// Streamable HTTP transport (/mcp) and the legacy SSE transport +// (/sse + /message), with /healthz as the liveness probe and / as the +// greeting. It binds on the IPv6 unspecified address so the Railway +// IPv6-only private-network proxy can reach it, reads the port from the +// environment defaulting to 8080, keeps the SSE stream alive every 15 +// seconds, defaults the log filter to "info", prints one boot line +// before tracing starts (R5: never silent), and shuts down gracefully +// on the interrupt signal. +// +// The servers, the router and the signal handling are plumbing; the +// decisions — the port fallback, the bind address, the route table with +// its two transports and four routes, the keep-alive interval, the +// default log filter, and the two handler texts — are ported here as +// pure functions and tested. The main transport keeps its name with an +// `undefined;` body that no test calls (issue #9776, "Port the decision, +// not the plumbing"). + +module port::trios_railway_mcp_main; + +// --------------------------------------------------------------------------- +// Original lines 39-105: `async fn main() -> Result<()>`. The boot: bind, +// mount both transports, serve with graceful shutdown. Pure transport; +// its decisions are ported below. +// --------------------------------------------------------------------------- + +pub fn main() -> ?str { + return undefined; // plumbing: the dual-transport server boot and serve loop (lines 39-105) +} + +// Original lines 55-58: the PORT environment read with the 8080 +// fallback — unset or unparseable falls back, per the presence flag. +pub fn default_port() -> u16 { + return 8080; +} + +pub fn resolve_listen_port(port_set: bool, port: u16) -> u16 { + if (!port_set) { + return default_port(); + } + return port; +} + +// Original lines 59-61: bind on the IPv6 unspecified address so the +// Railway IPv6-only private-network proxy reaches the server. +pub fn unspecified_bind_address() -> str { + return "[::]"; +} + +// Original lines 66-89: two transports (Streamable HTTP on /mcp, legacy +// SSE on /sse + /message) and the /healthz liveness probe — four routes +// in all. +pub fn transport_count() -> usize { + return 2; +} + +pub fn mounted_route_count() -> usize { + return 4; +} + +pub fn mcp_path() -> str { + return "/mcp"; +} + +pub fn sse_path() -> str { + return "/sse"; +} + +pub fn message_path() -> str { + return "/message"; +} + +pub fn healthz_path() -> str { + return "/healthz"; +} + +// Original line 79: the SSE keep-alive interval. +pub fn sse_keep_alive_secs() -> u64 { + return 15; +} + +// Original lines 47-49: the log filter defaults to "info" when the +// environment carries no valid filter. +pub fn default_log_filter() -> str { + return "info"; +} + +// --------------------------------------------------------------------------- +// Original lines 107-110: `async fn root_handler() -> &'static str` — +// the greeting names both transports and where to reach them. +// --------------------------------------------------------------------------- + +pub fn root_handler() -> str { + return "trios-railway-mcp: public MCP server for the IGLA project.\nPOST JSON-RPC to /mcp (Streamable HTTP) or connect to /sse (legacy SSE)\n"; +} + +// --------------------------------------------------------------------------- +// Original lines 112-114: `async fn health_handler() -> &'static str` — +// the liveness answer. +// --------------------------------------------------------------------------- + +pub fn health_handler() -> str { + return "ok"; +} + +// --------------------------------------------------------------------------- +// Tests: every decision above asserted against values read from the +// original. No test calls the main transport. +// --------------------------------------------------------------------------- + +test health_answers_ok { + assert(health_handler() == "ok"); +} + +test root_greeting_names_both_transports { + assert(root_handler() == "trios-railway-mcp: public MCP server for the IGLA project.\nPOST JSON-RPC to /mcp (Streamable HTTP) or connect to /sse (legacy SSE)\n"); +} + +test port_defaults_and_overrides { + assert(default_port() == 8080); + assert(resolve_listen_port(false, 0) == 8080); + assert(resolve_listen_port(true, 9000) == 9000); +} + +test bind_and_route_layout { + assert(unspecified_bind_address() == "[::]"); + assert(transport_count() == 2); + assert(mounted_route_count() == 4); + assert(mcp_path() == "/mcp"); + assert(sse_path() == "/sse"); + assert(message_path() == "/message"); + assert(healthz_path() == "/healthz"); +} + +test keep_alive_and_log_filter { + assert(sse_keep_alive_secs() == 15); + assert(default_log_filter() == "info"); +}