runStdio

Serve server over the process's standard input/output: read JSON-RPC messages from stdin (one per line) and write responses to stdout. Per the MCP stdio transport, only valid MCP messages are written to stdout; use stderr for logging. Blocks until stdin reaches end-of-file.

runStdio drives the process's standard input/output and MUST be called at most once per process (enforced by an explicit module-level guard that throws on any second call, concurrent or sequential). Rather than adopting fd 0/1 directly -- which would let releaseRef close() the process's real stdin/stdout, and which sets O_NONBLOCK on their shared open file description -- it dup()s fd 0/1 first and adopts the dups. On return the saved descriptor flags are restored on fd 0/1 (clearing the O_NONBLOCK that adopt set on the dup's shared open file description) and only then are the adopted dups released (their fds closed). fd 0/1 themselves are never adopted and never closed, so a caller that keeps running after runStdio returns inherits an open, blocking stdin/stdout.

stdin (fd 0) and stdout (fd 1) are adopted as vibe-async pipes (eventDriver.pipes.adopt, the same mechanism vibe.core.process uses for a spawned child), so the read loop is a plain cooperative vibe task — there is NO dedicated OS reader thread and therefore no OS-thread ⇄ event-loop seam to race on. Background notifications (notifyResourceUpdated, notify*ListChanged) and concurrent tool handlers work because every write goes through the channel's serialized writer.

maxLineBytes bounds a single inbound line; an oversized frame is dropped (its bytes are skipped up to the next newline) and the loop continues so one misbehaving frame neither exhausts memory nor kills the server.

@safe
void
runStdio