Skip to main content

Client

Struct Client 

Source
pub struct Client { /* private fields */ }
Expand description

A client bound to a negotiated session.

§Examples

use std::sync::Arc;

use protocol_droid::application::client::{Client, ClientConfig, TransportError};
use protocol_droid::domain::ir::JsonObject;
use protocol_droid::domain::{ProtocolVersion, TransportKind};

/// A transport that always refuses, which is enough to show the gate.
struct Dead;

let version = ProtocolVersion::V2024_11_05;
let config = ClientConfig::new(version, TransportKind::Stdio);
let client = Client::connect(
    config.negotiated_with(version, protocol_droid::domain::CapabilitySet::all_for(version)),
    Arc::new(Dead),
);

// Tools exist in every revision, and both sides declared everything that
// revision defines, so the gate is open and the dead transport is what
// fails the call.
assert!(client.supports(protocol_droid::domain::Feature::Tools));

let outcome = tokio::runtime::Builder::new_current_thread()
    .build()
    .map_err(|e| e.to_string())?
    .block_on(client.list_tools());
assert_eq!(
    outcome.map_or_else(|e| e.to_string(), |_| "succeeded".to_string()),
    "the request did not complete: no route",
);

Implementations§

Source§

impl Client

Source

pub fn connect( negotiation: Negotiation, transport: Arc<dyn ClientTransport>, ) -> Self

Binds a transport to a session that has already negotiated.

Takes the whole Negotiation rather than just the version. The profile is a function of four inputs, and a client knows only two of them — its own capabilities and the transport. Re-deriving it here would mean guessing the server’s half, and a guess that leaves the server’s capabilities empty turns a feature nobody declared into a feature nobody may use. The transport did the handshake and has the real answer, so it passes the real answer.

Prefer from_session when a NegotiatedSession is to hand: it is this constructor minus the copy, and without the copy’s chance of being wrong. This one exists for a caller assembling a Negotiation directly, where re-deriving is the only way to get a profile at all.

Source

pub fn from_session( session: &NegotiatedSession, transport: Arc<dyn ClientTransport>, ) -> Self

Binds a transport to a session that has already negotiated.

The one to reach for. connect takes the four inputs resolve needs and re-derives a profile from them — but a NegotiatedSession has already resolved one, from those same four fields, so a caller going through connect rebuilds a struct literal and re-runs a pure function to arrive back where it started.

That is worse than redundant. Nothing obliges the four fields a caller copies to be the four the session was built from, so a caller who transposes one — or leaves client_caps empty because the transport made it awkward — gets a client whose profile disagrees with the session it just negotiated, and the disagreement surfaces much later as a feature that was advertised and then refused. Reading the profile off the session removes the copy, and with it the possibility of copying it wrong.

§Examples
use std::sync::Arc;

use protocol_droid::application::client::{Client, ClientConfig, ClientTransport, handshake};
use protocol_droid::domain::{ProtocolVersion, TransportKind};

async fn connect(transport: Arc<dyn ClientTransport>) -> Result<(), String> {
    let config = ClientConfig::speaking(
        [ProtocolVersion::V2025_06_18].into_iter(),
        TransportKind::Stdio,
    )
    .map_err(|e| e.to_string())?;
    let session = handshake(transport.as_ref(), &config)
        .await
        .map_err(|e| e.to_string())?;
    let client = Client::from_session(&session, transport);
    assert!(client.supports(protocol_droid::domain::Feature::Tools));
    Ok(())
}
Source

pub async fn connect_to(spec: &TransportSpec) -> Result<Self, ConnectError>

Connects to spec, negotiates, and returns a bound client.

The one-call form, for the common case where the caller has a URL and no opinion about revisions. The four steps it replaces — pick the adapter, choose what to offer, run the handshake, bind the result — are all decisions the crate can make from the spec, and a caller who has to make them is doing the crate’s work.

Offers every revision this build has wire types for. Narrow that with connect_to_speaking when the server is known to be older, or when a build is expected to run against something that answers a narrow offer badly.

§Errors

ConnectError naming the stage that failed. A transport this build has no adapter for is ConnectError::Unsupported rather than a handshake error, so “this crate cannot speak that” is never reported as “that server would not talk”.

§Examples
let spec = TransportSpec::new(
    protocol_droid::domain::TransportKind::StreamableHttp,
    "http://127.0.0.1:3000/mcp",
)?;
let client = Client::connect_to(&spec).await?;
let tools = client.list_tools().await?;
Source

pub async fn connect_to_speaking( spec: &TransportSpec, versions: &SupportedVersions, ) -> Result<Self, ConnectError>

Connects to spec, offering exactly versions.

connect_to for a caller who has an opinion about what to offer. The narrowing is deliberate: offering a revision a server does not speak gets the next one it does, so a set that excludes the right revision is refused by servers that would otherwise have worked.

§Errors

As connect_to, plus ConnectError::Config when versions is empty.

Source

pub async fn connect_with( spec: &TransportSpec, versions: &SupportedVersions, options: ConnectOptions, ) -> Result<Self, ConnectError>

Connects to spec, offering versions and applying options.

For a client that can answer the server: a server asks for sampling, elicitation or roots only when the client declared them, so connect_to, which declares nothing, can never be asked. The handler in options answers what the server does ask.

§Errors

As connect_to_speaking.

Source

pub async fn connect_stdio(spec: &ChildSpec) -> Result<Self, StdioConnectError>

Connects to an MCP server over stdio by spawning it as a child.

spec is the program and (optionally) arguments; args is appended after the program’s own arguments. The function spawns the child, drains the child’s stderr into Diagnostics::stderr(), runs the MCP initialize handshake, and binds the resulting session to a Client.

This is the stdio counterpart of connect_to for the HTTP transports. The four steps the consumer would otherwise write by hand — spawn a StdioClient, build a ClientConfig, run handshake, bind the session with from_session — are one call here. Each of those steps is a decision the consumer should not be making for a use case the library already has an answer to: which adapter speaks a given spec is a fact about the crate, the set of revisions to offer is a matter of what this build has wire types for, and the diagnostics channel is the obvious one. A consumer who has an opinion on any of those uses the four steps directly; both paths are public.

§Errors

Returns StdioConnectError on any of the four stages. The child is killed on failure, so a refused handshake does not leave a process behind.

§Example
use protocol_droid::adapters::secondary::stdio::ChildSpec;
use protocol_droid::application::client::Client;

let spec = ChildSpec::new("upstream-mcp-server")?;
let client = Client::connect_stdio(&spec).await?;
let tools = client.list_tools().await?;
Source

pub fn negotiated(&self) -> ProtocolVersion

The revision this client and its server agreed on.

Source

pub fn profile(&self) -> FeatureProfile

The profile that revision produced.

Source

pub fn session(&self) -> &NegotiatedSession

The negotiated session behind this client.

Everything the handshake agreed on, including the parts a caller usually wants for reporting rather than for gating: what the server called itself and which revision it answered with, what each side advertised, which transport this is, and whether the profile is recomputed per request with nothing retained.

The accessors above answer the two questions a caller asks before every call. This answers the one asked once, usually by a log line or a /healthz body — and it answers it from the session rather than from a second copy of the parts a caller remembered to keep.

Not const: the session is behind an Arc, and reading through one is not a constant expression. A caller who needs the version in a const should negotiate in a function it can run, not reach for one here.

Source

pub fn supports(&self, feature: Feature) -> bool

Whether feature may be used on this session.

Source

pub async fn send_unchecked( &self, method: &str, params: &JsonObject, ) -> Result<JsonObject, ClientError>

Sends method exactly as given, skipping the capability gate.

The typed methods refuse what the server did not advertise, which is right for an application and wrong for a tool whose job is to see what a server actually does: an inspector, a prober, a conformance client. A server may serve a method it did not advertise, and the only way to learn that is to ask.

§Errors

ClientError::Transport as for any request.

Source

pub async fn list_tools(&self) -> Result<JsonObject, ClientError>

Lists the server’s tools.

§Errors

ClientError::FeatureUnavailable when the session has no Feature::Tools, having written nothing. A 2024-11-05 session always has one, but a session whose peer advertises nothing still might not, and a caller learns which from this rather than from an opaque protocol error.

Source

pub async fn call_tool( &self, name: &str, arguments: &JsonObject, ) -> Result<JsonObject, ClientError>

Calls name with arguments.

§Errors

As list_tools.

Source

pub async fn call_tool_resolving( &self, name: &str, arguments: &JsonObject, resolve: &(impl InputResolver + ?Sized), ) -> Result<JsonObject, ClientError>

Calls name, answering any input the server asks for along the way.

See call_resolving for the rules.

§Errors

As call_tool, plus ClientError::InputUnresolved and ClientError::TooManyRounds.

Source

pub async fn list_resources(&self) -> Result<JsonObject, ClientError>

Lists the server’s resources.

§Errors

As list_tools, with Feature::Resources in place of Feature::Tools.

Source

pub async fn list_prompts(&self) -> Result<JsonObject, ClientError>

Lists the server’s prompts.

§Errors

As list_tools, with Feature::Prompts in place of Feature::Tools.

Source

pub async fn list_resource_templates(&self) -> Result<JsonObject, ClientError>

Lists the server’s resource templates.

§Errors

As list_resources.

Source

pub async fn read_resource(&self, uri: &str) -> Result<JsonObject, ClientError>

Reads the resource at uri.

§Errors

As list_resources.

Source

pub async fn get_prompt( &self, name: &str, arguments: &JsonObject, ) -> Result<JsonObject, ClientError>

Gets the prompt name, rendered with arguments.

§Errors

As list_prompts.

Trait Implementations§

Source§

impl Clone for Client

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Client

Source§

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

Prints the negotiation, not the transport.

For the same reason PeerSession does: a transport is a socket and a vtable pointer, and neither survives being printed at a call site.

The session is printed as its version and feature count rather than in full because it carries both peers’ PeerInfo, which is caller-supplied text and does not belong in a log line nobody asked for.

Auto Trait Implementations§

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> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
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> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
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