feat: better http server support (#199)

* feat: better http server support

1. allow user get extensions in tool call.
2. allow user serve sse service without initialization.

* fix: fix document test
This commit is contained in:
4t145 2025-05-19 18:49:18 +08:00 • committed by GitHub
commit 076dc2c2cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 95 additions and 96 deletions

View file

@ -129,7 +129,7 @@ tracing-subscriber = { version = "0.3", features = [
async-trait = "0.1" async-trait = "0.1"
[[test]] [[test]]
name = "test_tool_macros" name = "test_tool_macros"
required-features = ["server"] required-features = ["server", "client"]
path = "tests/test_tool_macros.rs" path = "tests/test_tool_macros.rs"
[[test]] [[test]]

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
error::Error as McpError, error::Error as McpError,
model::*, model::*,
service::{Peer, RequestContext, RoleClient, Service, ServiceRole}, service::{RequestContext, RoleClient, Service, ServiceRole},
}; };
impl<H: ClientHandler> Service<RoleClient> for H { impl<H: ClientHandler> Service<RoleClient> for H {
@ -118,47 +118,16 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
std::future::ready(()) std::future::ready(())
} }
fn get_peer(&self) -> Option<Peer<RoleClient>>;
fn set_peer(&mut self, peer: Peer<RoleClient>);
fn get_info(&self) -> ClientInfo { fn get_info(&self) -> ClientInfo {
ClientInfo::default() ClientInfo::default()
} }
} }
/// Do nothing, just store the peer. /// Do nothing, with default client info.
impl ClientHandler for Option<Peer<RoleClient>> { impl ClientHandler for () {}
fn get_peer(&self) -> Option<Peer<RoleClient>> {
self.clone()
}
fn set_peer(&mut self, peer: Peer<RoleClient>) { /// Do nothing, with a specific client info.
*self = Some(peer);
}
}
/// Do nothing, even store the peer.
impl ClientHandler for () {
fn get_peer(&self) -> Option<Peer<RoleClient>> {
None
}
fn set_peer(&mut self, peer: Peer<RoleClient>) {
drop(peer);
}
}
/// Do nothing, even store the peer.
impl ClientHandler for ClientInfo { impl ClientHandler for ClientInfo {
fn get_peer(&self) -> Option<Peer<RoleClient>> {
None
}
fn set_peer(&mut self, peer: Peer<RoleClient>) {
drop(peer);
}
fn get_info(&self) -> ClientInfo { fn get_info(&self) -> ClientInfo {
self.clone() self.clone()
} }

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
error::Error as McpError, error::Error as McpError,
model::*, model::*,
service::{Peer, RequestContext, RoleServer, Service, ServiceRole}, service::{RequestContext, RoleServer, Service, ServiceRole},
}; };
mod resource; mod resource;
@ -108,6 +108,10 @@ pub trait ServerHandler: Sized + Send + Sync + 'static {
request: InitializeRequestParam, request: InitializeRequestParam,
context: RequestContext<RoleServer>, context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<InitializeResult, McpError>> + Send + '_ { ) -> impl Future<Output = Result<InitializeResult, McpError>> + Send + '_ {
if context.peer.peer_info().is_none() {
context.peer.set_peer_info(request);
}
let info = self.get_info();
std::future::ready(Ok(self.get_info())) std::future::ready(Ok(self.get_info()))
} }
fn complete( fn complete(
@ -210,14 +214,6 @@ pub trait ServerHandler: Sized + Send + Sync + 'static {
std::future::ready(()) std::future::ready(())
} }
fn get_peer(&self) -> Option<Peer<RoleServer>> {
None
}
fn set_peer(&mut self, peer: Peer<RoleServer>) {
drop(peer);
}
fn get_info(&self) -> ServerInfo { fn get_info(&self) -> ServerInfo {
ServerInfo::default() ServerInfo::default()
} }

View file

@ -86,6 +86,9 @@ impl<'service, S> ToolCallContext<'service, S> {
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
&self.name &self.name
} }
pub fn request_context(&self) -> &RequestContext<RoleServer> {
&self.request_context
}
} }
pub trait FromToolCallContextPart<'a, S>: Sized { pub trait FromToolCallContextPart<'a, S>: Sized {
@ -284,6 +287,39 @@ impl<'a, S> FromToolCallContextPart<'a, S> for JsonObject {
} }
} }
impl<'a, S> FromToolCallContextPart<'a, S> for crate::model::Extensions {
fn from_tool_call_context_part(
context: ToolCallContext<'a, S>,
) -> Result<(Self, ToolCallContext<'a, S>), crate::Error> {
let extensions = context.request_context.extensions.clone();
Ok((extensions, context))
}
}
pub struct Extension<T>(pub T);
impl<'a, S, T> FromToolCallContextPart<'a, S> for Extension<T>
where
T: Send + Sync + 'static + Clone,
{
fn from_tool_call_context_part(
context: ToolCallContext<'a, S>,
) -> Result<(Self, ToolCallContext<'a, S>), crate::Error> {
let extension = context
.request_context
.extensions
.get::<T>()
.cloned()
.ok_or_else(|| {
crate::Error::invalid_params(
format!("missing extension {}", std::any::type_name::<T>()),
None,
)
})?;
Ok((Extension(extension), context))
}
}
impl<'s, S> ToolCallContext<'s, S> { impl<'s, S> ToolCallContext<'s, S> {
pub fn invoke<H, A>(self, h: H) -> H::Fut pub fn invoke<H, A>(self, h: H) -> H::Fut
where where

View file

@ -303,7 +303,7 @@ pub struct Peer<R: ServiceRole> {
tx: mpsc::Sender<PeerSinkMessage<R>>, tx: mpsc::Sender<PeerSinkMessage<R>>,
request_id_provider: Arc<dyn RequestIdProvider>, request_id_provider: Arc<dyn RequestIdProvider>,
progress_token_provider: Arc<dyn ProgressTokenProvider>, progress_token_provider: Arc<dyn ProgressTokenProvider>,
info: Arc<R::PeerInfo>, info: Arc<tokio::sync::OnceCell<R::PeerInfo>>,
} }
impl<R: ServiceRole> std::fmt::Debug for Peer<R> { impl<R: ServiceRole> std::fmt::Debug for Peer<R> {
@ -333,7 +333,7 @@ impl<R: ServiceRole> Peer<R> {
const CLIENT_CHANNEL_BUFFER_SIZE: usize = 1024; const CLIENT_CHANNEL_BUFFER_SIZE: usize = 1024;
pub(crate) fn new( pub(crate) fn new(
request_id_provider: Arc<dyn RequestIdProvider>, request_id_provider: Arc<dyn RequestIdProvider>,
peer_info: R::PeerInfo, peer_info: Option<R::PeerInfo>,
) -> (Peer<R>, ProxyOutbound<R>) { ) -> (Peer<R>, ProxyOutbound<R>) {
let (tx, rx) = mpsc::channel(Self::CLIENT_CHANNEL_BUFFER_SIZE); let (tx, rx) = mpsc::channel(Self::CLIENT_CHANNEL_BUFFER_SIZE);
( (
@ -341,7 +341,7 @@ impl<R: ServiceRole> Peer<R> {
tx, tx,
request_id_provider, request_id_provider,
progress_token_provider: Arc::new(AtomicU32ProgressTokenProvider::default()), progress_token_provider: Arc::new(AtomicU32ProgressTokenProvider::default()),
info: peer_info.into(), info: Arc::new(tokio::sync::OnceCell::new_with(peer_info)),
}, },
rx, rx,
) )
@ -402,8 +402,16 @@ impl<R: ServiceRole> Peer<R> {
peer: self.clone(), peer: self.clone(),
}) })
} }
pub fn peer_info(&self) -> &R::PeerInfo { pub fn peer_info(&self) -> Option<&R::PeerInfo> {
&self.info self.info.get()
}
pub fn set_peer_info(&self, info: R::PeerInfo) {
if self.info.initialized() {
tracing::warn!("trying to set peer info, which is already initialized");
} else {
let _ = self.info.set(info);
}
} }
pub fn is_transport_closed(&self) -> bool { pub fn is_transport_closed(&self) -> bool {
@ -469,7 +477,7 @@ pub struct RequestContext<R: ServiceRole> {
pub async fn serve_directly<R, S, T, E, A>( pub async fn serve_directly<R, S, T, E, A>(
service: S, service: S,
transport: T, transport: T,
peer_info: R::PeerInfo, peer_info: Option<R::PeerInfo>,
) -> RunningService<R, S> ) -> RunningService<R, S>
where where
R: ServiceRole, R: ServiceRole,
@ -484,7 +492,7 @@ where
pub async fn serve_directly_with_ct<R, S, T, E, A>( pub async fn serve_directly_with_ct<R, S, T, E, A>(
service: S, service: S,
transport: T, transport: T,
peer_info: R::PeerInfo, peer_info: Option<R::PeerInfo>,
ct: CancellationToken, ct: CancellationToken,
) -> RunningService<R, S> ) -> RunningService<R, S>
where where

View file

@ -174,7 +174,7 @@ where
error, error,
context: "send initialized notification".into(), context: "send initialized notification".into(),
})?; })?;
let (peer, peer_rx) = Peer::new(id_provider, initialize_result); let (peer, peer_rx) = Peer::new(id_provider, Some(initialize_result));
Ok(serve_inner(service, transport, peer, peer_rx, ct).await) Ok(serve_inner(service, transport, peer, peer_rx, ct).await)
} }

View file

@ -156,7 +156,7 @@ where
ClientJsonRpcMessage::request(request, id), ClientJsonRpcMessage::request(request, id),
))); )));
}; };
let (peer, peer_rx) = Peer::new(id_provider, peer_info.params.clone()); let (peer, peer_rx) = Peer::new(id_provider, Some(peer_info.params.clone()));
let context = RequestContext { let context = RequestContext {
ct: ct.child_token(), ct: ct.child_token(),
id: id.clone(), id: id.clone(),

View file

@ -57,7 +57,7 @@
//! //!
//! // create transport from std io //! // create transport from std io
//! async fn io() -> Result<(), Box<dyn std::error::Error>> { //! async fn io() -> Result<(), Box<dyn std::error::Error>> {
//! let client = None.serve((tokio::io::stdin(), tokio::io::stdout())).await?; //! let client = ().serve((tokio::io::stdin(), tokio::io::stdout())).await?;
//! let tools = client.peer().list_tools(Default::default()).await?; //! let tools = client.peer().list_tools(Default::default()).await?;
//! println!("{:?}", tools); //! println!("{:?}", tools);
//! Ok(()) //! Ok(())

View file

@ -18,7 +18,7 @@ use tracing::Instrument;
use crate::{ use crate::{
RoleServer, Service, RoleServer, Service,
model::ClientJsonRpcMessage, model::ClientJsonRpcMessage,
service::{RxJsonRpcMessage, TxJsonRpcMessage}, service::{RxJsonRpcMessage, TxJsonRpcMessage, serve_directly_with_ct},
transport::common::axum::{DEFAULT_AUTO_PING_INTERVAL, SessionId, session_id}, transport::common::axum::{DEFAULT_AUTO_PING_INTERVAL, SessionId, session_id},
}; };
@ -67,7 +67,7 @@ async fn post_event_handler(
parts: Parts, parts: Parts,
Json(mut message): Json<ClientJsonRpcMessage>, Json(mut message): Json<ClientJsonRpcMessage>,
) -> Result<StatusCode, StatusCode> { ) -> Result<StatusCode, StatusCode> {
tracing::debug!(session_id, ?message, "new client message"); tracing::debug!(session_id, ?parts, ?message, "new client message");
let tx = { let tx = {
let rg = app.txs.read().await; let rg = app.txs.read().await;
rg.get(session_id.as_str()) rg.get(session_id.as_str())
@ -84,9 +84,10 @@ async fn post_event_handler(
async fn sse_handler( async fn sse_handler(
State(app): State<App>, State(app): State<App>,
parts: Parts,
) -> Result<Sse<impl Stream<Item = Result<Event, io::Error>>>, Response<String>> { ) -> Result<Sse<impl Stream<Item = Result<Event, io::Error>>>, Response<String>> {
let session = session_id(); let session = session_id();
tracing::info!(%session, "sse connection"); tracing::info!(%session, ?parts, "sse connection");
use tokio_stream::{StreamExt, wrappers::ReceiverStream}; use tokio_stream::{StreamExt, wrappers::ReceiverStream};
use tokio_util::sync::PollSender; use tokio_util::sync::PollSender;
let (from_client_tx, from_client_rx) = tokio::sync::mpsc::channel(64); let (from_client_tx, from_client_rx) = tokio::sync::mpsc::channel(64);
@ -300,6 +301,27 @@ impl SseServer {
ct ct
} }
/// This allows you to skip the initialization steps for incoming request.
pub fn with_service_directly<S, F>(mut self, service_provider: F) -> CancellationToken
where
S: Service<RoleServer>,
F: Fn() -> S + Send + 'static,
{
let ct = self.config.ct.clone();
tokio::spawn(async move {
while let Some(transport) = self.next_transport().await {
let service = service_provider();
let ct = self.config.ct.child_token();
tokio::spawn(async move {
let server = serve_directly_with_ct(service, transport, None, ct).await;
server.waiting().await?;
tokio::io::Result::Ok(())
});
}
});
ct
}
pub fn cancel(&self) { pub fn cancel(&self) {
self.config.ct.cancel(); self.config.ct.cancel();
} }

View file

@ -4,16 +4,14 @@ use std::{
}; };
use rmcp::{ use rmcp::{
ClientHandler, Error as McpError, RoleClient, RoleServer, ServerHandler, ClientHandler, Error as McpError, RoleClient, RoleServer, ServerHandler, model::*,
model::*, service::RequestContext,
service::{Peer, RequestContext},
}; };
use serde_json::json; use serde_json::json;
use tokio::sync::Notify; use tokio::sync::Notify;
#[derive(Clone)] #[derive(Clone)]
pub struct TestClientHandler { pub struct TestClientHandler {
pub peer: Option<Peer<RoleClient>>,
pub honor_this_server: bool, pub honor_this_server: bool,
pub honor_all_servers: bool, pub honor_all_servers: bool,
pub receive_signal: Arc<Notify>, pub receive_signal: Arc<Notify>,
@ -24,7 +22,6 @@ impl TestClientHandler {
#[allow(dead_code)] #[allow(dead_code)]
pub fn new(honor_this_server: bool, honor_all_servers: bool) -> Self { pub fn new(honor_this_server: bool, honor_all_servers: bool) -> Self {
Self { Self {
peer: None,
honor_this_server, honor_this_server,
honor_all_servers, honor_all_servers,
receive_signal: Arc::new(Notify::new()), receive_signal: Arc::new(Notify::new()),
@ -40,7 +37,6 @@ impl TestClientHandler {
received_messages: Arc<Mutex<Vec<LoggingMessageNotificationParam>>>, received_messages: Arc<Mutex<Vec<LoggingMessageNotificationParam>>>,
) -> Self { ) -> Self {
Self { Self {
peer: None,
honor_this_server, honor_this_server,
honor_all_servers, honor_all_servers,
receive_signal, receive_signal,
@ -50,14 +46,6 @@ impl TestClientHandler {
} }
impl ClientHandler for TestClientHandler { impl ClientHandler for TestClientHandler {
fn get_peer(&self) -> Option<Peer<RoleClient>> {
self.peer.clone()
}
fn set_peer(&mut self, peer: Peer<RoleClient>) {
self.peer = Some(peer);
}
async fn create_message( async fn create_message(
&self, &self,
params: CreateMessageRequestParam, params: CreateMessageRequestParam,

View file

@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use rmcp::{ use rmcp::{
ClientHandler, Peer, RoleClient, ServerHandler, ServiceExt, ClientHandler, ServerHandler, ServiceExt,
model::{ model::{
ResourceUpdatedNotificationParam, ServerCapabilities, ServerInfo, SubscribeRequestParam, ResourceUpdatedNotificationParam, ServerCapabilities, ServerInfo, SubscribeRequestParam,
}, },
@ -49,7 +49,6 @@ impl ServerHandler for Server {
pub struct Client { pub struct Client {
receive_signal: Arc<Notify>, receive_signal: Arc<Notify>,
peer: Option<Peer<RoleClient>>,
} }
impl ClientHandler for Client { impl ClientHandler for Client {
@ -58,14 +57,6 @@ impl ClientHandler for Client {
tracing::info!("Resource updated: {}", uri); tracing::info!("Resource updated: {}", uri);
self.receive_signal.notify_one(); self.receive_signal.notify_one();
} }
fn set_peer(&mut self, peer: Peer<RoleClient>) {
self.peer.replace(peer);
}
fn get_peer(&self) -> Option<Peer<RoleClient>> {
self.peer.clone()
}
} }
#[tokio::test] #[tokio::test]
@ -85,7 +76,6 @@ async fn test_server_notification() -> anyhow::Result<()> {
}); });
let receive_signal = Arc::new(Notify::new()); let receive_signal = Arc::new(Notify::new());
let client = Client { let client = Client {
peer: Default::default(),
receive_signal: receive_signal.clone(), receive_signal: receive_signal.clone(),
} }
.serve(client_transport) .serve(client_transport)

View file

@ -3,7 +3,7 @@
use std::sync::Arc; use std::sync::Arc;
use rmcp::{ use rmcp::{
ClientHandler, Peer, RoleClient, ServerHandler, ServiceExt, ClientHandler, ServerHandler, ServiceExt,
handler::server::tool::ToolCallContext, handler::server::tool::ToolCallContext,
model::{CallToolRequestParam, ClientInfo}, model::{CallToolRequestParam, ClientInfo},
tool, tool,
@ -242,22 +242,12 @@ fn test_optional_field_schema_generation_via_macro() {
// Define a dummy client handler // Define a dummy client handler
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
struct DummyClientHandler { struct DummyClientHandler {}
peer: Option<Peer<RoleClient>>,
}
impl ClientHandler for DummyClientHandler { impl ClientHandler for DummyClientHandler {
fn get_info(&self) -> ClientInfo { fn get_info(&self) -> ClientInfo {
ClientInfo::default() ClientInfo::default()
} }
fn set_peer(&mut self, peer: Peer<RoleClient>) {
self.peer = Some(peer);
}
fn get_peer(&self) -> Option<Peer<RoleClient>> {
self.peer.clone()
}
} }
#[tokio::test] #[tokio::test]

View file

@ -21,7 +21,7 @@ async fn main() -> anyhow::Result<()> {
let ct = SseServer::serve(BIND_ADDRESS.parse()?) let ct = SseServer::serve(BIND_ADDRESS.parse()?)
.await? .await?
.with_service(Counter::new); .with_service_directly(Counter::new);
tokio::signal::ctrl_c().await?; tokio::signal::ctrl_c().await?;
ct.cancel(); ct.cancel();