DuplexChannel

The shared full-duplex core of the MCP **stdio** transport, used by BOTH the client and the server. The stdio transport is a single newline-delimited JSON-RPC byte stream carrying traffic in both directions at once (basic/transports §stdio), so concurrency is structured as one cooperative vibe read-loop task that demultiplexes inbound lines, plus a serialized writer:

- a *response* / *errorResponse* line resolves the matching outbound request in the shared DuplexCoordinator, waking whichever task is blocked in await (so several requests can be in flight concurrently and replies may arrive out of order); - a *request* / *notification* line is handed to onInbound (the client's or server's inbound dispatcher), which a peer typically runs in its own task so multiple inbound requests are handled concurrently.

The read loop is a plain cooperative vibe task over an async readLine delegate — there is NO dedicated OS reader thread, so there is no OS-thread ⇄ event-loop seam to race on. All writes go through one TaskMutex so two tasks emitting at once cannot interleave bytes of different frames on the wire (the stdio transport requires each message to be a single newline-delimited line).

Construct it with three async delegates supplied by the concrete transport: readLine (returns the next line without its terminator, or null at end-of-input), writeLine (emits one line; the delegate appends the terminator), and onInbound (dispatches an inbound request/notification).

Constructors

this
this(string delegate() @(safe) readLine, void delegate(string) @(safe) writeLine, void delegate(Message) @(safe) onInbound)

readLine returns the next inbound line (without terminator) or null at EOF (and the read loop ends). writeLine emits one outbound line. onInbound is invoked for every inbound request / notification line (never for a response, which the channel correlates itself).

this
this(string delegate() @(safe) readLine, void delegate(string) @(safe) writeLine, void delegate(Message) @(safe) onInbound, void delegate(string) @(safe) onInboundBatch)

As the three-argument constructor, but with onInboundBatch: an optional handler for an inbound JSON-RPC batch *array* line, given the line's raw text. The SERVER inbound path supplies this so a batch is dispatched as a whole through server.handleRaw — preserving the protocol-version batch gate and the single JSON-array response framing that JSON-RPC 2.0 requires. When it is null (the CLIENT read path) a batch line is split into its members and each is routed individually, so per-id response correlation still works.

Members

Functions

close
void close()

Close the channel. Marks it closed and fails every still-pending request so awaiting callers are released immediately instead of waiting out their timeout; any request issued after this point also fails fast. The owning transport still closes the underlying byte stream to stop the read loop; close is idempotent and the read loop's own EOF path is equivalent.

closed
bool closed()

Whether the channel has closed: the read loop ended (EOF or read error) or close was called. Once closed, deliver/request fail fast instead of blocking on a reply that can never arrive.

deliver
Json deliver(Json message, long expectId, Duration timeout)

Send a request whose id was already chosen by the caller (the CLIENT path: McpClient pre-allocates the id), and block the current task until the correlated reply arrives. Returns its result, or throws McpException on an error reply / timeout / channel close.

request
Json request(string method, Json params, Duration timeout)

Originate a server->client request (the SERVER path: sampling / elicitation / roots / ping), allocating a fresh id, and block until the peer replies. Returns its result, or throws McpException on an error reply / timeout / channel close.

runReadLoop
void runReadLoop()

Run the read loop inline on the current task (does not spawn a task). runStdio/serveStdio call this so the server's main task IS the read loop and the function blocks until stdin reaches EOF.

send
void send(Json message)

Write one JSON-RPC message as a single newline-delimited line, serialized against concurrent writers. Json.toString never emits a raw newline, so the line framing holds and only a valid MCP message is written.

sendRaw
void sendRaw(string text)

Write an already-serialized JSON-RPC line as a single newline-delimited line, serialized against concurrent writers. Used by callers that already hold the serialized text (the stdio server sink and reply path) so the line is not parsed back to Json only to be re-serialized. The caller is responsible for the text being one valid MCP message with no embedded newline.

start
void start()

Start the cooperative read loop as a vibe task. Requires a running event loop (runEventLoop).