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 releaseRefclose() 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.
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.