From 1573b3a1a7c7ba1e29331167b36c0a9dbda3034c Mon Sep 17 00:00:00 2001 From: Ali Hashemi Date: Wed, 15 Oct 2025 08:32:31 -0300 Subject: [PATCH 1/4] refactor: eventstore trait --- crates/rust-mcp-transport/src/event_store.rs | 68 +++++++++++++++++-- .../src/event_store/in_memory_event_store.rs | 9 ++- .../src/message_dispatcher.rs | 18 +++-- 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/crates/rust-mcp-transport/src/event_store.rs b/crates/rust-mcp-transport/src/event_store.rs index fdc0734..e3c78be 100644 --- a/crates/rust-mcp-transport/src/event_store.rs +++ b/crates/rust-mcp-transport/src/event_store.rs @@ -1,8 +1,8 @@ mod in_memory_event_store; -use async_trait::async_trait; -pub use in_memory_event_store::*; use crate::{EventId, SessionId, StreamId}; +use async_trait::async_trait; +pub use in_memory_event_store::*; #[derive(Debug, Clone)] pub struct EventStoreMessages { @@ -11,17 +11,77 @@ pub struct EventStoreMessages { pub messages: Vec, } +/// Trait defining the interface for event storage and retrieval, used by the MCP server +/// to store and replay events for state reconstruction after client reconnection #[async_trait] pub trait EventStore: Send + Sync { + /// Stores a new event in the store and returns the generated event ID. + /// For MCP, this stores protocol messages, timestamp is the number of microseconds since UNIX_EPOCH. + /// The timestamp helps determine the order in which messages arrived. + /// + /// # Parameters + /// - `session_id`: The session identifier for the event. + /// - `stream_id`: The stream identifier within the session. + /// - `timestamp`: The u128 timestamp of the event. + /// - `message`: The event payload as json string. + /// + /// # Returns + /// - `Ok(EventId)`: The generated ID (format: session_id:stream_id:timestamp) on success. + /// - `Err(Self::Error)`: If input is invalid or storage fails. async fn store_event( &self, session_id: SessionId, stream_id: StreamId, - time_stamp: u128, + timestamp: u128, message: String, - ) -> EventId; + ) -> Option; + + /// Removes all events associated with a given session ID. + /// Used to clean up all events for a session when it is no longer needed (e.g., session ended). + /// + /// # Parameters + /// - `session_id`: The session ID whose events should be removed. + /// async fn remove_by_session_id(&self, session_id: SessionId); + /// Removes all events for a specific stream within a session. + /// Useful for cleaning up a specific stream without affecting others. + /// + /// # Parameters + /// - `session_id`: The session ID containing the stream. + /// - `stream_id`: The stream ID whose events should be removed. + /// + /// # Returns + /// - `Ok(())`: On successful deletion. + /// - `Err(Self::Error)`: If deletion fails. async fn remove_stream_in_session(&self, session_id: SessionId, stream_id: StreamId); + /// Clears all events from the store. + /// Used for resetting the store. + /// async fn clear(&self); + /// Retrieves events after a given event ID for a session and stream. + /// Critical for MCP server to replay events after a client reconnects, starting from the last known event. + /// Events are returned in chronological order (ascending timestamp) to reconstruct state. + /// + /// # Parameters + /// - `last_event_id`: The event ID to fetch events after. + /// + /// # Returns + /// - `Some(Some(EventStoreMessages))`: Events after the specified ID, if any. + /// - `None`: If no events exist after it OR the event ID is invalid. async fn events_after(&self, last_event_id: EventId) -> Option; + /// Prunes excess events to control storage usage. + /// Implementations may apply custom logic, such as limiting + /// the number of events per session or removing events older than a certain timestamp. + /// Default implementation logs a warning if not overridden by the store. + /// + /// # Parameters + /// - `session_id`: Optional session ID to prune a specific session; if None, prunes all sessions. + async fn prune_excess_events(&self, _session_id: Option) { + tracing::warn!("prune_excess_events() is not implemented for the event store."); + } + /// Counts the total number of events in the store. + /// + /// # Returns + /// - The number of events across all sessions and streams. + async fn count(&self) -> usize; } diff --git a/crates/rust-mcp-transport/src/event_store/in_memory_event_store.rs b/crates/rust-mcp-transport/src/event_store/in_memory_event_store.rs index 66e738c..d708026 100644 --- a/crates/rust-mcp-transport/src/event_store/in_memory_event_store.rs +++ b/crates/rust-mcp-transport/src/event_store/in_memory_event_store.rs @@ -147,7 +147,7 @@ impl EventStore for InMemoryEventStore { stream_id: StreamId, time_stamp: u128, message: String, - ) -> EventId { + ) -> Option { let event_id = self.generate_event_id(&session_id, &stream_id, time_stamp); let mut storage_map = self.storage_map.write().await; @@ -172,7 +172,7 @@ impl EventStore for InMemoryEventStore { session_map.push_back(entry); - event_id + Some(event_id) } /// Removes all events associated with a given stream ID within a specific session. @@ -271,4 +271,9 @@ impl EventStore for InMemoryEventStore { let mut storage_map = self.storage_map.write().await; storage_map.clear(); } + + async fn count(&self) -> usize { + let storage_map = self.storage_map.read().await; + storage_map.len() + } } diff --git a/crates/rust-mcp-transport/src/message_dispatcher.rs b/crates/rust-mcp-transport/src/message_dispatcher.rs index cd9727c..27ebad2 100644 --- a/crates/rust-mcp-transport/src/message_dispatcher.rs +++ b/crates/rust-mcp-transport/src/message_dispatcher.rs @@ -412,16 +412,14 @@ impl McpDispatch self.stream_id.as_ref(), self.event_store.as_ref(), ) { - event_id = Some( - event_store - .store_event( - session_id.clone(), - stream_id.clone(), - current_timestamp(), - payload.to_owned(), - ) - .await, - ) + event_id = event_store + .store_event( + session_id.clone(), + stream_id.clone(), + current_timestamp(), + payload.to_owned(), + ) + .await }; } From c37c366aaf41c6179f0b26ec6ab990e906cf92e7 Mon Sep 17 00:00:00 2001 From: Ali Hashemi Date: Wed, 15 Oct 2025 17:32:43 -0300 Subject: [PATCH 2/4] refactor: introducing traits for converting handlers to McpServerHandler --- README.md | 4 ++-- .../src/hyper_servers/hyper_server.rs | 17 +++++------------ .../src/hyper_servers/hyper_server_core.rs | 10 +++------- crates/rust-mcp-sdk/src/lib.rs | 5 ++++- .../src/mcp_handlers/mcp_server_handler.rs | 7 +------ .../src/mcp_handlers/mcp_server_handler_core.rs | 8 ++++++++ .../server_runtime/mcp_server_runtime.rs | 8 ++------ .../server_runtime/mcp_server_runtime_core.rs | 8 ++------ .../rust-mcp-sdk/src/mcp_traits/mcp_handler.rs | 12 ++++++++++++ crates/rust-mcp-sdk/tests/common/test_server.rs | 14 +++++++++++--- .../tests/test_protocol_compatibility.rs | 3 ++- doc/getting-started-mcp-server.md | 6 ++++-- .../src/main.rs | 11 +++++++++-- .../hello-world-mcp-server-stdio/src/main.rs | 4 ++-- .../README.md | 3 ++- .../src/main.rs | 4 ++-- .../README.md | 3 ++- .../src/main.rs | 4 ++-- 18 files changed, 75 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 2c70c3e..6722309 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ let handler = MyServerHandler {}; // STEP 3: instantiate HyperServer, providing `server_details` , `handler` and HyperServerOptions let server = hyper_server::create_server( server_details, - handler, + handler.to_mcp_server_handler(), HyperServerOptions { host: "127.0.0.1".to_string(), sse_support: false, @@ -511,7 +511,7 @@ A typical example of creating a HyperServer that exposes the MCP server via Stre let server = hyper_server::create_server( server_details, - handler, + handler.to_mcp_server_handler(), HyperServerOptions { host: "127.0.0.1".to_string(), enable_ssl: true, diff --git a/crates/rust-mcp-sdk/src/hyper_servers/hyper_server.rs b/crates/rust-mcp-sdk/src/hyper_servers/hyper_server.rs index b85b8ba..b933401 100644 --- a/crates/rust-mcp-sdk/src/hyper_servers/hyper_server.rs +++ b/crates/rust-mcp-sdk/src/hyper_servers/hyper_server.rs @@ -1,10 +1,7 @@ -use std::sync::Arc; - -use crate::schema::InitializeResult; - -use crate::mcp_server::{server_runtime::ServerRuntimeInternalHandler, ServerHandler}; - use super::{HyperServer, HyperServerOptions}; +use crate::mcp_traits::mcp_handler::McpServerHandler; +use crate::schema::InitializeResult; +use std::sync::Arc; /// Creates a new HyperServer instance with the provided handler and options /// The handler must implement ServerHandler. @@ -18,12 +15,8 @@ use super::{HyperServer, HyperServerOptions}; /// * `HyperServer` - A configured HyperServer instance ready to start pub fn create_server( server_details: InitializeResult, - handler: impl ServerHandler, + handler: Arc, server_options: HyperServerOptions, ) -> HyperServer { - HyperServer::new( - server_details, - Arc::new(ServerRuntimeInternalHandler::new(Box::new(handler))), - server_options, - ) + HyperServer::new(server_details, handler, server_options) } diff --git a/crates/rust-mcp-sdk/src/hyper_servers/hyper_server_core.rs b/crates/rust-mcp-sdk/src/hyper_servers/hyper_server_core.rs index 9599134..da13a99 100644 --- a/crates/rust-mcp-sdk/src/hyper_servers/hyper_server_core.rs +++ b/crates/rust-mcp-sdk/src/hyper_servers/hyper_server_core.rs @@ -1,5 +1,5 @@ use super::{HyperServer, HyperServerOptions}; -use crate::mcp_server::{server_runtime_core::RuntimeCoreInternalHandler, ServerHandlerCore}; +use crate::mcp_traits::mcp_handler::McpServerHandler; use crate::schema::InitializeResult; use std::sync::Arc; @@ -15,12 +15,8 @@ use std::sync::Arc; /// * `HyperServer` - A configured HyperServer instance ready to start pub fn create_server( server_details: InitializeResult, - handler: impl ServerHandlerCore, + handler: Arc, server_options: HyperServerOptions, ) -> HyperServer { - HyperServer::new( - server_details, - Arc::new(RuntimeCoreInternalHandler::new(Box::new(handler))), - server_options, - ) + HyperServer::new(server_details, handler, server_options) } diff --git a/crates/rust-mcp-sdk/src/lib.rs b/crates/rust-mcp-sdk/src/lib.rs index 9a9e0a9..7c6ae04 100644 --- a/crates/rust-mcp-sdk/src/lib.rs +++ b/crates/rust-mcp-sdk/src/lib.rs @@ -67,7 +67,7 @@ pub mod mcp_server { //! handle each message based on its type and parameters. //! //! Refer to [examples/hello-world-mcp-server-stdio-core](https://github.com/rust-mcp-stack/rust-mcp-sdk/tree/main/examples/hello-world-mcp-server-stdio-core) for an example. - pub use super::mcp_handlers::mcp_server_handler::{ServerHandler, ToMcpServerHandler}; + pub use super::mcp_handlers::mcp_server_handler::ServerHandler; pub use super::mcp_handlers::mcp_server_handler_core::ServerHandlerCore; pub use super::mcp_runtimes::server_runtime::mcp_server_runtime as server_runtime; @@ -84,6 +84,9 @@ pub mod mcp_server { #[cfg(feature = "hyper-server")] pub use super::mcp_http::{McpAppState, McpHttpHandler}; + pub use super::mcp_traits::mcp_handler::{ + McpServerHandler, ToMcpServerHandler, ToMcpServerHandlerCore, + }; } #[cfg(feature = "client")] diff --git a/crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler.rs b/crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler.rs index 9f8c9e3..bcf48d8 100644 --- a/crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler.rs +++ b/crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler.rs @@ -1,6 +1,6 @@ use crate::{ mcp_server::server_runtime::ServerRuntimeInternalHandler, - mcp_traits::mcp_handler::McpServerHandler, + mcp_traits::mcp_handler::{McpServerHandler, ToMcpServerHandler}, schema::{schema_utils::CallToolError, *}, }; use async_trait::async_trait; @@ -331,11 +331,6 @@ pub trait ServerHandler: Send + Sync + 'static { } } -// Custom trait for conversion -pub trait ToMcpServerHandler { - fn to_mcp_server_handler(self) -> Arc; -} - impl ToMcpServerHandler for T { fn to_mcp_server_handler(self) -> Arc { Arc::new(ServerRuntimeInternalHandler::new(Box::new(self))) diff --git a/crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler_core.rs b/crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler_core.rs index 9275da7..be4314b 100644 --- a/crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler_core.rs +++ b/crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler_core.rs @@ -1,3 +1,5 @@ +use crate::mcp_server::server_runtime_core::RuntimeCoreInternalHandler; +use crate::mcp_traits::mcp_handler::{McpServerHandler, ToMcpServerHandlerCore}; use crate::mcp_traits::mcp_server::McpServer; use crate::schema::schema_utils::*; use crate::schema::*; @@ -49,3 +51,9 @@ pub trait ServerHandlerCore: Send + Sync + 'static { runtime: Arc, ) -> std::result::Result<(), RpcError>; } + +impl ToMcpServerHandlerCore for T { + fn to_mcp_server_handler(self) -> Arc { + Arc::new(RuntimeCoreInternalHandler::new(Box::new(self))) + } +} diff --git a/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime.rs b/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime.rs index 62fd31f..8843dd8 100644 --- a/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime.rs +++ b/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime.rs @@ -48,13 +48,9 @@ pub fn create_server( ServerMessages, ServerMessage, >, - handler: impl ServerHandler, + handler: Arc, ) -> Arc { - ServerRuntime::new( - server_details, - transport, - Arc::new(ServerRuntimeInternalHandler::new(Box::new(handler))), - ) + ServerRuntime::new(server_details, transport, handler) } #[cfg(feature = "hyper-server")] diff --git a/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime_core.rs b/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime_core.rs index 110b20b..0f31d58 100644 --- a/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime_core.rs +++ b/crates/rust-mcp-sdk/src/mcp_runtimes/server_runtime/mcp_server_runtime_core.rs @@ -42,13 +42,9 @@ pub fn create_server( ServerMessages, ServerMessage, >, - handler: impl ServerHandlerCore, + handler: Arc, ) -> Arc { - ServerRuntime::new( - server_details, - transport, - Arc::new(RuntimeCoreInternalHandler::new(Box::new(handler))), - ) + ServerRuntime::new(server_details, transport, handler) } pub(crate) struct RuntimeCoreInternalHandler { diff --git a/crates/rust-mcp-sdk/src/mcp_traits/mcp_handler.rs b/crates/rust-mcp-sdk/src/mcp_traits/mcp_handler.rs index cb37f2a..07ae9bc 100644 --- a/crates/rust-mcp-sdk/src/mcp_traits/mcp_handler.rs +++ b/crates/rust-mcp-sdk/src/mcp_traits/mcp_handler.rs @@ -60,3 +60,15 @@ pub trait McpClientHandler: Send + Sync { runtime: &dyn McpClient, ) -> SdkResult<()>; } + +// Custom trait for converting ServerHandler +#[cfg(feature = "server")] +pub trait ToMcpServerHandler { + fn to_mcp_server_handler(self) -> Arc; +} + +// Custom trait for converting ServerHandlerCore +#[cfg(feature = "server")] +pub trait ToMcpServerHandlerCore { + fn to_mcp_server_handler(self) -> Arc; +} diff --git a/crates/rust-mcp-sdk/tests/common/test_server.rs b/crates/rust-mcp-sdk/tests/common/test_server.rs index d64244b..d6cf57b 100644 --- a/crates/rust-mcp-sdk/tests/common/test_server.rs +++ b/crates/rust-mcp-sdk/tests/common/test_server.rs @@ -14,6 +14,7 @@ pub mod test_server_common { ClientCapabilities, Implementation, InitializeRequest, InitializeRequestParams, InitializeResult, ServerCapabilities, ServerCapabilitiesTools, }; + use rust_mcp_sdk::ToMcpServerHandler; use rust_mcp_sdk::{ mcp_server::{hyper_server, HyperServer, HyperServerOptions, ServerHandler}, McpServer, SessionId, @@ -114,7 +115,11 @@ pub mod test_server_common { } pub fn create_test_server(options: HyperServerOptions) -> HyperServer { - hyper_server::create_server(test_server_details(), TestServerHandler {}, options) + hyper_server::create_server( + test_server_details(), + TestServerHandler {}.to_mcp_server_handler(), + options, + ) } pub async fn create_start_server(options: HyperServerOptions) -> LaunchedServer { @@ -123,8 +128,11 @@ pub mod test_server_common { let sse_message_url = options.sse_message_url(); let event_store_clone = options.event_store.clone(); - let server = - hyper_server::create_server(test_server_details(), TestServerHandler {}, options); + let server = hyper_server::create_server( + test_server_details(), + TestServerHandler {}.to_mcp_server_handler(), + options, + ); let hyper_runtime = HyperRuntime::create(server).await.unwrap(); diff --git a/crates/rust-mcp-sdk/tests/test_protocol_compatibility.rs b/crates/rust-mcp-sdk/tests/test_protocol_compatibility.rs index 9f2fd95..ca5f093 100644 --- a/crates/rust-mcp-sdk/tests/test_protocol_compatibility.rs +++ b/crates/rust-mcp-sdk/tests/test_protocol_compatibility.rs @@ -5,6 +5,7 @@ mod protocol_compatibility_on_server { use rust_mcp_sdk::mcp_server::ServerHandler; use rust_mcp_sdk::schema::{InitializeRequest, InitializeResult, RpcError, INTERNAL_ERROR}; + use rust_mcp_sdk::ToMcpServerHandler; use crate::common::{ test_client_info, @@ -26,7 +27,7 @@ mod protocol_compatibility_on_server { let runtime = rust_mcp_sdk::mcp_server::server_runtime::create_server( test_server_details(), transport, - TestServerHandler {}, + TestServerHandler {}.to_mcp_server_handler(), ); handler diff --git a/doc/getting-started-mcp-server.md b/doc/getting-started-mcp-server.md index 418fd66..97ba5ed 100644 --- a/doc/getting-started-mcp-server.md +++ b/doc/getting-started-mcp-server.md @@ -216,7 +216,9 @@ use rust_mcp_sdk::schema::{ ServerCapabilitiesTools, }; use rust_mcp_sdk::{ - McpServer, StdioTransport, TransportOptions, error::SdkResult, mcp_server::server_runtime, + error::SdkResult, + mcp_server::{server_runtime, ServerRuntime, ToMcpServerHandler}, + McpServer, StdioTransport, TransportOptions, }; #[tokio::main] @@ -246,7 +248,7 @@ async fn main() -> SdkResult<()> { let handler = MyServerHandler {}; //create the MCP server - let server = server_runtime::create_server(server_details, transport, handler); + let server = server_runtime::create_server(server_details, transport, handler.to_mcp_server_handler()); // Start the server server.start().await diff --git a/examples/hello-world-mcp-server-stdio-core/src/main.rs b/examples/hello-world-mcp-server-stdio-core/src/main.rs index d410526..1dcc176 100644 --- a/examples/hello-world-mcp-server-stdio-core/src/main.rs +++ b/examples/hello-world-mcp-server-stdio-core/src/main.rs @@ -6,8 +6,11 @@ use rust_mcp_sdk::schema::{ Implementation, InitializeResult, ServerCapabilities, ServerCapabilitiesTools, LATEST_PROTOCOL_VERSION, }; + use rust_mcp_sdk::{ - error::SdkResult, mcp_server::server_runtime_core, McpServer, StdioTransport, TransportOptions, + error::SdkResult, + mcp_server::{server_runtime_core, ToMcpServerHandlerCore}, + McpServer, StdioTransport, TransportOptions, }; #[tokio::main] @@ -38,7 +41,11 @@ async fn main() -> SdkResult<()> { let handler = MyServerHandler {}; // STEP 4: create a MCP server - let server = server_runtime_core::create_server(server_details, transport, handler); + let server = server_runtime_core::create_server( + server_details, + transport, + handler.to_mcp_server_handler(), + ); // STEP 5: Start the server if let Err(start_error) = server.start().await { diff --git a/examples/hello-world-mcp-server-stdio/src/main.rs b/examples/hello-world-mcp-server-stdio/src/main.rs index 9e5d2b3..79f9d31 100644 --- a/examples/hello-world-mcp-server-stdio/src/main.rs +++ b/examples/hello-world-mcp-server-stdio/src/main.rs @@ -8,7 +8,7 @@ use rust_mcp_sdk::schema::{ }; use rust_mcp_sdk::{ error::SdkResult, - mcp_server::{server_runtime, ServerRuntime}, + mcp_server::{server_runtime, ServerRuntime, ToMcpServerHandler}, McpServer, StdioTransport, TransportOptions, }; use std::sync::Arc; @@ -41,7 +41,7 @@ async fn main() -> SdkResult<()> { // STEP 4: create a MCP server let server: Arc = - server_runtime::create_server(server_details, transport, handler); + server_runtime::create_server(server_details, transport, handler.to_mcp_server_handler()); // STEP 5: Start the server if let Err(start_error) = server.start().await { diff --git a/examples/hello-world-server-streamable-http-core/README.md b/examples/hello-world-server-streamable-http-core/README.md index 49af2c2..cbfc294 100644 --- a/examples/hello-world-server-streamable-http-core/README.md +++ b/examples/hello-world-server-streamable-http-core/README.md @@ -17,7 +17,8 @@ To disable the SSE transport, set the `sse_support` value in the `HyperServerOpt ```rs let server = - hyper_server_core::create_server(server_details, handler, + hyper_server_core::create_server(server_details, + handler.to_mcp_server_handler(), HyperServerOptions{ sse_support: false, // Disable SSE support Default::default() diff --git a/examples/hello-world-server-streamable-http-core/src/main.rs b/examples/hello-world-server-streamable-http-core/src/main.rs index 81a6ae5..f725060 100644 --- a/examples/hello-world-server-streamable-http-core/src/main.rs +++ b/examples/hello-world-server-streamable-http-core/src/main.rs @@ -11,7 +11,7 @@ use rust_mcp_sdk::schema::{ }; use rust_mcp_sdk::{ error::SdkResult, - mcp_server::{hyper_server_core, HyperServerOptions}, + mcp_server::{hyper_server_core, HyperServerOptions, ToMcpServerHandlerCore}, }; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -48,7 +48,7 @@ async fn main() -> SdkResult<()> { // STEP 3: create a MCP server let server = hyper_server_core::create_server( server_details, - handler, + handler.to_mcp_server_handler(), HyperServerOptions { sse_support: true, event_store: Some(Arc::new(InMemoryEventStore::default())), // enable resumability diff --git a/examples/hello-world-server-streamable-http/README.md b/examples/hello-world-server-streamable-http/README.md index 7e3f3b6..aa20251 100644 --- a/examples/hello-world-server-streamable-http/README.md +++ b/examples/hello-world-server-streamable-http/README.md @@ -16,7 +16,8 @@ To disable the SSE transport, set the `sse_support` value in the `HyperServerOpt ```rs let server = - hyper_server_core::create_server(server_details, handler, + hyper_server_core::create_server(server_details, + handler.to_mcp_server_handler(), HyperServerOptions{ sse_support: false, // Disable SSE support Default::default() diff --git a/examples/hello-world-server-streamable-http/src/main.rs b/examples/hello-world-server-streamable-http/src/main.rs index c4fd373..e297a16 100644 --- a/examples/hello-world-server-streamable-http/src/main.rs +++ b/examples/hello-world-server-streamable-http/src/main.rs @@ -3,7 +3,7 @@ mod tools; use handler::MyServerHandler; use rust_mcp_sdk::event_store::InMemoryEventStore; -use rust_mcp_sdk::mcp_server::{hyper_server, HyperServerOptions}; +use rust_mcp_sdk::mcp_server::{hyper_server, HyperServerOptions, ToMcpServerHandler}; use rust_mcp_sdk::schema::{ Implementation, InitializeResult, ServerCapabilities, ServerCapabilitiesTools, LATEST_PROTOCOL_VERSION, @@ -52,7 +52,7 @@ async fn main() -> SdkResult<()> { // STEP 3: instantiate HyperServer, providing `server_details` , `handler` and HyperServerOptions let server = hyper_server::create_server( server_details, - handler, + handler.to_mcp_server_handler(), HyperServerOptions { host: "127.0.0.1".to_string(), ping_interval: Duration::from_secs(5), From e35e1d886a8e7fb479cb4159d06305f6d466981e Mon Sep 17 00:00:00 2001 From: Ali Hashemi Date: Wed, 15 Oct 2025 18:51:22 -0300 Subject: [PATCH 3/4] feat: enhance EventStore with better error handling and stability --- .../src/mcp_http/mcp_http_utils.rs | 10 ++- .../tests/test_streamable_http_server.rs | 1 + crates/rust-mcp-transport/src/event_store.rs | 49 +++++++++--- .../src/event_store/in_memory_event_store.rs | 75 +++++++++++-------- .../src/message_dispatcher.rs | 5 ++ 5 files changed, 98 insertions(+), 42 deletions(-) diff --git a/crates/rust-mcp-sdk/src/mcp_http/mcp_http_utils.rs b/crates/rust-mcp-sdk/src/mcp_http/mcp_http_utils.rs index 608207a..6d003b9 100644 --- a/crates/rust-mcp-sdk/src/mcp_http/mcp_http_utils.rs +++ b/crates/rust-mcp-sdk/src/mcp_http/mcp_http_utils.rs @@ -187,7 +187,15 @@ async fn create_sse_stream( tokio::spawn(async move { if let Some(last_event_id) = last_event_id { if let Some(event_store) = state.event_store.as_ref() { - if let Some(events) = event_store.events_after(last_event_id).await { + let events = event_store + .events_after(last_event_id) + .await + .unwrap_or_else(|err| { + tracing::error!("{err}"); + None + }); + + if let Some(events) = events { for message_payload in events.messages { // skip storing replay messages let error = transport.write_str(&message_payload, true).await; diff --git a/crates/rust-mcp-sdk/tests/test_streamable_http_server.rs b/crates/rust-mcp-sdk/tests/test_streamable_http_server.rs index 79c9f00..3592c97 100644 --- a/crates/rust-mcp-sdk/tests/test_streamable_http_server.rs +++ b/crates/rust-mcp-sdk/tests/test_streamable_http_server.rs @@ -1428,6 +1428,7 @@ async fn should_store_and_include_event_ids_in_server_sse_messages() { .unwrap() .events_after(first_id) .await + .unwrap() .unwrap(); assert_eq!(events.messages.len(), 1); diff --git a/crates/rust-mcp-transport/src/event_store.rs b/crates/rust-mcp-transport/src/event_store.rs index e3c78be..35eaf40 100644 --- a/crates/rust-mcp-transport/src/event_store.rs +++ b/crates/rust-mcp-transport/src/event_store.rs @@ -3,14 +3,37 @@ mod in_memory_event_store; use crate::{EventId, SessionId, StreamId}; use async_trait::async_trait; pub use in_memory_event_store::*; +use thiserror::Error; #[derive(Debug, Clone)] -pub struct EventStoreMessages { +pub struct EventStoreEntry { pub session_id: SessionId, pub stream_id: StreamId, pub messages: Vec, } +#[derive(Debug, Error)] +#[error("{message}")] +pub struct EventStoreError { + pub message: String, +} + +impl From<&str> for EventStoreError { + fn from(s: &str) -> Self { + EventStoreError { + message: s.to_string(), + } + } +} + +impl From for EventStoreError { + fn from(s: String) -> Self { + EventStoreError { message: s } + } +} + +type EventStoreResult = Result; + /// Trait defining the interface for event storage and retrieval, used by the MCP server /// to store and replay events for state reconstruction after client reconnection #[async_trait] @@ -34,7 +57,7 @@ pub trait EventStore: Send + Sync { stream_id: StreamId, timestamp: u128, message: String, - ) -> Option; + ) -> EventStoreResult; /// Removes all events associated with a given session ID. /// Used to clean up all events for a session when it is no longer needed (e.g., session ended). @@ -42,7 +65,7 @@ pub trait EventStore: Send + Sync { /// # Parameters /// - `session_id`: The session ID whose events should be removed. /// - async fn remove_by_session_id(&self, session_id: SessionId); + async fn remove_by_session_id(&self, session_id: SessionId) -> EventStoreResult<()>; /// Removes all events for a specific stream within a session. /// Useful for cleaning up a specific stream without affecting others. /// @@ -53,11 +76,15 @@ pub trait EventStore: Send + Sync { /// # Returns /// - `Ok(())`: On successful deletion. /// - `Err(Self::Error)`: If deletion fails. - async fn remove_stream_in_session(&self, session_id: SessionId, stream_id: StreamId); + async fn remove_stream_in_session( + &self, + session_id: SessionId, + stream_id: StreamId, + ) -> EventStoreResult<()>; /// Clears all events from the store. /// Used for resetting the store. /// - async fn clear(&self); + async fn clear(&self) -> EventStoreResult<()>; /// Retrieves events after a given event ID for a session and stream. /// Critical for MCP server to replay events after a client reconnects, starting from the last known event. /// Events are returned in chronological order (ascending timestamp) to reconstruct state. @@ -66,9 +93,12 @@ pub trait EventStore: Send + Sync { /// - `last_event_id`: The event ID to fetch events after. /// /// # Returns - /// - `Some(Some(EventStoreMessages))`: Events after the specified ID, if any. + /// - `Some(Some(EventStoreEntry))`: Events after the specified ID, if any. /// - `None`: If no events exist after it OR the event ID is invalid. - async fn events_after(&self, last_event_id: EventId) -> Option; + async fn events_after( + &self, + last_event_id: EventId, + ) -> EventStoreResult>; /// Prunes excess events to control storage usage. /// Implementations may apply custom logic, such as limiting /// the number of events per session or removing events older than a certain timestamp. @@ -76,12 +106,13 @@ pub trait EventStore: Send + Sync { /// /// # Parameters /// - `session_id`: Optional session ID to prune a specific session; if None, prunes all sessions. - async fn prune_excess_events(&self, _session_id: Option) { + async fn prune_excess_events(&self, _session_id: Option) -> EventStoreResult<()> { tracing::warn!("prune_excess_events() is not implemented for the event store."); + Ok(()) } /// Counts the total number of events in the store. /// /// # Returns /// - The number of events across all sessions and streams. - async fn count(&self) -> usize; + async fn count(&self) -> EventStoreResult; } diff --git a/crates/rust-mcp-transport/src/event_store/in_memory_event_store.rs b/crates/rust-mcp-transport/src/event_store/in_memory_event_store.rs index d708026..095a014 100644 --- a/crates/rust-mcp-transport/src/event_store/in_memory_event_store.rs +++ b/crates/rust-mcp-transport/src/event_store/in_memory_event_store.rs @@ -1,13 +1,13 @@ +use crate::event_store::EventStoreResult; +use crate::{ + event_store::{EventStore, EventStoreEntry}, + EventId, SessionId, StreamId, +}; use async_trait::async_trait; use std::collections::HashMap; use std::collections::VecDeque; use tokio::sync::RwLock; -use crate::{ - event_store::{EventStore, EventStoreMessages}, - EventId, SessionId, StreamId, -}; - const MAX_EVENTS_PER_SESSION: usize = 64; const ID_SEPARATOR: &str = "-.-"; @@ -101,16 +101,19 @@ impl InMemoryEventStore { /// ); /// assert_eq!(store.parse_event_id("invalid"), None); /// ``` - pub fn parse_event_id<'a>(&self, event_id: &'a str) -> Option<(&'a str, &'a str, &'a str)> { + pub fn parse_event_id<'a>( + &self, + event_id: &'a str, + ) -> EventStoreResult<(&'a str, &'a str, u128)> { // Check for empty input or invalid characters (e.g., NULL) if event_id.is_empty() || event_id.contains('\0') { - return None; + return Err("Event ID is empty!".into()); } // Split into exactly three parts let parts: Vec<&'a str> = event_id.split(ID_SEPARATOR).collect(); if parts.len() != 3 { - return None; + return Err("Invalid Event ID format.".into()); } let session_id = parts[0]; @@ -119,10 +122,14 @@ impl InMemoryEventStore { // Ensure no part is empty if session_id.is_empty() || stream_id.is_empty() || time_stamp.is_empty() { - return None; + return Err("Invalid Event ID format.".into()); } - Some((session_id, stream_id, time_stamp)) + let time_stamp: u128 = time_stamp + .parse() + .map_err(|err| format!("Error parsing timestamp: {err}"))?; + + Ok((session_id, stream_id, time_stamp)) } } @@ -147,7 +154,7 @@ impl EventStore for InMemoryEventStore { stream_id: StreamId, time_stamp: u128, message: String, - ) -> Option { + ) -> EventStoreResult { let event_id = self.generate_event_id(&session_id, &stream_id, time_stamp); let mut storage_map = self.storage_map.write().await; @@ -172,7 +179,7 @@ impl EventStore for InMemoryEventStore { session_map.push_back(entry); - Some(event_id) + Ok(event_id) } /// Removes all events associated with a given stream ID within a specific session. @@ -184,7 +191,11 @@ impl EventStore for InMemoryEventStore { /// # Arguments /// - `session_id`: The session identifier to target. /// - `stream_id`: The stream identifier to remove. - async fn remove_stream_in_session(&self, session_id: SessionId, stream_id: StreamId) { + async fn remove_stream_in_session( + &self, + session_id: SessionId, + stream_id: StreamId, + ) -> EventStoreResult<()> { let mut storage_map = self.storage_map.write().await; // Check if session exists @@ -194,9 +205,10 @@ impl EventStore for InMemoryEventStore { // Remove session if empty if events.is_empty() { storage_map.remove(&session_id); - } + }; } // No action if session_id doesn’t exist (idempotent) + Ok(()) } /// Removes all events associated with a given session ID. @@ -205,9 +217,10 @@ impl EventStore for InMemoryEventStore { /// /// # Arguments /// - `session_id`: The session identifier to remove. - async fn remove_by_session_id(&self, session_id: SessionId) { + async fn remove_by_session_id(&self, session_id: SessionId) -> EventStoreResult<()> { let mut storage_map = self.storage_map.write().await; storage_map.remove(&session_id); + Ok(()) } /// Retrieves events after a given `event_id` for a specific session and stream. @@ -221,23 +234,20 @@ impl EventStore for InMemoryEventStore { /// - `last_event_id`: The event ID (format: `session-.-stream-.-timestamp`) to start after. /// /// # Returns - /// An `Option` containing `EventStoreMessages` with the session ID, stream ID, and sorted messages, + /// An `Option` containing `EventStoreEntry` with the session ID, stream ID, and sorted messages, /// or `None` if no events are found or the input is invalid. - async fn events_after(&self, last_event_id: EventId) -> Option { - let Some((session_id, stream_id, time_stamp)) = self.parse_event_id(&last_event_id) else { - tracing::warn!("error parsing last event id: '{last_event_id}'"); - return None; - }; + async fn events_after( + &self, + last_event_id: EventId, + ) -> EventStoreResult> { + let (session_id, stream_id, time_stamp) = self.parse_event_id(&last_event_id)?; let storage_map = self.storage_map.read().await; + + // fail silently if session id does not exists let Some(events) = storage_map.get(session_id) else { tracing::warn!("could not find the session_id in the store : '{session_id}'"); - return None; - }; - - let Ok(time_stamp) = time_stamp.parse::() else { - tracing::warn!("could not parse the timestamp: '{time_stamp}'"); - return None; + return Ok(None); }; let events = match events @@ -260,20 +270,21 @@ impl EventStore for InMemoryEventStore { tracing::trace!("{} messages after '{last_event_id}'", events.len()); - Some(EventStoreMessages { + Ok(Some(EventStoreEntry { session_id: session_id.to_string(), stream_id: stream_id.to_string(), messages: events, - }) + })) } - async fn clear(&self) { + async fn clear(&self) -> EventStoreResult<()> { let mut storage_map = self.storage_map.write().await; storage_map.clear(); + Ok(()) } - async fn count(&self) -> usize { + async fn count(&self) -> EventStoreResult { let storage_map = self.storage_map.read().await; - storage_map.len() + Ok(storage_map.len()) } } diff --git a/crates/rust-mcp-transport/src/message_dispatcher.rs b/crates/rust-mcp-transport/src/message_dispatcher.rs index 27ebad2..62c591f 100644 --- a/crates/rust-mcp-transport/src/message_dispatcher.rs +++ b/crates/rust-mcp-transport/src/message_dispatcher.rs @@ -420,6 +420,11 @@ impl McpDispatch payload.to_owned(), ) .await + .map(Some) + .unwrap_or_else(|err| { + tracing::error!("{err}"); + None + }); }; } From f503b76e7b19b164a4d48761b048b51ceb307b70 Mon Sep 17 00:00:00 2001 From: Ali Hashemi Date: Wed, 15 Oct 2025 21:15:08 -0300 Subject: [PATCH 4/4] chore: fix failing checks --- Cargo.lock | 122 +++--------------- .../rust-mcp-sdk/tests/common/test_server.rs | 5 +- .../tests/test_protocol_compatibility.rs | 3 +- 3 files changed, 23 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6fc3e9d..ec473d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,21 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "aho-corasick" version = "1.1.3" @@ -94,16 +79,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.32.2" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b715a6010afb9e457ca2b7c9d2b9c344baa8baed7b38dc476034c171b32575" +checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c" dependencies = [ "bindgen", "cc", "cmake", "dunce", "fs_extra", - "libloading", ] [[package]] @@ -180,21 +164,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - [[package]] name = "base64" version = "0.13.1" @@ -274,9 +243,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -611,24 +580,18 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "glob" version = "0.3.3" @@ -1048,17 +1011,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -1096,7 +1048,7 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] @@ -1124,12 +1076,12 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.5", + "windows-link", ] [[package]] @@ -1214,15 +1166,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - [[package]] name = "mio" version = "1.0.4" @@ -1288,15 +1231,6 @@ dependencies = [ "libc", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -1436,7 +1370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", @@ -1567,7 +1501,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -1772,12 +1706,6 @@ dependencies = [ "wiremock", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -2223,29 +2151,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2 0.6.1", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -2442,7 +2367,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] @@ -2486,15 +2411,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" diff --git a/crates/rust-mcp-sdk/tests/common/test_server.rs b/crates/rust-mcp-sdk/tests/common/test_server.rs index d6cf57b..0c639de 100644 --- a/crates/rust-mcp-sdk/tests/common/test_server.rs +++ b/crates/rust-mcp-sdk/tests/common/test_server.rs @@ -14,9 +14,10 @@ pub mod test_server_common { ClientCapabilities, Implementation, InitializeRequest, InitializeRequestParams, InitializeResult, ServerCapabilities, ServerCapabilitiesTools, }; - use rust_mcp_sdk::ToMcpServerHandler; use rust_mcp_sdk::{ - mcp_server::{hyper_server, HyperServer, HyperServerOptions, ServerHandler}, + mcp_server::{ + hyper_server, HyperServer, HyperServerOptions, ServerHandler, ToMcpServerHandler, + }, McpServer, SessionId, }; use std::sync::{Arc, RwLock}; diff --git a/crates/rust-mcp-sdk/tests/test_protocol_compatibility.rs b/crates/rust-mcp-sdk/tests/test_protocol_compatibility.rs index ca5f093..7a563b6 100644 --- a/crates/rust-mcp-sdk/tests/test_protocol_compatibility.rs +++ b/crates/rust-mcp-sdk/tests/test_protocol_compatibility.rs @@ -3,9 +3,8 @@ pub mod common; mod protocol_compatibility_on_server { - use rust_mcp_sdk::mcp_server::ServerHandler; + use rust_mcp_sdk::mcp_server::{ServerHandler, ToMcpServerHandler}; use rust_mcp_sdk::schema::{InitializeRequest, InitializeResult, RpcError, INTERNAL_ERROR}; - use rust_mcp_sdk::ToMcpServerHandler; use crate::common::{ test_client_info,