DuplexCoordinator

Correlates outbound JSON-RPC requests with the peer's responses on a single duplex byte channel (the MCP **stdio** transport, where both directions share one stream). It is the symmetric, transport-neutral counterpart to the Streamable HTTP transport's StreamCoordinator (which matches a server->client request with the client's reply that arrives on a *separate* POST), used by DuplexChannel for BOTH peers:

- the client awaits the response to a request it sent the server, and - the server awaits the response to a server->client request it sent the client (sampling / elicitation / roots / ping).

A waiting task blocks on a vibe LocalManualEvent, so the await is cooperative: the channel's read loop keeps running and resolves the waiter when the matching reply line arrives. One instance is shared across the channel; the read loop is the single resolve/failPending caller, and each awaiting task is its own register/await caller, so on the default single- threaded vibe event loop no additional locking is required.

Members

Functions

alloc
long alloc()

Allocate a fresh outbound request id. Used by the server peer, which originates its own server->client requests; the client peer supplies the id McpClient pre-allocated.

await
Json await(long id, Duration timeout)

Block the current task until the peer responds to id (or timeout elapses). Returns the result, or throws McpException on error / timeout / channel close (failPending). Deregisters the waiter on the way out.

cancel
void cancel(long id)

Drop a registered-but-unawaited request id (e.g. when sending the request frame failed so a response will never arrive). Idempotent; unknown ids are ignored. Keeps the waiter table from leaking when register is not followed by await.

closed
bool closed()

Whether the channel has closed (failPending ran). Once true, any newly registered request is failed fast rather than left to time out.

failPending
void failPending(McpException error)

Fail every still-pending request with error and wake its awaiting task. Called by the read loop at end-of-input so a caller blocked in await is released with an exception instead of hanging until its timeout. Marks the coordinator closed so any request registered AFTER this point is failed fast (see register) rather than left to time out.

register
void register(long id)

Begin tracking a pending outbound request id. Call before sending the request frame so a fast reply cannot race ahead of the registration. If the channel has already closed (failPending ran), the waiter is created already resolved with the channel-closed error, so a subsequent await fails fast instead of blocking until its timeout.

resolve
bool resolve(Json idJson, Json result, Json error)

Deliver a peer response / errorResponse. Returns true if idJson matched a pending outbound request (waking its awaiting task), false otherwise (an id we are not awaiting — e.g. a stray response — is ignored).