first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
// This file contains code copied from the rmcp crate (https://github.com/modelcontextprotocol/rust-sdk),
|
||||
// originally located at `crates/rmcp/src/transport/common/auth/sse_client.rs`.
|
||||
// Used under the terms of the Apache License, Version 2.0.
|
||||
// See https://github.com/modelcontextprotocol/rust-sdk/blob/main/LICENSE for the full license text.
|
||||
|
||||
use http::Uri;
|
||||
use rmcp::transport::auth::AuthClient;
|
||||
|
||||
use super::sse_client::{SseClient, SseTransportError};
|
||||
|
||||
impl<C> SseClient for AuthClient<C>
|
||||
where
|
||||
C: SseClient,
|
||||
{
|
||||
type Error = SseTransportError<C::Error>;
|
||||
|
||||
async fn post_message(
|
||||
&self,
|
||||
uri: Uri,
|
||||
message: rmcp::model::ClientJsonRpcMessage,
|
||||
mut auth_token: Option<String>,
|
||||
) -> Result<(), SseTransportError<Self::Error>> {
|
||||
if auth_token.is_none() {
|
||||
auth_token = Some(self.get_access_token().await?);
|
||||
}
|
||||
self.http_client
|
||||
.post_message(uri, message, auth_token)
|
||||
.await
|
||||
.map_err(SseTransportError::Client)
|
||||
}
|
||||
|
||||
async fn get_stream(
|
||||
&self,
|
||||
uri: Uri,
|
||||
last_event_id: Option<String>,
|
||||
mut auth_token: Option<String>,
|
||||
) -> Result<super::client_side_sse::BoxedSseResponse, SseTransportError<Self::Error>> {
|
||||
if auth_token.is_none() {
|
||||
auth_token = Some(self.get_access_token().await?);
|
||||
}
|
||||
self.http_client
|
||||
.get_stream(uri, last_event_id, auth_token)
|
||||
.await
|
||||
.map_err(SseTransportError::Client)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
// This file contains code copied from the rmcp crate (https://github.com/modelcontextprotocol/rust-sdk),
|
||||
// originally located at `crates/rmcp/src/transport/common/client_side_sse.rs`.
|
||||
// Used under the terms of the Apache License, Version 2.0.
|
||||
// See https://github.com/modelcontextprotocol/rust-sdk/blob/main/LICENSE for the full license text.
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{ready, Poll};
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::stream::BoxStream;
|
||||
use futures::Stream;
|
||||
use rmcp::model::ServerJsonRpcMessage;
|
||||
use sse_stream::{Error as SseError, Sse};
|
||||
|
||||
pub type BoxedSseResponse = BoxStream<'static, Result<Sse, SseError>>;
|
||||
|
||||
pub trait SseRetryPolicy: std::fmt::Debug + Send + Sync {
|
||||
fn retry(&self, current_times: usize) -> Option<Duration>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FixedInterval {
|
||||
pub max_times: Option<usize>,
|
||||
pub duration: Duration,
|
||||
}
|
||||
|
||||
impl SseRetryPolicy for FixedInterval {
|
||||
fn retry(&self, current_times: usize) -> Option<Duration> {
|
||||
if let Some(max_times) = self.max_times {
|
||||
if current_times >= max_times {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(self.duration)
|
||||
}
|
||||
}
|
||||
|
||||
impl FixedInterval {
|
||||
pub const DEFAULT_MIN_DURATION: Duration = Duration::from_millis(1000);
|
||||
}
|
||||
|
||||
impl Default for FixedInterval {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_times: None,
|
||||
duration: Self::DEFAULT_MIN_DURATION,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExponentialBackoff {
|
||||
pub max_times: Option<usize>,
|
||||
pub base_duration: Duration,
|
||||
}
|
||||
|
||||
impl ExponentialBackoff {
|
||||
pub const DEFAULT_DURATION: Duration = Duration::from_millis(1000);
|
||||
}
|
||||
|
||||
impl Default for ExponentialBackoff {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_times: None,
|
||||
base_duration: Self::DEFAULT_DURATION,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SseRetryPolicy for ExponentialBackoff {
|
||||
fn retry(&self, current_times: usize) -> Option<Duration> {
|
||||
if let Some(max_times) = self.max_times {
|
||||
if current_times >= max_times {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(self.base_duration * (2u32.pow(current_times as u32)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct NeverRetry;
|
||||
|
||||
impl SseRetryPolicy for NeverRetry {
|
||||
fn retry(&self, _current_times: usize) -> Option<Duration> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstraction for SSE reconnection logic. Implementors can hook into
|
||||
/// [`handle_control_event`](Self::handle_control_event) to consume control
|
||||
/// frames (e.g. `event: endpoint`) that arrive when a server restarts an SSE
|
||||
/// stream. The default implementation is a no-op, keeping existing behaviour
|
||||
/// intact.
|
||||
pub(crate) trait SseStreamReconnect {
|
||||
type Error: std::error::Error;
|
||||
type Future: std::future::Future<Output = Result<BoxedSseResponse, Self::Error>> + Send;
|
||||
fn retry_connection(&mut self, last_event_id: Option<&str>) -> Self::Future;
|
||||
fn handle_control_event(&mut self, _event: &Sse) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn handle_stream_error(
|
||||
&mut self,
|
||||
error: &(dyn std::error::Error + 'static),
|
||||
last_event_id: Option<&str>,
|
||||
) {
|
||||
if let Some(id) = last_event_id {
|
||||
tracing::warn!(%id, "sse stream error: {error}");
|
||||
} else {
|
||||
tracing::warn!("sse stream error: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pin_project_lite::pin_project! {
|
||||
pub(crate) struct SseAutoReconnectStream<R>
|
||||
where R: SseStreamReconnect
|
||||
{
|
||||
retry_policy: Arc<dyn SseRetryPolicy>,
|
||||
last_event_id: Option<String>,
|
||||
server_retry_interval: Option<Duration>,
|
||||
connector: R,
|
||||
#[pin]
|
||||
state: SseAutoReconnectStreamState<R::Future>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: SseStreamReconnect> SseAutoReconnectStream<R> {
|
||||
pub fn new(
|
||||
stream: BoxedSseResponse,
|
||||
connector: R,
|
||||
retry_policy: Arc<dyn SseRetryPolicy>,
|
||||
) -> Self {
|
||||
Self {
|
||||
retry_policy,
|
||||
last_event_id: None,
|
||||
server_retry_interval: None,
|
||||
connector,
|
||||
state: SseAutoReconnectStreamState::Connected { stream },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pin_project_lite::pin_project! {
|
||||
#[project = SseAutoReconnectStreamStateProj]
|
||||
pub enum SseAutoReconnectStreamState<F> {
|
||||
Connected {
|
||||
#[pin]
|
||||
stream: BoxedSseResponse,
|
||||
},
|
||||
Retrying {
|
||||
retry_times: usize,
|
||||
#[pin]
|
||||
retrying: F,
|
||||
},
|
||||
WaitingNextRetry {
|
||||
#[pin]
|
||||
sleep: tokio::time::Sleep,
|
||||
retry_times: usize,
|
||||
},
|
||||
Terminated,
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Stream for SseAutoReconnectStream<R>
|
||||
where
|
||||
R: SseStreamReconnect,
|
||||
{
|
||||
type Item = Result<ServerJsonRpcMessage, R::Error>;
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.as_mut().project();
|
||||
let state = this.state.as_mut().project();
|
||||
let next_state = match state {
|
||||
SseAutoReconnectStreamStateProj::Connected { stream } => {
|
||||
match ready!(stream.poll_next(cx)) {
|
||||
Some(Ok(sse)) => {
|
||||
if let Some(new_server_retry) = sse.retry {
|
||||
*this.server_retry_interval =
|
||||
Some(Duration::from_millis(new_server_retry));
|
||||
}
|
||||
if let Some(ref event_id) = sse.id {
|
||||
*this.last_event_id = Some(event_id.clone());
|
||||
}
|
||||
// Only treat blank/`message` events as JSON-RPC payloads.
|
||||
// Other control frames (endpoint, ping, etc.) are passed to
|
||||
// the reconnection handler.
|
||||
let is_message_event =
|
||||
matches!(sse.event.as_deref(), None | Some("") | Some("message"));
|
||||
if !is_message_event {
|
||||
match this.connector.handle_control_event(&sse) {
|
||||
Ok(()) => return self.poll_next(cx),
|
||||
Err(e) => {
|
||||
this.state.set(SseAutoReconnectStreamState::Terminated);
|
||||
return Poll::Ready(Some(Err(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(data) = sse.data {
|
||||
match serde_json::from_str::<ServerJsonRpcMessage>(&data) {
|
||||
Err(e) => {
|
||||
let last_id = this.last_event_id.as_deref().unwrap_or("");
|
||||
tracing::debug!(last_event_id=%last_id, "failed to deserialize server message: {e}");
|
||||
return self.poll_next(cx);
|
||||
}
|
||||
Ok(message) => {
|
||||
return Poll::Ready(Some(Ok(message)));
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return self.poll_next(cx);
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
this.connector
|
||||
.handle_stream_error(&e, this.last_event_id.as_deref());
|
||||
let retrying = this
|
||||
.connector
|
||||
.retry_connection(this.last_event_id.as_deref());
|
||||
SseAutoReconnectStreamState::Retrying {
|
||||
retry_times: 0,
|
||||
retrying,
|
||||
}
|
||||
}
|
||||
None => {
|
||||
tracing::debug!("sse stream terminated");
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
SseAutoReconnectStreamStateProj::Retrying {
|
||||
retry_times,
|
||||
retrying,
|
||||
} => {
|
||||
let retry_result = ready!(retrying.poll(cx));
|
||||
match retry_result {
|
||||
Ok(new_stream) => SseAutoReconnectStreamState::Connected { stream: new_stream },
|
||||
Err(e) => {
|
||||
tracing::debug!("retry sse stream error: {e}");
|
||||
*retry_times += 1;
|
||||
if let Some(interval) = this.retry_policy.retry(*retry_times) {
|
||||
let interval = this
|
||||
.server_retry_interval
|
||||
.map(|server_retry_interval| server_retry_interval.max(interval))
|
||||
.unwrap_or(interval);
|
||||
let sleep = tokio::time::sleep(interval);
|
||||
SseAutoReconnectStreamState::WaitingNextRetry {
|
||||
sleep,
|
||||
retry_times: *retry_times,
|
||||
}
|
||||
} else {
|
||||
tracing::error!("sse stream error: {e}, max retry times reached");
|
||||
this.state.set(SseAutoReconnectStreamState::Terminated);
|
||||
return Poll::Ready(Some(Err(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SseAutoReconnectStreamStateProj::WaitingNextRetry { sleep, retry_times } => {
|
||||
ready!(sleep.poll(cx));
|
||||
let retrying = this
|
||||
.connector
|
||||
.retry_connection(this.last_event_id.as_deref());
|
||||
let retry_times = *retry_times;
|
||||
SseAutoReconnectStreamState::Retrying {
|
||||
retry_times,
|
||||
retrying,
|
||||
}
|
||||
}
|
||||
SseAutoReconnectStreamStateProj::Terminated => {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
};
|
||||
// Update the state.
|
||||
this.state.set(next_state);
|
||||
self.poll_next(cx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/// Legacy SSE client transport for MCP, preserved from the rmcp fork after upstream
|
||||
/// removed SSE transport support in v0.11.0. This allows Warp to continue connecting
|
||||
/// to MCP servers that only support the older SSE protocol.
|
||||
mod auth_impl;
|
||||
mod client_side_sse;
|
||||
mod reqwest_impl;
|
||||
mod sse_client;
|
||||
|
||||
pub use client_side_sse::{ExponentialBackoff, FixedInterval, NeverRetry, SseRetryPolicy};
|
||||
pub use sse_client::{SseClient, SseClientConfig, SseClientTransport, SseTransportError};
|
||||
@@ -0,0 +1,96 @@
|
||||
// This file contains code copied from the rmcp crate (https://github.com/modelcontextprotocol/rust-sdk),
|
||||
// originally located at `crates/rmcp/src/transport/common/reqwest/sse_client.rs`.
|
||||
// Used under the terms of the Apache License, Version 2.0.
|
||||
// See https://github.com/modelcontextprotocol/rust-sdk/blob/main/LICENSE for the full license text.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::StreamExt;
|
||||
use http::Uri;
|
||||
use reqwest::header::ACCEPT;
|
||||
use sse_stream::SseStream;
|
||||
|
||||
use super::sse_client::{SseClient, SseClientConfig, SseClientTransport, SseTransportError};
|
||||
|
||||
const HEADER_LAST_EVENT_ID: &str = "Last-Event-Id";
|
||||
const EVENT_STREAM_MIME_TYPE: &str = "text/event-stream";
|
||||
|
||||
impl From<reqwest::Error> for SseTransportError<reqwest::Error> {
|
||||
fn from(e: reqwest::Error) -> Self {
|
||||
SseTransportError::Client(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl SseClient for reqwest::Client {
|
||||
type Error = reqwest::Error;
|
||||
|
||||
async fn post_message(
|
||||
&self,
|
||||
uri: Uri,
|
||||
message: rmcp::model::ClientJsonRpcMessage,
|
||||
auth_token: Option<String>,
|
||||
) -> Result<(), SseTransportError<Self::Error>> {
|
||||
let mut request_builder = self.post(uri.to_string()).json(&message);
|
||||
if let Some(auth_header) = auth_token {
|
||||
request_builder = request_builder.bearer_auth(auth_header);
|
||||
}
|
||||
request_builder
|
||||
.send()
|
||||
.await
|
||||
.and_then(|resp| resp.error_for_status())
|
||||
.map_err(SseTransportError::from)
|
||||
.map(drop)
|
||||
}
|
||||
|
||||
async fn get_stream(
|
||||
&self,
|
||||
uri: Uri,
|
||||
last_event_id: Option<String>,
|
||||
auth_token: Option<String>,
|
||||
) -> Result<super::client_side_sse::BoxedSseResponse, SseTransportError<Self::Error>> {
|
||||
let mut request_builder = self
|
||||
.get(uri.to_string())
|
||||
.header(ACCEPT, EVENT_STREAM_MIME_TYPE);
|
||||
if let Some(auth_header) = auth_token {
|
||||
request_builder = request_builder.bearer_auth(auth_header);
|
||||
}
|
||||
if let Some(last_event_id) = last_event_id {
|
||||
request_builder = request_builder.header(HEADER_LAST_EVENT_ID, last_event_id);
|
||||
}
|
||||
let response = request_builder.send().await?;
|
||||
let response = response.error_for_status()?;
|
||||
match response.headers().get(reqwest::header::CONTENT_TYPE) {
|
||||
Some(ct) => {
|
||||
if !ct.as_bytes().starts_with(EVENT_STREAM_MIME_TYPE.as_bytes()) {
|
||||
return Err(SseTransportError::UnexpectedContentType(Some(
|
||||
String::from_utf8_lossy(ct.as_bytes()).to_string(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(SseTransportError::UnexpectedContentType(None));
|
||||
}
|
||||
}
|
||||
let event_stream = SseStream::from_byte_stream(response.bytes_stream()).boxed();
|
||||
Ok(event_stream)
|
||||
}
|
||||
}
|
||||
|
||||
impl SseClientTransport<reqwest::Client> {
|
||||
/// Creates a new transport using reqwest with the specified SSE endpoint.
|
||||
///
|
||||
/// This is a convenience method that creates a transport using the default
|
||||
/// reqwest client.
|
||||
pub async fn start(
|
||||
uri: impl Into<Arc<str>>,
|
||||
) -> Result<Self, SseTransportError<reqwest::Error>> {
|
||||
SseClientTransport::start_with_client(
|
||||
reqwest::Client::default(),
|
||||
SseClientConfig {
|
||||
sse_endpoint: uri.into(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
// This file contains code copied from the rmcp crate (https://github.com/modelcontextprotocol/rust-sdk),
|
||||
// originally located at `crates/rmcp/src/transport/sse_client.rs`.
|
||||
// Used under the terms of the Apache License, Version 2.0.
|
||||
// See https://github.com/modelcontextprotocol/rust-sdk/blob/main/LICENSE for the full license text.
|
||||
//
|
||||
// Reference: <https://html.spec.whatwg.org/multipage/server-sent-events.html>
|
||||
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use futures::future::BoxFuture;
|
||||
use futures::StreamExt;
|
||||
use http::Uri;
|
||||
use rmcp::model::{ClientJsonRpcMessage, ServerJsonRpcMessage};
|
||||
use rmcp::transport::Transport;
|
||||
use rmcp::RoleClient;
|
||||
use sse_stream::{Error as SseError, Sse};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::client_side_sse::{
|
||||
BoxedSseResponse, SseAutoReconnectStream, SseRetryPolicy, SseStreamReconnect,
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SseTransportError<E: std::error::Error + Send + Sync + 'static> {
|
||||
#[error("SSE error: {0}")]
|
||||
Sse(#[from] SseError),
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("Client error: {0}")]
|
||||
Client(E),
|
||||
#[error("unexpected end of stream")]
|
||||
UnexpectedEndOfStream,
|
||||
#[error("Unexpected content type: {0:?}")]
|
||||
UnexpectedContentType(Option<String>),
|
||||
#[error("Auth error: {0}")]
|
||||
Auth(#[from] rmcp::transport::AuthError),
|
||||
#[error("Invalid uri: {0}")]
|
||||
InvalidUri(#[from] http::uri::InvalidUri),
|
||||
#[error("Invalid uri parts: {0}")]
|
||||
InvalidUriParts(#[from] http::uri::InvalidUriParts),
|
||||
}
|
||||
|
||||
pub trait SseClient: Clone + Send + Sync + 'static {
|
||||
type Error: std::error::Error + Send + Sync + 'static;
|
||||
fn post_message(
|
||||
&self,
|
||||
uri: Uri,
|
||||
message: ClientJsonRpcMessage,
|
||||
auth_token: Option<String>,
|
||||
) -> impl std::future::Future<Output = Result<(), SseTransportError<Self::Error>>> + Send + '_;
|
||||
fn get_stream(
|
||||
&self,
|
||||
uri: Uri,
|
||||
last_event_id: Option<String>,
|
||||
auth_token: Option<String>,
|
||||
) -> impl std::future::Future<Output = Result<BoxedSseResponse, SseTransportError<Self::Error>>>
|
||||
+ Send
|
||||
+ '_;
|
||||
}
|
||||
|
||||
/// Helper that refreshes the POST endpoint whenever the server emits
|
||||
/// control frames during SSE reconnect; used together with
|
||||
/// [`SseAutoReconnectStream`].
|
||||
struct SseClientReconnect<C> {
|
||||
pub client: C,
|
||||
pub uri: Uri,
|
||||
pub message_endpoint: Arc<RwLock<Uri>>,
|
||||
}
|
||||
|
||||
impl<C: SseClient> SseStreamReconnect for SseClientReconnect<C> {
|
||||
type Error = SseTransportError<C::Error>;
|
||||
type Future = BoxFuture<'static, Result<BoxedSseResponse, Self::Error>>;
|
||||
fn retry_connection(&mut self, last_event_id: Option<&str>) -> Self::Future {
|
||||
let client = self.client.clone();
|
||||
let uri = self.uri.clone();
|
||||
let last_event_id = last_event_id.map(|s| s.to_owned());
|
||||
Box::pin(async move { client.get_stream(uri, last_event_id, None).await })
|
||||
}
|
||||
|
||||
fn handle_control_event(&mut self, event: &Sse) -> Result<(), Self::Error> {
|
||||
if event.event.as_deref() != Some("endpoint") {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(data) = event.data.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
// Servers typically resend the message POST endpoint (often with a new
|
||||
// sessionId) when a stream reconnects. Reuse `message_endpoint` helper
|
||||
// to resolve it and update the shared URI.
|
||||
let new_endpoint = message_endpoint(self.uri.clone(), data.clone())
|
||||
.map_err(SseTransportError::InvalidUri)?;
|
||||
*self
|
||||
.message_endpoint
|
||||
.write()
|
||||
.expect("message endpoint lock poisoned") = new_endpoint;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_stream_error(
|
||||
&mut self,
|
||||
error: &(dyn std::error::Error + 'static),
|
||||
last_event_id: Option<&str>,
|
||||
) {
|
||||
tracing::warn!(
|
||||
uri = %self.uri,
|
||||
last_event_id = last_event_id.unwrap_or(""),
|
||||
"sse stream error: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
type ServerMessageStream<C> = Pin<Box<SseAutoReconnectStream<SseClientReconnect<C>>>>;
|
||||
|
||||
/// A client-agnostic SSE transport for MCP that supports Server-Sent Events.
|
||||
///
|
||||
/// This transport allows you to choose your preferred HTTP client implementation
|
||||
/// by implementing the [`SseClient`] trait. The transport handles SSE streaming
|
||||
/// and automatic reconnection.
|
||||
pub struct SseClientTransport<C: SseClient> {
|
||||
client: C,
|
||||
config: SseClientConfig,
|
||||
/// Current POST endpoint; refreshed when the server sends new endpoint
|
||||
/// control frames.
|
||||
message_endpoint: Arc<RwLock<Uri>>,
|
||||
stream: Option<ServerMessageStream<C>>,
|
||||
}
|
||||
|
||||
impl<C: SseClient> Transport<RoleClient> for SseClientTransport<C> {
|
||||
type Error = SseTransportError<C::Error>;
|
||||
async fn receive(&mut self) -> Option<ServerJsonRpcMessage> {
|
||||
self.stream.as_mut()?.next().await?.ok()
|
||||
}
|
||||
fn send(
|
||||
&mut self,
|
||||
item: rmcp::service::TxJsonRpcMessage<RoleClient>,
|
||||
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send + 'static {
|
||||
let client = self.client.clone();
|
||||
let message_endpoint = self.message_endpoint.clone();
|
||||
async move {
|
||||
let uri = {
|
||||
let guard = message_endpoint
|
||||
.read()
|
||||
.expect("message endpoint lock poisoned");
|
||||
guard.clone()
|
||||
};
|
||||
client.post_message(uri, item, None).await
|
||||
}
|
||||
}
|
||||
async fn close(&mut self) -> Result<(), Self::Error> {
|
||||
self.stream.take();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: SseClient + std::fmt::Debug> std::fmt::Debug for SseClientTransport<C> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SseClientTransport")
|
||||
.field("client", &self.client)
|
||||
.field("config", &self.config)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: SseClient> SseClientTransport<C> {
|
||||
pub async fn start_with_client(
|
||||
client: C,
|
||||
config: SseClientConfig,
|
||||
) -> Result<Self, SseTransportError<C::Error>> {
|
||||
let sse_endpoint = config.sse_endpoint.as_ref().parse::<http::Uri>()?;
|
||||
|
||||
let mut sse_stream = client.get_stream(sse_endpoint.clone(), None, None).await?;
|
||||
let initial_message_endpoint = if let Some(endpoint) = config.use_message_endpoint.clone() {
|
||||
let ep = endpoint.parse::<http::Uri>()?;
|
||||
let mut sse_endpoint_parts = sse_endpoint.clone().into_parts();
|
||||
sse_endpoint_parts.path_and_query = ep.into_parts().path_and_query;
|
||||
Uri::from_parts(sse_endpoint_parts)?
|
||||
} else {
|
||||
// Wait for the endpoint event.
|
||||
loop {
|
||||
let sse = sse_stream
|
||||
.next()
|
||||
.await
|
||||
.ok_or(SseTransportError::UnexpectedEndOfStream)??;
|
||||
let Some("endpoint") = sse.event.as_deref() else {
|
||||
continue;
|
||||
};
|
||||
let ep = sse.data.unwrap_or_default();
|
||||
|
||||
break message_endpoint(sse_endpoint.clone(), ep)?;
|
||||
}
|
||||
};
|
||||
let message_endpoint = Arc::new(RwLock::new(initial_message_endpoint));
|
||||
|
||||
let stream = Box::pin(SseAutoReconnectStream::new(
|
||||
sse_stream,
|
||||
SseClientReconnect {
|
||||
client: client.clone(),
|
||||
uri: sse_endpoint.clone(),
|
||||
message_endpoint: message_endpoint.clone(),
|
||||
},
|
||||
config.retry_policy.clone(),
|
||||
));
|
||||
Ok(Self {
|
||||
client,
|
||||
config,
|
||||
message_endpoint,
|
||||
stream: Some(stream),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn message_endpoint(base: http::Uri, endpoint: String) -> Result<http::Uri, http::uri::InvalidUri> {
|
||||
// If endpoint is a full URL, parse and return it directly.
|
||||
if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
|
||||
return endpoint.parse::<http::Uri>();
|
||||
}
|
||||
|
||||
let mut base_parts = base.into_parts();
|
||||
let endpoint_clone = endpoint.clone();
|
||||
|
||||
if endpoint.starts_with("?") {
|
||||
// Query only - keep base path and append query.
|
||||
if let Some(base_path_and_query) = &base_parts.path_and_query {
|
||||
let base_path = base_path_and_query.path();
|
||||
base_parts.path_and_query = Some(format!("{base_path}{endpoint}").parse()?);
|
||||
} else {
|
||||
base_parts.path_and_query = Some(format!("/{endpoint}").parse()?);
|
||||
}
|
||||
} else {
|
||||
// Path (with optional query) - replace entire path_and_query.
|
||||
let path_to_use = if endpoint.starts_with("/") {
|
||||
endpoint // Use absolute path as-is.
|
||||
} else {
|
||||
format!("/{endpoint}") // Make relative path absolute.
|
||||
};
|
||||
base_parts.path_and_query = Some(path_to_use.parse()?);
|
||||
}
|
||||
|
||||
http::Uri::from_parts(base_parts).map_err(|_| endpoint_clone.parse::<http::Uri>().unwrap_err())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SseClientConfig {
|
||||
/// The client SSE endpoint URL.
|
||||
pub sse_endpoint: Arc<str>,
|
||||
pub retry_policy: Arc<dyn SseRetryPolicy>,
|
||||
/// If this is set, the client will use this endpoint to send messages and
|
||||
/// skip waiting for the endpoint event.
|
||||
pub use_message_endpoint: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for SseClientConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sse_endpoint: "".into(),
|
||||
retry_policy: Arc::new(super::client_side_sse::FixedInterval::default()),
|
||||
use_message_endpoint: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user