Skip to main content

StdioClient

Struct StdioClient 

Source
pub struct StdioClient<DW> { /* private fields */ }
Expand description

A running conversation with a child MCP server.

Generic over the diagnostics channel for the same reason the server’s ServeLoop is: “somewhere that is not the protocol stream” is a decision the caller makes, and a test needs to be able to read it back.

See the module docs for what this is and is not.

Implementations§

Source§

impl<DW> StdioClient<DW>

Source

pub fn child_id(&self) -> Option<u32>

The child’s process id, while it is alive.

For a log line. It is not proof of anything: a caller that needs to know the process is gone wants take_stderr_forwarder, because end-of-file on the child’s own pipe is the kernel saying so and a process id is not.

Unbounded deliberately — reading the process id touches the Child and nothing else, so putting it behind the DW bounds would mean Debug had to carry them too.

Source§

impl<DW> StdioClient<DW>
where DW: AsyncWrite + Unpin + Send + 'static,

Source

pub async fn spawn( spec: &ChildSpec, config: ClientConfig, diagnostics: Diagnostics<DW>, ) -> Result<Self, SpawnError>

Starts a child server and opens a conversation with it.

The child’s stdout becomes a StdioSource, its stdin a StdioSink, and its stderr is drained into diagnostics by a task that lives as long as the child does. All three of those adapters take the same StdioConfig, which is the point: a client that could be given one frame ceiling for what it reads and another for what it writes would be a client whose two halves disagree about what a frame is.

Dropping the returned client kills the child. That is the kill_on_drop(true) set in child::spawn, and it is the reason this function is the only way to get a client.

§Errors

SpawnError::NotStarted when the program could not be started, and SpawnError::NoPipe when it started without one of the three pipes the conversation needs. In the second case the child is dropped on the way out, so the failure does not leave a process behind.

§Examples
use protocol_droid::adapters::primary::stdio::{Diagnostics, StdioConfig};
use protocol_droid::adapters::secondary::stdio::{ChildSpec, ClientConfig, StdioClient};

let spec = ChildSpec::new("my-server")?;
let config = ClientConfig::new(StdioConfig::default());
let client = StdioClient::spawn(&spec, config, Diagnostics::stderr()).await?;

assert!(client.child_id().is_some());
Source

pub const fn config(&self) -> ClientConfig

The settings this client was built with.

Source

pub fn diagnostics(&self) -> &Diagnostics<DW>

The child’s stderr channel.

The child does not write to this — child::forward_stderr does, one line at a time — and neither does anything else this crate knows of. It is here so a caller can add its own, the same way the server’s diagnostics channel is.

Source

pub const fn source(&self) -> &StdioSource<ChildStdout>

The read half, for a caller driving the conversation itself.

Source

pub const fn sink(&self) -> &StdioSink<ChildStdin>

The write half, for a caller driving the conversation itself.

Source

pub const fn take_stderr_forwarder(&mut self) -> Option<JoinHandle<()>>

Detaches the stderr forwarder and hands back its handle.

Awaiting the handle completes when the child’s stderr reaches end-of-file, which is the point: the kernel closes a process’s descriptors when the process is torn down, so this is a caller being told by the operating system that the child is gone — not a caller observing that a handle was dropped.

Taken rather than merely borrowed because the question it answers is asked after the client, and a borrow of a field would keep the client alive. Detaching the task does not stop it: it holds its own reference to the diagnostics channel and to the child’s stderr, and it ends when the pipe does.

Source

pub async fn send_document(&self, document: &str) -> Result<(), StdioError>

Writes one already-encoded document to the child.

The framing — the delimiter, the ceiling, the flush — is StdioSink’s, which is the same code the server writes its replies with. A caller reaching for this is assembling a document itself; a caller with a method and some parameters wants call.

§Errors

StdioError::ReplyTooLarge when the document does not fit the configured ceiling, and StdioError::Transport when the child’s stdin refused it — which, for a child that has exited, is the normal way to find out.

Source

pub async fn next_frame(&self) -> Result<Option<String>, SourceError>

Reads the child’s next frame.

Ok(None) is the child closing, and nothing else: it is StdioSource’s answer passed through unaltered, because the decoder underneath it answers Ok(None) for the opposite reason — a line with no terminator yet — and forwarding that up would turn “read more” into “the server is gone”. A caller that skips a None here will conclude the child died on a request that was merely slow, and call is where that mistake is most expensive.

§Errors

SourceError — see StdioSource::next_frame.

Source

pub fn next_request_id(&self) -> RequestId

A fresh request id from this client’s counter.

A counter and not a random value, for the reason everything else in this crate that needs an identity takes one: the domain does not call a generator, so a client’s ids are as reproducible as its config, and two requests in flight on one client can never collide.

Source

pub async fn call( &self, id: &RequestId, method: &MethodName, params: &JsonObject, ) -> Result<JsonObject, StdioClientError>

Sends a request and returns the peer’s answer.

The three steps — write the document, read the next frame, match its id — are a request and a reply, and a caller that wants a different order wants send_document and next_frame instead.

§Errors

StdioClientError::Document when the request document could not be assembled, StdioClientError::Stdio and StdioClientError::Source when the pipe refused, and StdioClientError::PeerClosed when the child closed its stdout before answering. A child that answers with a failure is StdioClientError::Request, because that is a result of the request rather than a fault in the transport.

Source

pub async fn notify( &self, method: &MethodName, params: &JsonObject, ) -> Result<(), StdioClientError>

Sends a notification, which never gets an answer.

§Errors

As send_document, plus StdioClientError::Document if the document cannot be assembled.

Source

pub async fn terminate( &mut self, ) -> Result<Option<ExitStatus>, StdioClientError>

Kills the child and waits for it to be reaped.

The deliberate counterpart to dropping the client, for a caller that wants the exit status and for the reap to have happened before it moves on. A dropped client kills the child too, and the operating system reaps it; it just does not tell anyone how it ended.

Calling this twice is not a second kill: the second call reports Ok(None), because there is nothing left to wait for.

§Errors

StdioClientError::Terminate when the child could not be waited for.

Trait Implementations§

Source§

impl<DW> ClientTransport for StdioClient<DW>
where DW: AsyncWrite + Unpin + Send + 'static,

Source§

fn send( &self, method: &str, params: &JsonObject, ) -> Pin<Box<dyn Future<Output = Result<JsonObject, TransportError>> + Send + '_>>

Sends method with params under a fresh id and returns the result.

The boxed future because the port asks for one: a client’s transport is chosen at run time from a configuration, so there is no monomorphisation to win here and holding a dyn is the honest shape.

The document is assembled before the future is built rather than inside it. The port elides the return lifetime to &self, so a boxed future cannot hold the caller’s method or params; assembling first is what leaves the future owning only the id and the method name.

§Errors

TransportError::Unavailable naming whatever the client refused. The port is protocol-neutral and must not learn that a stdio client has a frame ceiling, so the detail is rendered rather than kept.

Source§

fn notify( &self, method: &str, params: &JsonObject, ) -> Pin<Box<dyn Future<Output = Result<(), TransportError>> + Send + '_>>

Writes a notification and returns.

Write-only by construction: there is no id to match a reply against, so there is nothing to wait for. The document is assembled before the future is built, for the same reason send does it — the port elides the return lifetime, so a boxed future cannot borrow the caller’s method name.

§Errors

TransportError::Unavailable if the document cannot be assembled or the pipe refuses the write.

Source§

impl<DW> Debug for StdioClient<DW>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Prints the process and the settings, and nothing about the streams.

The same reasoning as the server’s ServeLoop: a child’s buffered bytes and a pipe’s internals say nothing about which session is misbehaving, and requiring DW: Debug to print the one field that would be a bound nobody wants.

Auto Trait Implementations§

§

impl<DW> !Freeze for StdioClient<DW>

§

impl<DW> !RefUnwindSafe for StdioClient<DW>

§

impl<DW> !UnwindSafe for StdioClient<DW>

§

impl<DW> Send for StdioClient<DW>
where Arc<Diagnostics<DW>>: Send,

§

impl<DW> Sync for StdioClient<DW>
where Arc<Diagnostics<DW>>: Sync,

§

impl<DW> Unpin for StdioClient<DW>
where Arc<Diagnostics<DW>>: Unpin,

§

impl<DW> UnsafeUnpin for StdioClient<DW>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more