StdioClientTransport

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.

Constructors

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

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.

Members

Functions

attachProcess
void attachProcess(ProcessPipes* pipes)

Attach owned subprocess pipes so close() runs the stdio shutdown sequence. Set by McpClient.spawn.

close
void close()

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

closeProcess
int closeProcess(Duration termGrace, Duration killGrace)

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.

closeProcessRuns
int closeProcessRuns()

How many times the child-shutdown sequence has run (for tests asserting close() idempotency).

deliver
Json deliver(Json message, long expectId)

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.

openListen
SubscriptionStream openListen(Json message)

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.

repliesSynchronously
bool repliesSynchronously()

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.

sendOneway
void sendOneway(Json message)

Send a message that expects no correlated reply (notification, or a response to a server->client request).

setBearerToken
void setBearerToken(string token)

No-op: there is no OAuth bearer token over stdio.

setDraftProtocol
void setDraftProtocol(bool isDraft)

No-op: the draft-protocol flag has no effect on stdio (no SSE GET streams).

setProtocol
void setProtocol(ClientProtocol protocol)

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.

startLegacyFallback
void startLegacyFallback()

No-op: there is no HTTP+SSE backward-compatibility fallback over stdio.

startServerStream
void startServerStream()

No-op: there is no standalone server->client stream over stdio (the single duplex channel already carries server->client traffic).

Inherited Members

From ClientTransport

deliver
Json deliver(Json requestMessage, long expectId)

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.

sendOneway
void sendOneway(Json message)

Send a message that expects no correlated reply: a notification, or a response to a server->client request.

repliesSynchronously
bool repliesSynchronously()

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.

startServerStream
void startServerStream()

Open the standalone server->client stream, if the transport has one (HTTP GET SSE). A no-op on stdio.

openListen
SubscriptionStream openListen(Json listenMessage)

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.

setInboundHandler
void setInboundHandler(void delegate(Message) @(safe) handler)

Install the client's inbound dispatcher (McpClient.dispatchInbound), invoked for notifications and server->client requests on any stream.

setProtocol
void setProtocol(ClientProtocol protocol)

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.

startLegacyFallback
void startLegacyFallback()

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.

setBearerToken
void setBearerToken(string token)

Attach an OAuth bearer access token (HTTP Authorization: Bearer); a no-op on stdio. An empty string clears it.

setDraftProtocol
void setDraftProtocol(bool isDraft)

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.

close
void close()

Release transport resources: stdio terminates the subprocess (when one was spawned); HTTP stops any background streams.