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:
parent
752b4388a3
commit
a3b42c4a83
5 changed files with 57 additions and 35 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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<
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue