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
impl HttpClient
Sourcepub fn new(config: HttpClientConfig) -> Result<Self, HttpClientError>
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.
Sourcepub fn from_client(inner: Client, config: HttpClientConfig) -> Self
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.
Sourcepub fn next_request_id(&self) -> RequestId
pub fn next_request_id(&self) -> RequestId
The id for the next request on this client.
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.
Sourcepub fn answering(self, handler: Arc<dyn ServerRequestHandler>) -> Self
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.
Sourcepub fn resuming(self, id: McpSessionId) -> Self
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.
Sourcepub const fn config(&self) -> &HttpClientConfig
pub const fn config(&self) -> &HttpClientConfig
The configuration this client was built with.
Sourcepub fn session_token(&self) -> Option<Arc<str>>
pub fn session_token(&self) -> Option<Arc<str>>
The session id the peer issued, exactly as it wrote it.
Sourcepub fn session_id(&self) -> Option<McpSessionId>
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.
Sourcepub async fn post(
&self,
body: &[u8],
require_session: bool,
) -> Result<HttpResponse, HttpClientError>
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.
Sourcepub async fn post_envelope(
&self,
envelope: &Map<String, Value>,
) -> Result<Option<Map<String, Value>>, HttpClientError>
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:
sendmints 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.sendreturnsreply_result, which pulls theresultmember out and turns anerrormember into aTransportError. 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.
Sourcepub async fn post_envelope_with(
&self,
envelope: &Map<String, Value>,
headers: &[(String, String)],
) -> Result<Option<Map<String, Value>>, HttpClientError>
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
Sourcepub async fn notify_envelope(
&self,
envelope: &Map<String, Value>,
) -> Result<(), HttpClientError>
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.
Sourcepub async fn post_streaming(
&self,
body: &[u8],
require_session: bool,
) -> Result<HttpReply, HttpClientError>
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.
Sourcepub async fn delete(&self) -> Result<HttpResponse, HttpClientError>
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.
Sourcepub async fn open_stream(
&self,
cursor: Option<EventId>,
require_session: bool,
) -> Result<HttpStream, HttpClientError>
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.
Sourcepub const fn resumable(
&self,
start: Option<EventId>,
require_session: bool,
policy: Resumable,
) -> ResumableStream<'_>
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.
Sourcepub async fn listen(&self) -> Result<(), HttpClientError>
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.
Sourcepub fn spawn_listener(self: &Arc<Self>)
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.
Trait Implementations§
Source§impl BridgePeer for HttpClient
Carries envelopes over one HTTP client’s session.
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>>
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>>
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>>
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>>
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, ()>
fn wait_pending(&self) -> SendFuture<'static, ()>
take_pending. Read moreSource§impl ClientTransport for HttpClient
impl ClientTransport for HttpClient
Source§fn send(
&self,
method: &str,
params: &JsonObject,
) -> Pin<Box<dyn Future<Output = Result<JsonObject, TransportError>> + Send + '_>>
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 + '_>>
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.