fix: sse endpoint build follow js's new URL(url, base) (#197)

* fix: sse endpoint build follow js's `new URL(url, base)`

* fix: fix feature denpencies
This commit is contained in:
4t145 2025-05-19 13:09:00 +08:00 • committed by GitHub
commit a3b42c4a83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 57 additions and 35 deletions

View file

@ -40,13 +40,14 @@ reqwest = { version = "0.12", default-features = false, features = [
"stream", "stream",
], optional = true } ], optional = true }
sse-stream = { version = "0.1.4", optional = true } sse-stream = { version = "0.1.4", optional = true }
http = { version = "1", optional = true }
url = { version = "2.4", optional = true } url = { version = "2.4", optional = true }
# For tower compatibility # For tower compatibility
tower-service = { version = "0.3", optional = true } tower-service = { version = "0.3", optional = true }
# for child process transport # for child process transport
process-wrap = { version = "8.2", features = ["tokio1"], optional = true} process-wrap = { version = "8.2", features = ["tokio1"], optional = true }
# for ws transport # for ws transport
# tokio-tungstenite ={ version = "0.26", optional = true } # tokio-tungstenite ={ version = "0.26", optional = true }
@ -75,7 +76,7 @@ reqwest-tls-no-provider = ["__reqwest", "reqwest?/rustls-tls-no-provider"]
axum = ["dep:axum"] axum = ["dep:axum"]
# SSE client # SSE client
client-side-sse = ["dep:sse-stream"] client-side-sse = ["dep:sse-stream", "dep:http"]
transport-sse-client = ["client-side-sse", "transport-worker"] transport-sse-client = ["client-side-sse", "transport-worker"]
@ -83,10 +84,7 @@ transport-worker = ["dep:tokio-stream"]
# Streamable HTTP client # Streamable HTTP client
transport-streamable-http-client = [ transport-streamable-http-client = ["client-side-sse", "transport-worker"]
"client-side-sse",
"transport-worker",
]
transport-async-rw = ["tokio/io-util", "tokio-util/codec"] transport-async-rw = ["tokio/io-util", "tokio-util/codec"]
@ -98,6 +96,7 @@ transport-child-process = [
] ]
transport-sse-server = [ transport-sse-server = [
"transport-async-rw", "transport-async-rw",
"transport-worker",
"axum", "axum",
"dep:rand", "dep:rand",
"dep:tokio-stream", "dep:tokio-stream",

View file

@ -1,3 +1,5 @@
use http::Uri;
use crate::transport::{ use crate::transport::{
auth::AuthClient, auth::AuthClient,
sse_client::{SseClient, SseTransportError}, sse_client::{SseClient, SseTransportError},
@ -10,7 +12,7 @@ where
async fn post_message( async fn post_message(
&self, &self,
uri: std::sync::Arc<str>, uri: Uri,
message: crate::model::ClientJsonRpcMessage, message: crate::model::ClientJsonRpcMessage,
mut auth_token: Option<String>, mut auth_token: Option<String>,
) -> Result<(), SseTransportError<Self::Error>> { ) -> Result<(), SseTransportError<Self::Error>> {
@ -25,7 +27,7 @@ where
async fn get_stream( async fn get_stream(
&self, &self,
uri: std::sync::Arc<str>, uri: Uri,
last_event_id: Option<String>, last_event_id: Option<String>,
mut auth_token: Option<String>, mut auth_token: Option<String>,
) -> Result< ) -> Result<

View file

@ -1,6 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use futures::StreamExt; use futures::StreamExt;
use http::Uri;
use reqwest::header::ACCEPT; use reqwest::header::ACCEPT;
use sse_stream::SseStream; use sse_stream::SseStream;
@ -15,11 +16,11 @@ impl SseClient for reqwest::Client {
async fn post_message( async fn post_message(
&self, &self,
uri: std::sync::Arc<str>, uri: Uri,
message: crate::model::ClientJsonRpcMessage, message: crate::model::ClientJsonRpcMessage,
auth_token: Option<String>, auth_token: Option<String>,
) -> Result<(), SseTransportError<Self::Error>> { ) -> Result<(), SseTransportError<Self::Error>> {
let mut request_builder = self.post(uri.as_ref()).json(&message); let mut request_builder = self.post(uri.to_string()).json(&message);
if let Some(auth_header) = auth_token { if let Some(auth_header) = auth_token {
request_builder = request_builder.bearer_auth(auth_header); request_builder = request_builder.bearer_auth(auth_header);
} }
@ -33,7 +34,7 @@ impl SseClient for reqwest::Client {
async fn get_stream( async fn get_stream(
&self, &self,
uri: std::sync::Arc<str>, uri: Uri,
last_event_id: Option<String>, last_event_id: Option<String>,
auth_token: Option<String>, auth_token: Option<String>,
) -> Result< ) -> Result<
@ -41,7 +42,7 @@ impl SseClient for reqwest::Client {
SseTransportError<Self::Error>, SseTransportError<Self::Error>,
> { > {
let mut request_builder = self let mut request_builder = self
.get(uri.as_ref()) .get(uri.to_string())
.header(ACCEPT, EVENT_STREAM_MIME_TYPE); .header(ACCEPT, EVENT_STREAM_MIME_TYPE);
if let Some(auth_header) = auth_token { if let Some(auth_header) = auth_token {
request_builder = request_builder.bearer_auth(auth_header); request_builder = request_builder.bearer_auth(auth_header);
@ -73,7 +74,7 @@ impl SseClientTransport<reqwest::Client> {
SseClientTransport::start_with_client( SseClientTransport::start_with_client(
reqwest::Client::default(), reqwest::Client::default(),
SseClientConfig { SseClientConfig {
uri: uri.into(), sse_endpoint: uri.into(),
..Default::default() ..Default::default()
}, },
) )

View file

@ -2,6 +2,7 @@
use std::{pin::Pin, sync::Arc}; use std::{pin::Pin, sync::Arc};
use futures::{StreamExt, future::BoxFuture}; use futures::{StreamExt, future::BoxFuture};
use http::Uri;
use reqwest::header::HeaderValue; use reqwest::header::HeaderValue;
use sse_stream::Error as SseError; use sse_stream::Error as SseError;
use thiserror::Error; use thiserror::Error;
@ -32,6 +33,10 @@ pub enum SseTransportError<E: std::error::Error + Send + Sync + 'static> {
#[cfg_attr(docsrs, doc(cfg(feature = "auth")))] #[cfg_attr(docsrs, doc(cfg(feature = "auth")))]
#[error("Auth error: {0}")] #[error("Auth error: {0}")]
Auth(#[from] crate::transport::auth::AuthError), Auth(#[from] crate::transport::auth::AuthError),
#[error("Invalid uri: {0}")]
InvalidUri(#[from] http::uri::InvalidUri),
#[error("Invalid uri parts: {0}")]
InvalidUriParts(#[from] http::uri::InvalidUriParts),
} }
impl From<reqwest::Error> for SseTransportError<reqwest::Error> { impl From<reqwest::Error> for SseTransportError<reqwest::Error> {
@ -44,13 +49,13 @@ pub trait SseClient: Clone + Send + Sync + 'static {
type Error: std::error::Error + Send + Sync + 'static; type Error: std::error::Error + Send + Sync + 'static;
fn post_message( fn post_message(
&self, &self,
uri: Arc<str>, uri: Uri,
message: ClientJsonRpcMessage, message: ClientJsonRpcMessage,
auth_token: Option<String>, auth_token: Option<String>,
) -> impl Future<Output = Result<(), SseTransportError<Self::Error>>> + Send + '_; ) -> impl Future<Output = Result<(), SseTransportError<Self::Error>>> + Send + '_;
fn get_stream( fn get_stream(
&self, &self,
uri: Arc<str>, uri: Uri,
last_event_id: Option<String>, last_event_id: Option<String>,
auth_token: Option<String>, auth_token: Option<String>,
) -> impl Future<Output = Result<BoxedSseResponse, SseTransportError<Self::Error>>> + Send + '_; ) -> impl Future<Output = Result<BoxedSseResponse, SseTransportError<Self::Error>>> + Send + '_;
@ -58,7 +63,7 @@ pub trait SseClient: Clone + Send + Sync + 'static {
struct SseClientReconnect<C> { struct SseClientReconnect<C> {
pub client: C, pub client: C,
pub uri: Arc<str>, pub uri: Uri,
} }
impl<C: SseClient> SseStreamReconnect for SseClientReconnect<C> { impl<C: SseClient> SseStreamReconnect for SseClientReconnect<C> {
@ -75,7 +80,7 @@ type ServerMessageStream<C> = Pin<Box<SseAutoReconnectStream<SseClientReconnect<
pub struct SseClientTransport<C: SseClient> { pub struct SseClientTransport<C: SseClient> {
client: C, client: C,
config: SseClientConfig, config: SseClientConfig,
post_uri: Arc<str>, message_endpoint: Uri,
stream: Option<ServerMessageStream<C>>, stream: Option<ServerMessageStream<C>>,
} }
@ -89,7 +94,7 @@ impl<C: SseClient> Transport<RoleClient> for SseClientTransport<C> {
item: crate::service::TxJsonRpcMessage<RoleClient>, item: crate::service::TxJsonRpcMessage<RoleClient>,
) -> impl Future<Output = Result<(), Self::Error>> + Send + 'static { ) -> impl Future<Output = Result<(), Self::Error>> + Send + 'static {
let client = self.client.clone(); let client = self.client.clone();
let uri = self.post_uri.clone(); let uri = self.message_endpoint.clone();
async move { client.post_message(uri, item, None).await } async move { client.post_message(uri, item, None).await }
} }
async fn close(&mut self) -> Result<(), Self::Error> { async fn close(&mut self) -> Result<(), Self::Error> {
@ -112,9 +117,11 @@ impl<C: SseClient> SseClientTransport<C> {
client: C, client: C,
config: SseClientConfig, config: SseClientConfig,
) -> Result<Self, SseTransportError<C::Error>> { ) -> Result<Self, SseTransportError<C::Error>> {
let mut sse_stream = client.get_stream(config.uri.clone(), None, None).await?; let sse_endpoint = config.sse_endpoint.as_ref().parse::<http::Uri>()?;
let endpoint = if let Some(endpoint) = config.use_endpoint.clone() {
endpoint let mut sse_stream = client.get_stream(sse_endpoint.clone(), None, None).await?;
let message_endpoint = if let Some(endpoint) = config.use_message_endpoint.clone() {
endpoint.parse::<http::Uri>()?
} else { } else {
// wait the endpoint event // wait the endpoint event
loop { loop {
@ -125,27 +132,29 @@ impl<C: SseClient> SseClientTransport<C> {
let Some("endpoint") = sse.event.as_deref() else { let Some("endpoint") = sse.event.as_deref() else {
continue; continue;
}; };
break sse.data.unwrap_or_default(); let sse_endpoint = sse.data.unwrap_or_default();
break sse_endpoint.parse::<http::Uri>()?;
} }
}; };
let post_uri: Arc<str> = format!(
"{}/{}", // sse: <authority><sse_pq> -> <authority><message_pq>
config.uri.trim_end_matches("/"), let message_endpoint = {
endpoint.trim_start_matches("/") let mut sse_endpoint_parts = sse_endpoint.clone().into_parts();
) sse_endpoint_parts.path_and_query = message_endpoint.into_parts().path_and_query;
.into(); Uri::from_parts(sse_endpoint_parts)?
};
let stream = Box::pin(SseAutoReconnectStream::new( let stream = Box::pin(SseAutoReconnectStream::new(
sse_stream, sse_stream,
SseClientReconnect { SseClientReconnect {
client: client.clone(), client: client.clone(),
uri: config.uri.clone(), uri: sse_endpoint.clone(),
}, },
config.retry_policy.clone(), config.retry_policy.clone(),
)); ));
Ok(Self { Ok(Self {
client, client,
config, config,
post_uri, message_endpoint,
stream: Some(stream), stream: Some(stream),
}) })
} }
@ -153,18 +162,29 @@ impl<C: SseClient> SseClientTransport<C> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SseClientConfig { pub struct SseClientConfig {
pub uri: Arc<str>, /// client sse endpoint
///
/// # How this client resolve the message endpoint
/// if sse_endpoint has this format: `<schema><authority?><sse_pq>`,
/// then the message endpoint will be `<schema><authority?><message_pq>`.
///
/// For example, if you config the sse_endpoint as `http://example.com/some_path/sse`,
/// and the server send the message endpoint event as `message?session_id=123`,
/// then the message endpoint will be `http://example.com/message`.
///
/// This follow the rules of JavaScript's [`new URL(url, base)`](https://developer.mozilla.org/zh-CN/docs/Web/API/URL/URL)
pub sse_endpoint: Arc<str>,
pub retry_policy: Arc<dyn SseRetryPolicy>, pub retry_policy: Arc<dyn SseRetryPolicy>,
/// if this is settled, the client will use this endpoint to send message and skip get the endpoint event /// if this is settled, the client will use this endpoint to send message and skip get the endpoint event
pub use_endpoint: Option<String>, pub use_message_endpoint: Option<String>,
} }
impl Default for SseClientConfig { impl Default for SseClientConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
uri: "".into(), sse_endpoint: "".into(),
retry_policy: Arc::new(super::common::client_side_sse::FixedInterval::default()), retry_policy: Arc::new(super::common::client_side_sse::FixedInterval::default()),
use_endpoint: None, use_message_endpoint: None,
} }
} }
} }

View file

@ -147,7 +147,7 @@ async fn main() -> Result<()> {
let transport = SseClientTransport::start_with_client( let transport = SseClientTransport::start_with_client(
client, client,
SseClientConfig { SseClientConfig {
uri: MCP_SSE_URL.into(), sse_endpoint: MCP_SSE_URL.into(),
..Default::default() ..Default::default()
}, },
) )