Skip to main content

tenx_mcp/
client.rs

1use futures::stream::SplitSink;
2use futures::{SinkExt, StreamExt};
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::Duration;
6use tokio::sync::{mpsc, oneshot, Mutex};
7use tokio::time::timeout;
8use tracing::{debug, error, info, warn};
9
10use crate::{
11    error::{MCPError, Result},
12    retry::RetryConfig,
13    schema::*,
14    transport::{Transport, TransportStream},
15};
16
17/// Type for handling either a response or error from JSON-RPC
18enum ResponseOrError {
19    Response(JSONRPCResponse),
20    Error(JSONRPCError),
21}
22
23/// Configuration for the MCP client
24#[derive(Clone, Debug)]
25pub struct ClientConfig {
26    /// Retry configuration for requests
27    pub retry: RetryConfig,
28    /// Default timeout for requests
29    pub request_timeout: Duration,
30}
31
32impl Default for ClientConfig {
33    fn default() -> Self {
34        Self {
35            retry: RetryConfig::default(),
36            request_timeout: Duration::from_secs(30),
37        }
38    }
39}
40
41/// MCP Client implementation
42pub struct MCPClient {
43    transport_tx: Option<SplitSink<Box<dyn TransportStream>, JSONRPCMessage>>,
44    pending_requests: Arc<Mutex<HashMap<String, oneshot::Sender<ResponseOrError>>>>,
45    notification_tx: mpsc::Sender<JSONRPCNotification>,
46    notification_rx: Option<mpsc::Receiver<JSONRPCNotification>>,
47    next_request_id: Arc<Mutex<u64>>,
48    config: ClientConfig,
49}
50
51impl MCPClient {
52    /// Create a new MCP client with default configuration
53    pub fn new() -> Self {
54        Self::with_config(ClientConfig::default())
55    }
56
57    /// Create a new MCP client with custom configuration
58    pub fn with_config(config: ClientConfig) -> Self {
59        let (notification_tx, notification_rx) = mpsc::channel(100);
60
61        Self {
62            transport_tx: None,
63            pending_requests: Arc::new(Mutex::new(HashMap::new())),
64            notification_tx,
65            notification_rx: Some(notification_rx),
66            next_request_id: Arc::new(Mutex::new(1)),
67            config,
68        }
69    }
70
71    /// Connect using the provided transport
72    pub async fn connect(&mut self, mut transport: Box<dyn Transport>) -> Result<()> {
73        transport.connect().await?;
74        let stream = transport.framed()?;
75
76        // Start the message handler task before storing transport
77        self.start_message_handler(stream).await?;
78
79        info!("MCP client connected");
80        Ok(())
81    }
82
83    /// Initialize the connection with the server
84    pub async fn initialize(
85        &mut self,
86        client_info: Implementation,
87        capabilities: ClientCapabilities,
88    ) -> Result<InitializeResult> {
89        let request = ClientRequest::Initialize {
90            protocol_version: LATEST_PROTOCOL_VERSION.to_string(),
91            capabilities,
92            client_info,
93        };
94
95        let value = self.request(request).await?;
96        let result: InitializeResult = serde_json::from_value(value)?;
97
98        // Send the initialized notification to complete the handshake
99        self.send_notification("notifications/initialized", None)
100            .await?;
101
102        Ok(result)
103    }
104
105    /// List available tools from the server
106    pub async fn list_tools(&mut self) -> Result<ListToolsResult> {
107        let value = self.request(ClientRequest::ListTools).await?;
108        let result: ListToolsResult = serde_json::from_value(value)?;
109        Ok(result)
110    }
111
112    /// Call a tool on the server
113    pub async fn call_tool(
114        &mut self,
115        name: String,
116        arguments: Option<serde_json::Value>,
117    ) -> Result<CallToolResult> {
118        let arguments = arguments.map(|args| {
119            if let serde_json::Value::Object(map) = args {
120                map.into_iter().collect()
121            } else {
122                std::collections::HashMap::new()
123            }
124        });
125
126        let request = ClientRequest::CallTool { name, arguments };
127        let value = self.request_with_retry(request).await?;
128        let result: CallToolResult = serde_json::from_value(value)?;
129        Ok(result)
130    }
131
132    /// Take the notification receiver channel
133    pub fn take_notification_receiver(&mut self) -> Option<mpsc::Receiver<JSONRPCNotification>> {
134        self.notification_rx.take()
135    }
136
137    /// Send a request with retry logic
138    async fn request_with_retry(&mut self, request: ClientRequest) -> Result<serde_json::Value> {
139        // For now, we'll just do a single request without retry
140        // TODO: Implement proper retry logic that doesn't require mutable self in closure
141        self.request(request).await
142    }
143
144    /// Send a request and wait for response
145    async fn request(&mut self, request: ClientRequest) -> Result<serde_json::Value> {
146        let id = self.next_request_id().await;
147        let (tx, rx) = oneshot::channel();
148
149        // Store the response channel
150        {
151            let mut pending = self.pending_requests.lock().await;
152            pending.insert(id.clone(), tx);
153        }
154
155        // Create the JSON-RPC request
156        let jsonrpc_request = JSONRPCRequest {
157            jsonrpc: JSONRPC_VERSION.to_string(),
158            id: RequestId::String(id.clone()),
159            request: Request {
160                method: request.method().to_string(),
161                params: Some(RequestParams {
162                    meta: None,
163                    other: serde_json::to_value(&request)?
164                        .as_object()
165                        .unwrap_or(&serde_json::Map::new())
166                        .iter()
167                        .map(|(k, v)| (k.clone(), v.clone()))
168                        .collect(),
169                }),
170            },
171        };
172
173        self.send_message(JSONRPCMessage::Request(jsonrpc_request))
174            .await?;
175
176        // Wait for response with timeout
177        match timeout(self.config.request_timeout, rx).await {
178            Ok(Ok(response_or_error)) => {
179                match response_or_error {
180                    ResponseOrError::Response(response) => {
181                        // Extract result from the flattened Result structure
182                        // For now, we'll return the whole result as JSON
183                        Ok(serde_json::to_value(response.result)?)
184                    }
185                    ResponseOrError::Error(error) => {
186                        // Map JSON-RPC errors to appropriate MCPError variants
187                        match error.error.code {
188                            METHOD_NOT_FOUND => Err(MCPError::MethodNotFound(error.error.message)),
189                            INVALID_PARAMS => Err(MCPError::invalid_params(
190                                request.method(),
191                                error.error.message,
192                            )),
193                            _ => Err(MCPError::Protocol(format!(
194                                "JSON-RPC error {}: {}",
195                                error.error.code, error.error.message
196                            ))),
197                        }
198                    }
199                }
200            }
201            Ok(Err(e)) => {
202                error!("Response channel closed for request {}: {}", id, e);
203                // Remove the pending request
204                self.pending_requests.lock().await.remove(&id);
205                Err(MCPError::Protocol("Response channel closed".to_string()))
206            }
207            Err(_) => {
208                // Timeout occurred
209                error!(
210                    "Request {} timed out after {:?}",
211                    id, self.config.request_timeout
212                );
213                // Remove the pending request
214                self.pending_requests.lock().await.remove(&id);
215                Err(MCPError::timeout(self.config.request_timeout, id))
216            }
217        }
218    }
219
220    /// Send a message through the transport
221    async fn send_message(&mut self, message: JSONRPCMessage) -> Result<()> {
222        if let Some(transport_tx) = &mut self.transport_tx {
223            transport_tx.send(message).await?;
224            Ok(())
225        } else {
226            Err(MCPError::Transport("Not connected".to_string()))
227        }
228    }
229
230    /// Send a notification to the server
231    async fn send_notification(
232        &mut self,
233        method: &str,
234        params: Option<serde_json::Value>,
235    ) -> Result<()> {
236        let notification_params = params.map(|v| NotificationParams {
237            meta: None,
238            other: if let Some(obj) = v.as_object() {
239                obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
240            } else {
241                HashMap::new()
242            },
243        });
244
245        let notification = JSONRPCNotification {
246            jsonrpc: JSONRPC_VERSION.to_string(),
247            notification: Notification {
248                method: method.to_string(),
249                params: notification_params,
250            },
251        };
252
253        self.send_message(JSONRPCMessage::Notification(notification))
254            .await
255    }
256
257    /// Generate the next request ID
258    async fn next_request_id(&self) -> String {
259        let mut id = self.next_request_id.lock().await;
260        let current = *id;
261        *id += 1;
262        format!("req-{current}")
263    }
264
265    /// Start the background task that handles incoming messages
266    async fn start_message_handler(&mut self, stream: Box<dyn TransportStream>) -> Result<()> {
267        let pending_requests = self.pending_requests.clone();
268        let notification_tx = self.notification_tx.clone();
269
270        // Split the transport stream into read and write halves
271        let (tx, mut rx) = stream.split();
272
273        // Store the sender half for sending messages
274        self.transport_tx = Some(tx);
275
276        // Spawn a task to handle incoming messages
277        tokio::spawn(async move {
278            debug!("Message handler started");
279
280            while let Some(result) = rx.next().await {
281                match result {
282                    Ok(message) => {
283                        debug!("Received message: {:?}", message);
284
285                        match message {
286                            JSONRPCMessage::Response(response) => {
287                                // Extract the ID and find the corresponding request
288                                if let RequestId::String(id) = &response.id {
289                                    let mut pending = pending_requests.lock().await;
290                                    if let Some(tx) = pending.remove(id) {
291                                        // Send the response to the waiting request
292                                        let _ = tx.send(ResponseOrError::Response(response));
293                                    } else {
294                                        warn!("Received response for unknown request ID: {}", id);
295                                    }
296                                }
297                            }
298                            JSONRPCMessage::Notification(notification) => {
299                                // Forward notifications to the notification channel
300                                if let Err(e) = notification_tx.send(notification).await {
301                                    error!("Failed to send notification: {}", e);
302                                    // If the receiver is dropped, we should stop
303                                    break;
304                                }
305                            }
306                            JSONRPCMessage::Error(error) => {
307                                // Handle JSON-RPC errors
308                                if let RequestId::String(id) = &error.id {
309                                    let mut pending = pending_requests.lock().await;
310                                    if let Some(tx) = pending.remove(id) {
311                                        let _ = tx.send(ResponseOrError::Error(error));
312                                    } else {
313                                        warn!("Received error for unknown request ID: {}", id);
314                                    }
315                                } else {
316                                    error!(
317                                        "Received error with non-string request ID: {:?}",
318                                        error.id
319                                    );
320                                }
321                            }
322                            JSONRPCMessage::Request(_request) => {
323                                // Clients typically don't receive requests from servers in MCP
324                                warn!("Received unexpected request from server");
325                            }
326                            JSONRPCMessage::BatchRequest(_batch) => {
327                                // Clients typically don't receive batch requests from servers
328                                warn!("Received unexpected batch request from server");
329                            }
330                            JSONRPCMessage::BatchResponse(_batch) => {
331                                // TODO: Handle batch responses if we implement batch requests
332                                warn!(
333                                    "Received batch response - batch requests not yet implemented"
334                                );
335                            }
336                        }
337                    }
338                    Err(e) => {
339                        error!("Error receiving message: {}", e);
340                        // On error, we should probably break the loop
341                        break;
342                    }
343                }
344            }
345
346            info!("Message handler stopped");
347        });
348
349        Ok(())
350    }
351}
352
353impl Default for MCPClient {
354    fn default() -> Self {
355        Self::new()
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn test_client_creation() {
365        let client = MCPClient::new();
366        assert!(client.transport_tx.is_none());
367    }
368
369    #[tokio::test]
370    async fn test_next_request_id() {
371        let client = MCPClient::new();
372        let id1 = client.next_request_id().await;
373        let id2 = client.next_request_id().await;
374
375        assert_eq!(id1, "req-1");
376        assert_eq!(id2, "req-2");
377    }
378}