use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use tokio::sync::{mpsc, oneshot, RwLock};
use tokio::time::{sleep, Instant};
use crate::error::{McpError, McpResult, ProtocolError};
use crate::messages::{
Capabilities, Implementation, InitializeRequest, InitializeResponse, InitializedNotification,
JsonRpcId, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse,
ProgressNotification, PromptListChangedNotification, ProtocolVersion,
ResourceListChangedNotification, ResourceUpdatedNotification, ToolListChangedNotification,
};
use crate::transport::{factory::TransportFactory, Transport, TransportConfig};
use tracing::{debug, info, warn};
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub request_timeout: Duration,
pub init_timeout: Duration,
pub max_retries: u32,
pub retry_base_delay: Duration,
pub auto_handle_notifications: bool,
pub message_buffer_size: usize,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
request_timeout: Duration::from_secs(30),
init_timeout: Duration::from_secs(10),
max_retries: 3,
retry_base_delay: Duration::from_secs(1),
auto_handle_notifications: true,
message_buffer_size: 1000,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClientState {
Disconnected,
Connecting,
Initializing,
Ready,
Error(String),
}
#[derive(Debug, Clone)]
pub struct ServerInfo {
pub implementation: Implementation,
pub protocol_version: ProtocolVersion,
pub capabilities: Capabilities,
pub connected_at: Instant,
}
#[derive(Debug, Clone, Default)]
pub struct ClientStats {
pub requests_sent: u64,
pub responses_received: u64,
pub notifications_sent: u64,
pub notifications_received: u64,
pub errors: u64,
pub retries: u64,
pub connection_attempts: u64,
pub last_activity: Option<Instant>,
}
#[async_trait]
pub trait NotificationHandler: Send + Sync {
async fn handle_progress(&self, notification: ProgressNotification) -> McpResult<()> {
debug!("Received progress notification: {:?}", notification);
Ok(())
}
async fn handle_resource_updated(
&self,
notification: ResourceUpdatedNotification,
) -> McpResult<()> {
debug!("Resource updated: {:?}", notification);
Ok(())
}
async fn handle_resource_list_changed(
&self,
notification: ResourceListChangedNotification,
) -> McpResult<()> {
debug!("Resource list changed: {:?}", notification);
Ok(())
}
async fn handle_tool_list_changed(
&self,
notification: ToolListChangedNotification,
) -> McpResult<()> {
debug!("Tool list changed: {:?}", notification);
Ok(())
}
async fn handle_prompt_list_changed(
&self,
notification: PromptListChangedNotification,
) -> McpResult<()> {
debug!("Prompt list changed: {:?}", notification);
Ok(())
}
}
#[derive(Debug, Default)]
pub struct DefaultNotificationHandler;
#[async_trait]
impl NotificationHandler for DefaultNotificationHandler {}
pub struct McpClient {
transport: Box<dyn Transport>,
config: ClientConfig,
state: RwLock<ClientState>,
server_info: RwLock<Option<ServerInfo>>,
stats: Arc<RwLock<ClientStats>>,
request_counter: AtomicU64,
pending_requests: Arc<RwLock<HashMap<String, oneshot::Sender<JsonRpcResponse>>>>,
notification_handler: Arc<dyn NotificationHandler>,
_message_sender: Option<mpsc::UnboundedSender<JsonRpcMessage>>,
}
impl McpClient {
pub async fn new(
transport_config: TransportConfig,
client_config: ClientConfig,
notification_handler: Box<dyn NotificationHandler>,
) -> McpResult<Self> {
let transport = TransportFactory::create(transport_config).await?;
Ok(Self {
transport,
config: client_config,
state: RwLock::new(ClientState::Disconnected),
server_info: RwLock::new(None),
stats: Arc::new(RwLock::new(ClientStats::default())),
request_counter: AtomicU64::new(1),
pending_requests: Arc::new(RwLock::new(HashMap::new())),
notification_handler: notification_handler.into(),
_message_sender: None,
})
}
pub async fn with_defaults(transport_config: TransportConfig) -> McpResult<Self> {
Self::new(
transport_config,
ClientConfig::default(),
Box::new(DefaultNotificationHandler),
)
.await
}
pub async fn state(&self) -> ClientState {
self.state.read().await.clone()
}
pub async fn server_info(&self) -> Option<ServerInfo> {
self.server_info.read().await.clone()
}
pub async fn stats(&self) -> ClientStats {
self.stats.read().await.clone()
}
pub async fn is_ready(&self) -> bool {
matches!(self.state().await, ClientState::Ready)
}
pub fn transport_info(&self) -> crate::transport::TransportInfo {
self.transport.get_info()
}
pub async fn connect(&mut self, client_info: Implementation) -> McpResult<ServerInfo> {
info!("Connecting MCP client to server");
*self.state.write().await = ClientState::Connecting;
self.transport.connect().await.map_err(|e| {
let error = format!("Transport connection failed: {e}");
self.set_error_state(error.clone());
McpError::Protocol(ProtocolError::InitializationFailed { reason: error })
})?;
self.start_message_processing().await?;
let server_info = self.perform_initialization(client_info).await?;
*self.state.write().await = ClientState::Ready;
*self.server_info.write().await = Some(server_info.clone());
info!(
"MCP client connected successfully to {}",
server_info.implementation.name
);
Ok(server_info)
}
pub async fn disconnect(&mut self) -> McpResult<()> {
info!("Disconnecting MCP client");
*self.state.write().await = ClientState::Disconnected;
*self.server_info.write().await = None;
self.pending_requests.write().await.clear();
self.transport.disconnect().await?;
info!("MCP client disconnected");
Ok(())
}
pub async fn send_notification<T>(&mut self, method: &str, params: T) -> McpResult<()>
where
T: serde::Serialize,
{
if !self.is_ready().await {
return Err(McpError::Protocol(ProtocolError::NotInitialized {
reason: "Client not ready for notifications".to_string(),
}));
}
let notification = JsonRpcNotification {
jsonrpc: "2.0".to_string(),
method: method.to_string(),
params: Some(serde_json::to_value(params)?),
};
self.transport.send_notification(notification).await?;
self.stats.write().await.notifications_sent += 1;
Ok(())
}
pub async fn send_request<T>(&mut self, method: &str, params: T) -> McpResult<JsonRpcResponse>
where
T: serde::Serialize,
{
if !self.is_ready().await {
return Err(McpError::Protocol(ProtocolError::NotInitialized {
reason: "Client not ready for requests".to_string(),
}));
}
self.send_request_with_timeout(method, params, None).await
}
fn set_error_state(&self, error: String) {
if let Ok(mut state) = self.state.try_write() {
*state = ClientState::Error(error);
}
}
fn generate_request_id(&self) -> String {
let counter = self.request_counter.fetch_add(1, Ordering::SeqCst);
format!("req_{counter}")
}
async fn start_message_processing(&mut self) -> McpResult<()> {
tracing::info!("Starting message processing task");
let (sender, mut receiver) = mpsc::unbounded_channel();
self._message_sender = Some(sender);
let pending_requests = Arc::clone(&self.pending_requests);
let stats = Arc::clone(&self.stats);
let notification_handler = Arc::clone(&self.notification_handler);
tokio::spawn(async move {
tracing::debug!("Message processing task started, waiting for messages");
while let Some(message) = receiver.recv().await {
tracing::debug!("Received message in processing task: {:?}", message);
match message {
JsonRpcMessage::Response(response) => {
tracing::debug!("Processing response with ID: {}", response.id);
if let Some(sender) = pending_requests
.write()
.await
.remove(&response.id.to_string())
{
tracing::debug!(
"Found pending request for ID {}, sending response",
response.id
);
let _ = sender.send(response);
stats.write().await.responses_received += 1;
} else {
tracing::warn!(
"Received response for unknown request ID: {}",
response.id
);
}
}
JsonRpcMessage::Notification(notification) => {
tracing::debug!("Processing notification: {}", notification.method);
Self::handle_notification(&*notification_handler, notification).await;
stats.write().await.notifications_received += 1;
}
JsonRpcMessage::Request(_) => {
tracing::warn!("Received unexpected server-to-client request");
}
}
}
});
Ok(())
}
async fn handle_notification(
handler: &dyn NotificationHandler,
notification: JsonRpcNotification,
) {
match notification.method.as_str() {
"notifications/progress" => {
if let Some(params) = notification.params {
if let Ok(progress) = serde_json::from_value::<ProgressNotification>(params) {
let _ = handler.handle_progress(progress).await;
}
}
}
"notifications/resources/updated" => {
if let Some(params) = notification.params {
if let Ok(resource_updated) =
serde_json::from_value::<ResourceUpdatedNotification>(params)
{
let _ = handler.handle_resource_updated(resource_updated).await;
}
}
}
"notifications/resources/list_changed" => {
if let Some(params) = notification.params {
if let Ok(list_changed) =
serde_json::from_value::<ResourceListChangedNotification>(params)
{
let _ = handler.handle_resource_list_changed(list_changed).await;
}
}
}
"notifications/tools/list_changed" => {
if let Some(params) = notification.params {
if let Ok(list_changed) =
serde_json::from_value::<ToolListChangedNotification>(params)
{
let _ = handler.handle_tool_list_changed(list_changed).await;
}
}
}
"notifications/prompts/list_changed" => {
if let Some(params) = notification.params {
if let Ok(list_changed) =
serde_json::from_value::<PromptListChangedNotification>(params)
{
let _ = handler.handle_prompt_list_changed(list_changed).await;
}
}
}
_ => {
warn!("Unknown notification method: {}", notification.method);
}
}
}
async fn perform_initialization(
&mut self,
client_info: Implementation,
) -> McpResult<ServerInfo> {
*self.state.write().await = ClientState::Initializing;
tracing::info!("Starting MCP protocol initialization");
let capabilities = Capabilities {
standard: crate::messages::StandardCapabilities {
tools: Some(crate::messages::ToolCapabilities {
list_changed: Some(true),
}),
resources: Some(crate::messages::ResourceCapabilities {
subscribe: Some(true),
list_changed: Some(true),
}),
prompts: Some(crate::messages::PromptCapabilities {
list_changed: Some(true),
}),
..Default::default()
},
..Default::default()
};
let request = InitializeRequest {
protocol_version: ProtocolVersion::default(),
capabilities,
client_info,
};
tracing::debug!("Sending initialize request: {:?}", request);
let response = self
.send_initialization_request("initialize", request, Some(self.config.init_timeout))
.await?;
tracing::debug!("Received initialize response: {:?}", response);
let init_response: InitializeResponse = match response.result {
Some(result) => {
tracing::debug!("Parsing initialize response result: {:?}", result);
serde_json::from_value(result)?
}
None => {
tracing::error!("Initialize response missing result field");
return Err(McpError::Protocol(ProtocolError::InitializationFailed {
reason: "Missing result in initialize response".to_string(),
}));
}
};
tracing::info!(
"Successfully parsed initialize response from server: {}",
init_response.server_info.name
);
let initialized = InitializedNotification {
metadata: HashMap::new(), };
tracing::debug!("Sending initialized notification");
self.send_initialized_notification("initialized", initialized)
.await?;
let server_info = ServerInfo {
implementation: init_response.server_info,
protocol_version: init_response.protocol_version,
capabilities: init_response.capabilities,
connected_at: Instant::now(),
};
Ok(server_info)
}
async fn send_initialization_request<T>(
&mut self,
method: &str,
params: T,
timeout_duration: Option<Duration>,
) -> McpResult<JsonRpcResponse>
where
T: serde::Serialize,
{
tracing::debug!("Sending initialization request: {}", method);
let request_id = self.generate_request_id();
let request = JsonRpcRequest {
jsonrpc: "2.0".to_string(),
id: JsonRpcId::String(request_id.clone()),
method: method.to_string(),
params: Some(serde_json::to_value(params)?),
};
let timeout_val = timeout_duration.unwrap_or(self.config.request_timeout);
self.send_request_with_retries(request, timeout_val).await
}
async fn send_initialized_notification<T>(&mut self, method: &str, params: T) -> McpResult<()>
where
T: serde::Serialize,
{
tracing::debug!("Sending initialization notification: {}", method);
let notification = JsonRpcNotification {
jsonrpc: "2.0".to_string(),
method: method.to_string(),
params: Some(serde_json::to_value(params)?),
};
self.transport.send_notification(notification).await?;
self.stats.write().await.notifications_sent += 1;
tracing::debug!("Initialization notification sent successfully");
Ok(())
}
async fn send_request_with_timeout<T>(
&mut self,
method: &str,
params: T,
timeout_duration: Option<Duration>,
) -> McpResult<JsonRpcResponse>
where
T: serde::Serialize,
{
let request_id = self.generate_request_id();
let request = JsonRpcRequest {
jsonrpc: "2.0".to_string(),
id: JsonRpcId::String(request_id.clone()),
method: method.to_string(),
params: Some(serde_json::to_value(params)?),
};
let timeout_val = timeout_duration.unwrap_or(self.config.request_timeout);
self.send_request_with_retries(request, timeout_val).await
}
async fn send_request_with_retries(
&mut self,
request: JsonRpcRequest,
timeout_duration: Duration,
) -> McpResult<JsonRpcResponse> {
let mut last_error = None;
for attempt in 0..=self.config.max_retries {
match self
.send_single_request(request.clone(), timeout_duration)
.await
{
Ok(response) => {
if attempt > 0 {
self.stats.write().await.retries += attempt as u64;
}
return Ok(response);
}
Err(e) => {
last_error = Some(e);
if attempt < self.config.max_retries {
let delay = self.config.retry_base_delay * 2_u32.pow(attempt);
debug!(
"Request failed, retrying in {:?} (attempt {} of {})",
delay,
attempt + 1,
self.config.max_retries + 1
);
sleep(delay).await;
}
}
}
}
self.stats.write().await.errors += 1;
Err(last_error.unwrap())
}
async fn send_single_request(
&mut self,
request: JsonRpcRequest,
timeout_duration: Duration,
) -> McpResult<JsonRpcResponse> {
let request_id = request.id.to_string();
tracing::debug!("Sending single request with ID: {}", request_id);
let response = self
.transport
.send_request(request, Some(timeout_duration))
.await?;
self.stats.write().await.requests_sent += 1;
tracing::debug!("Received response for request ID: {}", response.id);
Ok(response)
}
}
pub struct McpClientBuilder {
transport_config: Option<TransportConfig>,
client_config: ClientConfig,
notification_handler: Option<Box<dyn NotificationHandler>>,
}
impl McpClientBuilder {
pub fn new() -> Self {
Self {
transport_config: None,
client_config: ClientConfig::default(),
notification_handler: None,
}
}
pub fn transport(mut self, config: TransportConfig) -> Self {
self.transport_config = Some(config);
self
}
pub fn config(mut self, config: ClientConfig) -> Self {
self.client_config = config;
self
}
pub fn notification_handler(mut self, handler: Box<dyn NotificationHandler>) -> Self {
self.notification_handler = Some(handler);
self
}
pub fn request_timeout(mut self, timeout: Duration) -> Self {
self.client_config.request_timeout = timeout;
self
}
pub fn init_timeout(mut self, timeout: Duration) -> Self {
self.client_config.init_timeout = timeout;
self
}
pub fn max_retries(mut self, retries: u32) -> Self {
self.client_config.max_retries = retries;
self
}
pub async fn build(self) -> McpResult<McpClient> {
let transport_config = self.transport_config.ok_or_else(|| {
McpError::Protocol(ProtocolError::InvalidConfig {
reason: "Transport configuration is required".to_string(),
})
})?;
let notification_handler = self
.notification_handler
.unwrap_or_else(|| Box::new(DefaultNotificationHandler));
McpClient::new(transport_config, self.client_config, notification_handler).await
}
}
impl Default for McpClientBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::TransportConfig;
#[tokio::test]
async fn test_client_creation() {
let config = TransportConfig::stdio("echo", &[] as &[String]);
let client_config = ClientConfig::default();
let handler = Box::new(DefaultNotificationHandler);
let client = McpClient::new(config, client_config, handler)
.await
.unwrap();
assert_eq!(client.state().await, ClientState::Disconnected);
}
#[tokio::test]
async fn test_client_with_defaults() {
let config = TransportConfig::stdio("echo", &[] as &[String]);
let client = McpClient::with_defaults(config).await.unwrap();
assert_eq!(client.state().await, ClientState::Disconnected);
assert!(!client.is_ready().await);
}
#[test]
fn test_client_config_defaults() {
let config = ClientConfig::default();
assert_eq!(config.request_timeout, Duration::from_secs(30));
assert_eq!(config.init_timeout, Duration::from_secs(10));
assert_eq!(config.max_retries, 3);
}
}