Construct over a newline-delimited JSON-RPC channel. readLine returns the next line from the server (without its terminator) or null at end-of-input; writeLine emits one request/notification line to the server (the sink appends the terminator). Both MUST be async (cooperative) when the client is driven under an event loop.
Attach owned subprocess pipes so close() runs the stdio shutdown sequence. Set by McpClient.spawn.
Release transport resources. When this transport owns a spawned subprocess (McpClient.spawn), run the MCP stdio Shutdown sequence (basic/lifecycle §Shutdown -> stdio): close the child's stdin, escalate to SIGTERM, then SIGKILL if it does not exit within the grace periods. A no-op when there is no owned subprocess (a custom readLine/writeLine channel).
Shut the owned child down per the MCP stdio Shutdown sequence and return its exit status (a process killed by signal reports a negative status: -SIGTERM / -SIGKILL). Safe to call once.
How many times the child-shutdown sequence has run (for tests asserting close() idempotency).
Send a request and return its result (or throw McpException). The channel correlates the reply by expectId while its read loop concurrently dispatches any interleaved notifications and server->client requests, so multiple deliver calls may be in flight at once.
Open a draft subscriptions/listen stream over stdio. Unlike Streamable HTTP — where the listen stream is a separate long-lived SSE response — stdio shares one channel, so opening a subscription is just writing the subscriptions/listen request line; the server delivers the leading notifications/subscriptions/acknowledged and every subsequent change notification on the same stdout channel, each stamped with io.modelcontextprotocol/subscriptionId (the listen request id), and they reach the client's inbound dispatcher through the channel's read loop (draft basic/utilities/subscriptions: "On stdio ... clients MUST use this field to correlate notifications"). The returned handle's cancel()/close() ends the subscription by sending notifications/cancelled referencing the listen request id, per the draft stdio cancellation rule.
false: replies are deferred — see ClientTransport.repliesSynchronously. The read loop always runs as a vibe task (DuplexChannel.start -> runTask), so the event loop that deferral needs is always available.
Send a message that expects no correlated reply (notification, or a response to a server->client request).
No-op: there is no OAuth bearer token over stdio.
No-op: the draft-protocol flag has no effect on stdio (no SSE GET streams).
The stdio transport needs neither protocol-derived headers nor the cancelled-response predicate (it has no HTTP headers and correlates responses by id on a single channel), so it ignores the installed ClientProtocol.
No-op: there is no HTTP+SSE backward-compatibility fallback over stdio.
No-op: there is no standalone server->client stream over stdio (the single duplex channel already carries server->client traffic).
Send a JSON-RPC request requestMessage and return its result Json (throwing McpException on an error response). The id to await is expectId. Interleaved notifications and server->client requests seen while awaiting are dispatched to the inbound handler.
Send a message that expects no correlated reply: a notification, or a response to a server->client request.
Whether a reply to a server->client request may be written *inline* from the inbound-read callback rather than deferred to a background task. The reply is sent from inside the read loop of an in-flight request, while that loop must keep draining inbound bytes, so the answer turns on whether a nested synchronous send can wedge the read loop: - false (both concrete transports): defer the reply. stdio would block the single read-loop task on the OS pipe buffer (the child may be blocked writing stdout we have stopped draining while we block writing its stdin); HTTP's reply travels on a different request that could deadlock the connection. McpClient dispatches the reply via runTask, which requires a running event loop — both transports already provide one. - true: send inline, with no event loop required. Reserved for a future transport whose read loop neither blocks nor holds the awaited response.
Open the standalone server->client stream, if the transport has one (HTTP GET SSE). A no-op on stdio.
Open a long-lived subscriptions/listen stream for listenMessage, dispatching every inbound message on it to the inbound handler. Returns a handle whose cancel()/close() stops the stream.
Install the client's inbound dispatcher (McpClient.dispatchInbound), invoked for notifications and server->client requests on any stream.
Install the client's ClientProtocol collaborator, through which the transport obtains the protocol-derived request headers (headersFor) and the cancelled-response predicate (isCancelled). A transport that needs neither (e.g. stdio) may keep it but ignore it. McpClient calls this once at construction.
Initiate the transport's backward-compatibility fallback after a modern request was rejected in a way that signals an older server (HTTP: a 400/404/405 POST -> open the legacy HTTP+SSE GET stream and switch to the two-endpoint transport). A no-op on transports without a fallback path (stdio), symmetric with startServerStream/setBearerToken. The client follows this with the legacy initialize handshake.
Attach an OAuth bearer access token (HTTP Authorization: Bearer); a no-op on stdio. An empty string clears it.
Signal whether the negotiated protocol version is modern (2026-07-28 / draft). The HTTP transport uses this to skip Last-Event-ID resumption (GET) that the draft removed; a no-op on stdio and on transports where the flag is irrelevant.
Release transport resources: stdio terminates the subprocess (when one was spawned); HTTP stops any background streams.
A ClientTransport over the MCP **stdio** transport, built on the shared full-duplex DuplexChannel.
Per the MCP stdio transport, the host launches the MCP server as a subprocess and exchanges newline-delimited JSON-RPC messages over its stdin/stdout; only valid MCP messages are written to the server's stdin (newlines are never embedded in a message), and stderr is used by the server for logging.
This class is transport-pure: it is constructed with a readLine/writeLine pair (symmetric to mcp.transport.stdio.serveStdio on the server side) that a running event loop drives cooperatively. The DuplexChannel's read loop demultiplexes inbound lines, so several requests can be in flight at once and the server may push notifications (or server->client requests) at any time — each is routed to the owning McpClient's inbound dispatcher. There is no bearer token and no backward-compatibility fallback over stdio, so setBearerToken, startServerStream, and startLegacyFallback are no-ops. close() terminates the subprocess when one was spawned (see McpClient.spawn).
Concurrency requires a running vibe event loop: the supplied readLine/ writeLine MUST be async (non-blocking on a vibe stream). McpClient.spawn wires that automatically; the McpClient.stdio(readLine, writeLine) delegate overload is for custom channels and the caller is responsible for supplying async delegates.