use core::fmt;
use core::future::Future;
use core::pin::Pin;
use core::str::FromStr;
use core::sync::atomic::{AtomicI64, Ordering};
use serde_json::{Map, Value};
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::adapters::primary::transport::http::{SseError, SseEvent};
use crate::application::bridge::Unsolicited;
use crate::application::client::{ClientTransport, TransportError};
use crate::application::jsonrpc::document::{
notification_document, reply_result, request_document,
};
use crate::domain::ir::{JsonObject, MethodName};
use crate::domain::{EventId, McpSessionId, McpSessionIdError, RequestId};
use super::error::{HttpClientError, preview};
use super::frames::{FrameError, FrameReader};
use super::opaque::WireStream;
use crate::application::client::bearer::BearerAuth;
use crate::application::client::handler::ServerRequestHandler;
impl From<FrameError> for HttpClientError {
fn from(error: FrameError) -> Self {
match error {
FrameError::BlockTooLong { .. } => Self::StreamEnded { delivered: 0 },
FrameError::Grammar { source } => Self::Frame { source },
}
}
}
const SESSION_HEADER: &str = "mcp-session-id";
struct Auth(Arc<dyn BearerAuth>);
impl fmt::Debug for Auth {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("Auth(..)")
}
}
struct Handler(Arc<dyn ServerRequestHandler>);
impl fmt::Debug for Handler {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("Handler(..)")
}
}
const DEFAULT_RETRY: Duration = Duration::from_secs(1);
const MAX_RESUMES: u32 = 5;
fn is_request(frame: &Map<String, Value>) -> bool {
frame.contains_key("method") && frame.contains_key("id")
}
const PROTOCOL_HEADER: &str = "mcp-protocol-version";
const LAST_EVENT_ID: &str = "last-event-id";
const POST: &str = "POST";
const GET: &str = "GET";
const DELETE: &str = "DELETE";
const ACCEPT: &str = "application/json, text/event-stream";
const JSON: &str = "application/json";
const EVENT_STREAM: &str = "text/event-stream";
const MAX_CURSOR: usize = 32;
#[derive(Debug, Clone)]
pub struct HttpClientConfig {
endpoint: String,
request_timeout: Option<Duration>,
}
impl HttpClientConfig {
pub fn new(endpoint: &str) -> Result<Self, HttpClientError> {
reqwest::Url::parse(endpoint).map_err(|error| HttpClientError::Url {
got: endpoint.to_owned(),
reason: error.to_string(),
})?;
Ok(Self {
endpoint: endpoint.to_owned(),
request_timeout: None,
})
}
#[must_use]
pub const fn with_request_timeout(mut self, timeout: Duration) -> Self {
self.request_timeout = Some(timeout);
self
}
#[must_use]
pub const fn without_request_timeout(mut self) -> Self {
self.request_timeout = None;
self
}
#[must_use]
pub fn endpoint(&self) -> &str {
&self.endpoint
}
#[must_use]
pub const fn request_timeout(&self) -> Option<Duration> {
self.request_timeout
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetryBudget {
attempts: u32,
}
impl RetryBudget {
#[must_use]
pub const fn new(attempts: u32) -> Self {
Self { attempts }
}
#[must_use]
pub const fn attempts(self) -> u32 {
self.attempts
}
}
impl Default for RetryBudget {
fn default() -> Self {
Self::new(3)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Resumable {
pub budget: RetryBudget,
}
#[derive(Debug)]
pub struct HttpClient {
inner: reqwest::Client,
config: HttpClientConfig,
session: Mutex<Option<Arc<str>>>,
next_id: AtomicI64,
protocol: Mutex<Option<String>>,
handler: Option<Handler>,
auth: Option<Auth>,
pub(crate) unsolicited: Arc<Unsolicited>,
}
impl HttpClient {
pub fn new(config: HttpClientConfig) -> Result<Self, HttpClientError> {
let mut builder = reqwest::Client::builder()
.user_agent(concat!("protocol-droid/", env!("CARGO_PKG_VERSION")));
if let Some(limit) = config.request_timeout {
builder = builder.timeout(limit);
}
let inner = builder.build().map_err(|error| HttpClientError::Io {
method: "CLIENT",
error,
})?;
Ok(Self::from_client(inner, config))
}
#[must_use]
pub fn from_client(inner: reqwest::Client, config: HttpClientConfig) -> Self {
Self {
inner,
config,
session: Mutex::new(None),
next_id: AtomicI64::new(1),
protocol: Mutex::new(None),
handler: None,
auth: None,
unsolicited: Arc::new(Unsolicited::new()),
}
}
pub fn next_request_id(&self) -> RequestId {
RequestId::Number(self.next_id.fetch_add(1, Ordering::Relaxed))
}
#[must_use]
pub fn authorizing(mut self, auth: Arc<dyn BearerAuth>) -> Self {
self.auth = Some(Auth(auth));
self
}
#[must_use]
pub fn answering(mut self, handler: Arc<dyn ServerRequestHandler>) -> Self {
self.handler = Some(Handler(handler));
self
}
#[must_use]
pub fn resuming(self, id: McpSessionId) -> Self {
*self
.session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
Some(Arc::from(id.to_string().as_str()));
self
}
#[must_use]
pub const fn config(&self) -> &HttpClientConfig {
&self.config
}
#[must_use]
pub fn session_token(&self) -> Option<Arc<str>> {
self.session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
#[must_use]
pub fn session_id(&self) -> Option<McpSessionId> {
self.session_token()
.and_then(|token| McpSessionId::from_str(&token).ok())
}
pub async fn post(
&self,
body: &[u8],
require_session: bool,
) -> Result<HttpResponse, HttpClientError> {
let response = self.send(POST, body, require_session).await?;
self.read_document(response, POST).await
}
pub async fn post_envelope(
&self,
envelope: &Map<String, Value>,
) -> Result<Option<Map<String, Value>>, HttpClientError> {
self.post_envelope_with(envelope, &[]).await
}
pub async fn post_envelope_with(
&self,
envelope: &Map<String, Value>,
headers: &[(String, String)],
) -> Result<Option<Map<String, Value>>, HttpClientError> {
let body = serde_json::to_vec(envelope).map_err(|error| HttpClientError::Body {
method: "POST",
what: "encoded",
detail: error.to_string(),
})?;
let response = self.send_with(POST, &body, false, headers).await?;
let wants = envelope.get("id").cloned();
if response.status() == reqwest::StatusCode::ACCEPTED {
return Ok(None);
}
if Self::shape(&response)? == Shape::Stream {
return self.read_streamed(response, wants.as_ref()).await.map(Some);
}
let raw = response
.bytes()
.await
.map_err(|error| HttpClientError::Io {
method: POST,
error,
})?;
let raw: &[u8] = &raw;
if wants.is_none() {
return Ok(None);
}
if raw.is_empty() {
return Ok(None);
}
let text = std::str::from_utf8(raw).map_err(|error| HttpClientError::Body {
method: "POST",
what: "decoded as UTF-8",
detail: error.to_string(),
})?;
let value: Value = serde_json::from_str(text).map_err(|error| HttpClientError::Body {
method: "POST",
what: "parsed as JSON",
detail: error.to_string(),
})?;
value
.as_object()
.cloned()
.map(Some)
.ok_or_else(|| HttpClientError::NotAMessage {
got: value.to_string(),
})
}
pub async fn notify_envelope(
&self,
envelope: &Map<String, Value>,
) -> Result<(), HttpClientError> {
let body = serde_json::to_vec(envelope).map_err(|error| HttpClientError::Body {
method: "POST",
what: "encoded",
detail: error.to_string(),
})?;
self.post(&body, false).await?;
Ok(())
}
pub async fn post_streaming(
&self,
body: &[u8],
require_session: bool,
) -> Result<HttpReply, HttpClientError> {
let response = self.send(POST, body, require_session).await?;
match Self::shape(&response)? {
Shape::Json => self
.read_document(response, POST)
.await
.map(HttpReply::Document),
Shape::Stream => Ok(HttpReply::Stream(HttpStream::new(response))),
}
}
pub async fn delete(&self) -> Result<HttpResponse, HttpClientError> {
let Some(id) = self.session_token() else {
return Err(HttpClientError::NoSessionYet);
};
let mut request = self
.inner
.delete(&self.config.endpoint)
.header(SESSION_HEADER, id.as_ref())
.header(reqwest::header::ACCEPT, ACCEPT);
if let Some(Auth(auth)) = &self.auth
&& let Some(token) = auth.bearer()
{
request = request.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"));
}
let response = request.send().await.map_err(|error| HttpClientError::Io {
method: DELETE,
error,
})?;
self.read_document(response, DELETE).await
}
pub async fn open_stream(
&self,
cursor: Option<EventId>,
require_session: bool,
) -> Result<HttpStream, HttpClientError> {
let response = self.get(cursor, require_session).await?;
if Self::shape(&response)? != Shape::Stream {
return Err(media_error(&response));
}
Ok(HttpStream::new(response))
}
#[must_use]
pub const fn resumable(
&self,
start: Option<EventId>,
require_session: bool,
policy: Resumable,
) -> ResumableStream<'_> {
ResumableStream {
client: self,
current: None,
cursor: start,
delivered: 0,
attempts: 0,
require_session,
policy,
finished: false,
}
}
async fn send(
&self,
verb: &'static str,
body: &[u8],
require_session: bool,
) -> Result<reqwest::Response, HttpClientError> {
self.send_with(verb, body, require_session, &[]).await
}
async fn send_with(
&self,
verb: &'static str,
body: &[u8],
require_session: bool,
extra: &[(String, String)],
) -> Result<reqwest::Response, HttpClientError> {
let mut response = self.send_once(verb, body, require_session, extra).await?;
loop {
if response.status().is_success() {
if let Some(Auth(auth)) = &self.auth {
auth.accepted();
}
return Ok(response);
}
let status = response.status().as_u16();
if matches!(status, 401 | 403)
&& let Some(Auth(auth)) = &self.auth
{
let challenge = response
.headers()
.get(reqwest::header::WWW_AUTHENTICATE)
.and_then(|value| value.to_str().ok())
.map(ToOwned::to_owned);
auth.challenged(status, challenge.as_deref())
.await
.map_err(|error| HttpClientError::Authorization(error.to_string()))?;
response = self.send_once(verb, body, require_session, extra).await?;
continue;
}
return Err(refusal(verb, response).await);
}
}
async fn send_once(
&self,
verb: &'static str,
body: &[u8],
require_session: bool,
extra: &[(String, String)],
) -> Result<reqwest::Response, HttpClientError> {
let mut builder = self
.inner
.request(method_of(verb), &self.config.endpoint)
.header(reqwest::header::ACCEPT, ACCEPT)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(body.to_vec());
let protocol = self
.protocol
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
if let Some(protocol) = protocol
&& !extra
.iter()
.any(|(name, _)| name.eq_ignore_ascii_case(PROTOCOL_HEADER))
{
builder = builder.header(PROTOCOL_HEADER, protocol);
}
for (name, value) in extra {
builder = builder.header(name.as_str(), value.as_str());
}
if let Some(id) = self.session_token() {
builder = builder.header(SESSION_HEADER, id.as_ref());
} else if require_session {
return Err(HttpClientError::NoSessionYet);
}
if let Some(Auth(auth)) = &self.auth
&& let Some(token) = auth.bearer()
{
builder = builder.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"));
}
let response = builder.send().await.map_err(|error| HttpClientError::Io {
method: verb,
error,
})?;
self.absorb_session(&response)?;
Ok(response)
}
async fn get(
&self,
cursor: Option<EventId>,
require_session: bool,
) -> Result<reqwest::Response, HttpClientError> {
let mut builder = self
.inner
.get(&self.config.endpoint)
.header(reqwest::header::ACCEPT, ACCEPT);
if let Some(id) = self.session_token() {
builder = builder.header(SESSION_HEADER, id.as_ref());
} else if require_session {
return Err(HttpClientError::NoSessionYet);
}
if let Some(id) = cursor {
builder = builder.header(LAST_EVENT_ID, cursor_value(id)?);
}
let response = builder
.send()
.await
.map_err(|error| HttpClientError::Io { method: GET, error })?;
self.absorb_session(&response)?;
if response.status().is_success() {
Ok(response)
} else {
Err(refusal(GET, response).await)
}
}
fn remember_protocol(&self, result: &JsonObject) {
let Ok(parsed) = serde_json::from_str::<Value>(result.as_str()) else {
return;
};
let Some(version) = parsed
.get("protocolVersion")
.and_then(Value::as_str)
.and_then(|text| text.parse::<crate::domain::ProtocolVersion>().ok())
else {
return;
};
if version >= crate::domain::ProtocolVersion::V2025_06_18 {
*self
.protocol
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
Some(version.as_wire().into_owned());
}
}
fn absorb_session(&self, response: &reqwest::Response) -> Result<(), HttpClientError> {
let Some(raw) = response.headers().get(SESSION_HEADER) else {
return Ok(());
};
let text = String::from_utf8_lossy(raw.as_bytes());
let id = McpSessionIdError::validate_opaque(&text)
.map_err(|source| HttpClientError::SessionId { source })?;
*self
.session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Arc::from(id));
Ok(())
}
fn shape(response: &reqwest::Response) -> Result<Shape, HttpClientError> {
let found = media_type(response);
if is_event_stream(found.as_deref()) {
return Ok(Shape::Stream);
}
if is_json(found.as_deref()) {
return Ok(Shape::Json);
}
Err(media_error(response))
}
async fn read_streamed(
&self,
response: reqwest::Response,
wants: Option<&Value>,
) -> Result<Map<String, Value>, HttpClientError> {
let mut stream = WireStream::new(response);
let mut last_id: Option<String> = None;
let mut retry = DEFAULT_RETRY;
let mut resumes = 0_u32;
loop {
while let Some(event) = stream.next().await? {
if let Some(id) = event.id {
last_id = Some(id);
}
if let Some(interval) = event.retry {
retry = interval;
}
let Some(data) = event.data else {
continue;
};
let Ok(Value::Object(candidate)) = serde_json::from_str(&data) else {
continue;
};
if candidate.get("id") == wants && !is_request(&candidate) {
return Ok(candidate);
}
if is_request(&candidate)
&& let Some(Handler(handler)) = &self.handler
{
self.answer_request(handler.as_ref(), &candidate).await?;
continue;
}
self.unsolicited.hold(candidate);
}
if wants.is_none() {
return Ok(Map::new());
}
let Some(id) = last_id.clone() else {
return Err(HttpClientError::Unanswered { method: POST });
};
if resumes >= MAX_RESUMES {
return Err(HttpClientError::Unanswered { method: POST });
}
resumes = resumes.saturating_add(1);
tokio::time::sleep(retry).await;
let reopened = self.resume(&id).await?;
stream = WireStream::new(reopened);
}
}
async fn answer_request(
&self,
handler: &dyn ServerRequestHandler,
request: &Map<String, Value>,
) -> Result<(), HttpClientError> {
let method = request
.get("method")
.and_then(Value::as_str)
.unwrap_or_default();
let params = JsonObject::from_value(request.get("params").cloned().unwrap_or_default())
.unwrap_or_default();
let reply = match handler.handle(method, ¶ms).await {
Ok(result) => serde_json::json!({
"jsonrpc": "2.0",
"id": request.get("id").cloned().unwrap_or(Value::Null),
"result": serde_json::from_str::<Value>(result.as_str()).unwrap_or_default(),
}),
Err(refusal) => serde_json::json!({
"jsonrpc": "2.0",
"id": request.get("id").cloned().unwrap_or(Value::Null),
"error": { "code": refusal.code, "message": refusal.message },
}),
};
let body = serde_json::to_vec(&reply).map_err(|error| HttpClientError::Body {
method: "POST",
what: "encoded",
detail: error.to_string(),
})?;
self.send(POST, &body, false).await?;
Ok(())
}
async fn resume(&self, last_event_id: &str) -> Result<reqwest::Response, HttpClientError> {
self.get_stream(Some(last_event_id)).await
}
async fn get_stream(
&self,
last_event_id: Option<&str>,
) -> Result<reqwest::Response, HttpClientError> {
let mut builder = self
.inner
.get(&self.config.endpoint)
.header(reqwest::header::ACCEPT, ACCEPT);
if let Some(id) = last_event_id {
builder = builder.header(LAST_EVENT_ID, id);
}
if let Some(Auth(auth)) = &self.auth
&& let Some(token) = auth.bearer()
{
builder = builder.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"));
}
if let Some(id) = self.session_token() {
builder = builder.header(SESSION_HEADER, id.as_ref());
}
let protocol = self
.protocol
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
if let Some(protocol) = protocol {
builder = builder.header(PROTOCOL_HEADER, protocol);
}
let response = builder
.send()
.await
.map_err(|error| HttpClientError::Io { method: GET, error })?;
self.absorb_session(&response)?;
if response.status().is_success() {
Ok(response)
} else {
Err(refusal(GET, response).await)
}
}
pub async fn listen(&self) -> Result<(), HttpClientError> {
let mut stream = self.open_stream(None, true).await?;
let parked = Arc::clone(&self.unsolicited);
tokio::spawn(async move {
while let Ok(Some(event)) = stream.next_event().await {
if let Ok(Value::Object(frame)) = serde_json::from_str(event.data()) {
parked.hold(frame);
}
}
});
Ok(())
}
pub fn spawn_listener(self: &Arc<Self>) {
let weak = Arc::downgrade(self);
tokio::spawn(async move {
let mut last_id: Option<String> = None;
let mut retry = DEFAULT_RETRY;
let mut failures = 0_u32;
loop {
let Some(client) = weak.upgrade() else {
return;
};
let response = client.get_stream(last_id.as_deref()).await;
drop(client);
let response = match response {
Ok(response) => response,
Err(HttpClientError::Refused { status, .. })
if status == reqwest::StatusCode::METHOD_NOT_ALLOWED =>
{
return;
}
Err(_) => {
failures = failures.saturating_add(1);
if failures >= MAX_RESUMES {
return;
}
tokio::time::sleep(retry).await;
continue;
}
};
failures = 0;
let mut stream = WireStream::new(response);
while let Ok(Some(event)) = stream.next().await {
if let Some(id) = event.id {
last_id = Some(id);
}
if let Some(interval) = event.retry {
retry = interval;
}
let Some(data) = event.data else {
continue;
};
let Ok(Value::Object(frame)) = serde_json::from_str(&data) else {
continue;
};
let Some(client) = weak.upgrade() else {
return;
};
match &client.handler {
Some(Handler(handler)) if is_request(&frame) => {
let _ = client.answer_request(handler.as_ref(), &frame).await;
}
_ => client.unsolicited.hold(frame),
}
}
tokio::time::sleep(retry).await;
}
});
}
#[must_use]
pub fn take_pending(&self) -> Vec<Map<String, Value>> {
self.unsolicited.take()
}
async fn read_document(
&self,
response: reqwest::Response,
method: &'static str,
) -> Result<HttpResponse, HttpClientError> {
let shape = Self::shape(&response)?;
if shape == Shape::Stream {
return Err(media_error(&response));
}
let content_type = media_type(&response);
let status = response.status();
let body = response
.bytes()
.await
.map_err(|error| HttpClientError::Io { method, error })?;
Ok(HttpResponse {
status,
content_type,
body: body.to_vec(),
session: self.session_id(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Shape {
Json,
Stream,
}
pub struct HttpStream {
response: reqwest::Response,
reader: FrameReader,
ready: VecDeque<SseEvent>,
delivered: u64,
last: Option<EventId>,
}
impl fmt::Debug for HttpStream {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("HttpStream")
.field("delivered", &self.delivered)
.field("last", &self.last)
.field("buffered", &self.reader.buffered())
.finish_non_exhaustive()
}
}
impl HttpStream {
const fn new(response: reqwest::Response) -> Self {
Self {
response,
reader: FrameReader::new(),
ready: VecDeque::new(),
delivered: 0,
last: None,
}
}
pub async fn next_event(&mut self) -> Result<Option<SseEvent>, HttpClientError> {
loop {
if let Some(event) = self.ready.pop_front() {
return Ok(Some(self.record(event)));
}
let chunk = self
.response
.chunk()
.await
.map_err(|error| HttpClientError::Io { method: GET, error })?;
let Some(chunk) = chunk else {
return Ok(None);
};
self.ready.extend(self.reader.push(&chunk)?);
}
}
const fn record(&mut self, event: SseEvent) -> SseEvent {
self.delivered = self.delivered.saturating_add(1);
if let Some(id) = event.id() {
self.last = Some(id);
}
event
}
#[must_use]
pub const fn delivered(&self) -> u64 {
self.delivered
}
#[must_use]
pub const fn last_event_id(&self) -> Option<EventId> {
self.last
}
#[must_use]
pub const fn last_delivered(&self) -> Option<EventId> {
self.last
}
#[must_use]
pub const fn has_partial(&self) -> bool {
self.reader.buffered() > 0
}
}
#[derive(Debug)]
pub struct ResumableStream<'client> {
client: &'client HttpClient,
current: Option<HttpStream>,
cursor: Option<EventId>,
delivered: u64,
attempts: u32,
require_session: bool,
policy: Resumable,
finished: bool,
}
impl ResumableStream<'_> {
pub async fn next_event(&mut self) -> Result<Option<SseEvent>, HttpClientError> {
loop {
if self.finished {
return Ok(None);
}
let mut stream = loop {
if let Some(stream) = self.current.take() {
break stream;
}
match self
.client
.open_stream(self.cursor, self.require_session)
.await
{
Ok(opened) => self.current = Some(opened),
Err(error) if error.is_transient() => {
self.attempts = self.attempts.saturating_add(1);
if self.attempts > self.policy.budget.attempts() {
return Err(self.exhausted(error));
}
}
Err(error) => return Err(self.widen(error)),
}
};
match stream.next_event().await {
Ok(Some(event)) => {
self.delivered = self.delivered.saturating_add(1);
if let Some(id) = event.id() {
self.cursor = Some(id);
} else {
self.finished = true;
self.current = None;
return Ok(Some(event));
}
self.current = Some(stream);
return Ok(Some(event));
}
Ok(None) if stream.has_partial() => {
self.attempts = self.attempts.saturating_add(1);
if self.attempts > self.policy.budget.attempts() {
return Err(self.exhausted(HttpClientError::StreamEnded {
delivered: self.delivered,
}));
}
}
Ok(None) => {
self.finished = true;
self.current = None;
return Ok(None);
}
Err(error) if error.is_transient() => {
self.attempts = self.attempts.saturating_add(1);
if self.attempts > self.policy.budget.attempts() {
return Err(self.exhausted(error));
}
}
Err(error) => return Err(self.widen(error)),
}
}
}
#[must_use]
pub const fn delivered(&self) -> u64 {
self.delivered
}
#[must_use]
pub const fn cursor(&self) -> Option<EventId> {
self.cursor
}
#[must_use]
pub const fn reconnects(&self) -> u32 {
self.attempts
}
fn widen(&self, error: HttpClientError) -> HttpClientError {
if self.attempts == 0 {
error
} else {
HttpClientError::ResumeExhausted {
attempts: self.attempts,
delivered: self.delivered,
source: Box::new(error),
}
}
}
fn exhausted(&self, source: HttpClientError) -> HttpClientError {
HttpClientError::ResumeExhausted {
attempts: self.attempts.saturating_sub(1),
delivered: self.delivered,
source: Box::new(source),
}
}
}
#[derive(Debug)]
pub enum HttpReply {
Document(HttpResponse),
Stream(HttpStream),
}
#[derive(Debug, Clone)]
pub struct HttpResponse {
status: reqwest::StatusCode,
content_type: Option<String>,
body: Vec<u8>,
session: Option<McpSessionId>,
}
impl HttpResponse {
#[must_use]
pub const fn status(&self) -> reqwest::StatusCode {
self.status
}
#[must_use]
pub fn content_type(&self) -> Option<&str> {
self.content_type.as_deref()
}
#[must_use]
pub fn body(&self) -> &[u8] {
&self.body
}
#[must_use]
pub const fn session_id(&self) -> Option<McpSessionId> {
self.session
}
}
fn media_type(response: &reqwest::Response) -> Option<String> {
let raw = response
.headers()
.get(reqwest::header::CONTENT_TYPE)?
.to_str()
.ok()?;
Some(
raw.split(';')
.next()
.unwrap_or(raw)
.trim()
.to_ascii_lowercase(),
)
}
fn is_json(content_type: Option<&str>) -> bool {
content_type.is_some_and(|value| value == JSON || value.ends_with("+json"))
}
fn is_event_stream(content_type: Option<&str>) -> bool {
content_type == Some(EVENT_STREAM)
}
fn media_error(response: &reqwest::Response) -> HttpClientError {
media_type(response).map_or(HttpClientError::MissingMediaType, |got| {
HttpClientError::MediaType { got }
})
}
async fn refusal(method: &'static str, response: reqwest::Response) -> HttpClientError {
let status = response.status();
let content_type = media_type(&response);
let preview = response
.bytes()
.await
.map_or_else(|_| preview(&[]), |body| preview(&body));
HttpClientError::Refused {
method,
status,
content_type,
preview,
}
}
fn cursor_value(id: EventId) -> Result<reqwest::header::HeaderValue, HttpClientError> {
let text = id.to_string();
if text.len() > MAX_CURSOR {
return Err(HttpClientError::Frame {
source: SseError::MalformedId,
});
}
reqwest::header::HeaderValue::from_str(&text).map_err(|_| HttpClientError::Frame {
source: SseError::MalformedId,
})
}
const fn method_of(verb: &str) -> reqwest::Method {
match verb.as_bytes() {
b"POST" => reqwest::Method::POST,
b"DELETE" => reqwest::Method::DELETE,
_ => reqwest::Method::GET,
}
}
impl ClientTransport for HttpClient {
fn send(
&self,
method: &str,
params: &JsonObject,
) -> Pin<Box<dyn Future<Output = Result<JsonObject, TransportError>> + Send + '_>> {
let prepared = (|| {
let id = self.next_request_id();
let name = MethodName::new(method).map_err(unavailable)?;
let document = request_document(&id, &name, params).map_err(unavailable)?;
let envelope = document.as_object().cloned().ok_or_else(|| {
unavailable(HttpClientError::NotAMessage {
got: document.to_string(),
})
})?;
Ok::<_, TransportError>((id, envelope))
})();
let initializing = method == "initialize";
if initializing {
*self
.protocol
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
}
Box::pin(async move {
let (id, envelope) = prepared?;
let reply = self
.post_envelope(&envelope)
.await
.map_err(unavailable)?
.ok_or_else(|| unavailable("a request was answered with no body at all"))?;
let text = serde_json::to_string(&Value::Object(reply)).map_err(unavailable)?;
let result = reply_result(&text, &id).map_err(unavailable)?;
if initializing {
self.remember_protocol(&result);
}
Ok(result)
})
}
fn notify(
&self,
method: &str,
params: &JsonObject,
) -> Pin<Box<dyn Future<Output = Result<(), TransportError>> + Send + '_>> {
let prepared = (|| {
let name = MethodName::new(method).map_err(unavailable)?;
let document = notification_document(&name, params).map_err(unavailable)?;
let Some(envelope) = document.as_object().cloned() else {
return Err(unavailable(HttpClientError::NotAMessage {
got: document.to_string(),
}));
};
Ok::<_, TransportError>(envelope)
})();
Box::pin(async move {
let document = prepared?;
self.post_envelope(&document).await.map_err(unavailable)?;
Ok(())
})
}
}
fn unavailable(error: impl core::fmt::Display) -> TransportError {
TransportError::Unavailable(error.to_string())
}