Skip to main content

HttpClient

Struct HttpClient 

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

A client for one endpoint.

Holds the session the peer issued, so the type is Send + Sync and one client can drive concurrent requests. That is the point: the session id is the shared state, and it is a header rather than a connection, so two in-flight requests sharing it is normal and not a race.

Implementations§

Source§

impl HttpClient

Source

pub fn new(config: HttpClientConfig) -> Result<Self, HttpClientError>

Builds a client. Opens no socket.

§Errors

Propagates the builder’s own failure, which is a TLS backend that could not be initialised rather than anything about the endpoint.

Source

pub fn from_client(inner: Client, config: HttpClientConfig) -> Self

Builds a client around a connection pool that already exists.

Exists for the auto-detect step, which holds a reqwest::Client before it knows which transport the peer speaks — the probe request is the thing that tells it. Handing that pool to whichever client wins means one TLS handshake and one DNS resolution for the whole connection, rather than a second set for the transport that happened to win.

Takes the pool by value and cannot fail, because there is nothing left to validate: the configuration was checked by whoever parsed it, and the pool is already built.

Source

pub fn next_request_id(&self) -> RequestId

The id for the next request on this client.

Source

pub fn authorizing(self, auth: Arc<dyn BearerAuth>) -> Self

This client, authenticating through auth.

Every request carries the bearer token it supplies, and a 401 or 403 is handed back to it to recover from before the request is tried again.

Source

pub fn answering(self, handler: Arc<dyn ServerRequestHandler>) -> Self

This client, answering the server’s requests through handler.

Without it a sampling, elicitation or roots request the server sends mid-call is parked and never answered, and the tool waiting on it waits forever.

Source

pub fn resuming(self, id: McpSessionId) -> Self

A client that already knows the session the peer issued.

For the same probe: its response already carried an Mcp-Session-Id, and every later request has to carry it. Reconstructing the client from scratch to recover that header would mean re-running the handshake, and an initialize run twice is a second session on a server that is counting them.

Source

pub const fn config(&self) -> &HttpClientConfig

The configuration this client was built with.

Source

pub fn session_token(&self) -> Option<Arc<str>>

The session id the peer issued, exactly as it wrote it.

Source

pub fn session_id(&self) -> Option<McpSessionId>

The session the peer has issued, if it has issued one and it is in the form this crate’s own server writes.

None for a session any other server issued; session_token is the accessor that works against every server.

Source

pub async fn post( &self, body: &[u8], require_session: bool, ) -> Result<HttpResponse, HttpClientError>

POSTs a body and reads one complete response.

Reads the body in full, so a response the peer chose to frame as a stream is not acceptable here: its body is open-ended and buffering it waits forever. Use HttpClient::post_streaming for that shape.

§Errors

Any HttpClientError a request can produce, including HttpClientError::NoSessionYet when require_session is set and the peer has not issued a session, and HttpClientError::MediaType when a 2xx came back framed as a stream.

Source

pub async fn post_envelope( &self, envelope: &Map<String, Value>, ) -> Result<Option<Map<String, Value>>, HttpClientError>

POSTs a whole JSON-RPC envelope and returns the peer’s answer envelope.

The envelope-granularity entry point, for a caller that already holds a parsed message — a bridge, chiefly. ClientTransport::send cannot serve that, and the difference is not cosmetic:

  • send mints an id from this client’s counter. A proxy must carry the downstream id through instead, or the reply cannot be matched to the request the client is still waiting on.
  • send returns reply_result, which pulls the result member out and turns an error member into a TransportError. A proxy needs the whole reply: a peer that declines a request has sent a well-formed message, and re-wrapping it as a transport fault reports a network error for a request the server answered on purpose.

The session lifecycle is this client’s, not a second one — the Mcp-Session-Id this client obtained at handshake is the one every forwarded request carries, which is what lets a bridge reuse an established upstream session instead of re-handshaking per request.

require_session is false because a bridge forwards the handshake itself, and the first message it forwards is the one that has no session yet.

§Errors

Any HttpClientError a request can produce, plus HttpClientError::NotAMessage when the peer answers 2xx with a JSON body that is not an object.

§Panics

Never. Serialisation and parsing failures are HttpClientErrors.

Source

pub async fn post_envelope_with( &self, envelope: &Map<String, Value>, headers: &[(String, String)], ) -> Result<Option<Map<String, Value>>, HttpClientError>

Self::post_envelope, with headers added to this request alone.

For the 2026-07-28 request-metadata headers, which describe one body and so cannot be set once per client.

§Errors

As Self::post_envelope.

Source

pub async fn notify_envelope( &self, envelope: &Map<String, Value>, ) -> Result<(), HttpClientError>

Sends an already-assembled notification envelope.

The envelope-granularity counterpart to ClientTransport::notify, for a caller that already holds a parsed message. A notification is answered with a 202 and no body, so there is nothing to read back — this is HttpClient::post_envelope with the reply discarded, which is why it is a method rather than a call the adapter has to remember to write correctly itself.

§Errors

HttpClientError when the envelope cannot be encoded or the POST does not succeed.

Source

pub async fn post_streaming( &self, body: &[u8], require_session: bool, ) -> Result<HttpReply, HttpClientError>

POSTs a body and accepts either response shape.

§Errors

As HttpClient::post, except that a stream is a success rather than a HttpClientError::MediaType.

Source

pub async fn delete(&self) -> Result<HttpResponse, HttpClientError>

DELETEs the session, ending it at the peer.

§Errors

HttpClientError::NoSessionYet when there is no session to end. There is nothing here to be idempotent about, so guessing is not on the menu.

Source

pub async fn open_stream( &self, cursor: Option<EventId>, require_session: bool, ) -> Result<HttpStream, HttpClientError>

Opens an event stream from cursor, and does not reconnect.

For a stream that has to survive a drop use HttpClient::resumable, which is the same read with the reconnect inside it.

§Errors

HttpClientError::NoSessionYet when require_session is set and no session has been issued, or HttpClientError::MediaType if the peer did not answer with a stream.

Source

pub const fn resumable( &self, start: Option<EventId>, require_session: bool, policy: Resumable, ) -> ResumableStream<'_>

A read that reconnects itself when the peer’s stream breaks.

The three things this gets right, each of which is a way a resume silently loses or repeats an event:

  • It resumes from the last event delivered, never from a byte count and never from the last block read. A chunk can carry half an event; resuming from where reading stopped repeats it.
  • An event with no id cannot be resumed from, because the peer holds no cursor for it. Resuming from the previous id would make the peer repeat it, so the break is reported instead of papered over.
  • The first failure is not instantly fatal and neither is the hundredth: there is a budget, and running out of it is an error carrying how far the read got.

Nothing is opened until the first ResumableStream::next_event, so building one cannot fail and cannot hang.

Source

pub async fn listen(&self) -> Result<(), HttpClientError>

Opens the session’s standing stream and reads it until the peer ends it.

§Why a client that only posts needs this

A Streamable HTTP server may push at any time — list_changed, a sampling request, a log message — on the stream a GET opens. A client that only ever POSTs never opens one, so those messages have nowhere to arrive, and a bridge forwards every request while telling its own client nothing. Opening the stream is what makes Self::take_pending mean anything.

The read runs in its own task and parks into the same queue a mid-call frame lands in, so a caller cannot tell which of the two produced what, and one wait covers both.

§Errors

HttpClientError::NoSessionYet before a session exists — there is no stream to open without one — and any error a GET can produce. Failing to open is reported rather than swallowed.

Source

pub fn spawn_listener(self: &Arc<Self>)

Opens the standalone GET stream in the background and keeps it open.

Through 2025-11-25 a server may send a request of its own — sampling, elicitation, roots — and may send it on this stream rather than on the response to a call, since the call that needed the answer has no stream of its own to put it on. A client that declared one of those capabilities and never opens the stream is never asked, and a server whose tool waits for the answer waits forever.

Requests are answered through the wired handler; everything else is parked for take_pending. A stream the server ends is reopened after its retry interval with the last event id, and a server that answers 405 to the GET — the specification lets it — ends the task quietly. The task holds only a weak reference, so dropping the client stops it.

Source

pub fn take_pending(&self) -> Vec<Map<String, Value>>

Takes every frame a server volunteered while this client was busy.

Trait Implementations§

Source§

impl BridgePeer for HttpClient

Carries envelopes over one HTTP client’s session.

Source§

fn send( &self, envelope: Map<String, Value>, ) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>>

Delivers one envelope and yields the peer’s answer.

§Errors

BridgeError::Unreachable when the request could not be completed at all. A peer that answered with an error member is not an error here: see the module docs.

Source§

fn take_pending(&self) -> Vec<Map<String, Value>>

Delivers one envelope that expects no answer.

§Errors

BridgeError::Unreachable when the request could not be completed. The inherent take_pending and not this one, which is what an unqualified Self:: resolves to. Naming it here because a reader seeing the same method name twice has no other way to tell the two apart.

Source§

fn listen(&self) -> SendFuture<'_, Result<(), BridgeError>>

Opens the upstream’s standing stream.

This is the half that makes a server-initiated message reach the client at all: without it the client POSTs, the server answers, and anything the server volunteers in between goes to nobody.

Source§

fn close(&self) -> SendFuture<'_, Result<(), BridgeError>>

Ends the upstream session.

§Errors

BridgeError::Unreachable when the peer did not accept the teardown. A peer that has already gone is reported as unreachable rather than swallowed: pretending a shutdown succeeded would hide a leaked session.

Source§

fn wait_pending(&self) -> SendFuture<'static, ()>

Resolves once the peer may have something for take_pending. Read more
Source§

fn notify( &self, envelope: Map<String, Value>, ) -> SendFuture<'_, Result<(), BridgeError>>

Delivers one envelope that expects no answer. Read more
Source§

impl ClientTransport for HttpClient

Source§

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

POSTs a request frame and returns the result it carries.

The id matters here for the same reason it matters on stdio: a reply that arrives for a different request must be recognisable as one, and this transport carries a session’s worth of traffic over one connection.

§Errors

TransportError::Unavailable naming the refusal — a frame this client could not assemble, a status the transport rejects, or a reply whose error member names a failure.

Source§

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

POSTs a notification frame and returns without reading a body.

A notification is answered with a 202 and no content, so there is no result to read and no id to match — asking for one would turn every correctly-answered notification into a malformed-frame error.

§Errors

TransportError::Unavailable if the frame cannot be assembled or the POST does not succeed.

Source§

impl Debug for HttpClient

Source§

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

Formats the value using the given formatter. Read more

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