Skip to main content

turul_mcp_client/
streaming.rs

1//! Streaming support for MCP client
2
3use serde_json::Value;
4use std::sync::Arc;
5use tokio::sync::mpsc;
6use tracing::{debug, info, warn};
7
8use crate::error::{McpClientError, McpClientResult};
9use crate::transport::ServerEvent;
10
11/// Stream handler for processing server events
12#[derive(Debug)]
13pub struct StreamHandler {
14    /// Event receiver from transport
15    event_receiver: Option<mpsc::UnboundedReceiver<ServerEvent>>,
16    /// Event callbacks
17    callbacks: Arc<parking_lot::Mutex<StreamCallbacks>>,
18    /// Channel for sending JSON-RPC responses back to the server
19    response_sender: Option<mpsc::UnboundedSender<Value>>,
20}
21
22/// Type alias for request handler callback
23type RequestHandler = Box<dyn Fn(Value) -> Result<Value, String> + Send + Sync>;
24
25/// Callbacks for different types of server events
26#[derive(Default)]
27pub struct StreamCallbacks {
28    /// Notification callback
29    pub notification: Option<Box<dyn Fn(Value) + Send + Sync>>,
30    /// Request callback (server asking client)
31    pub request: Option<RequestHandler>,
32    /// Connection lost callback
33    pub connection_lost: Option<Box<dyn Fn() + Send + Sync>>,
34    /// Error callback
35    pub error: Option<Box<dyn Fn(String) + Send + Sync>>,
36    /// Heartbeat callback
37    pub heartbeat: Option<Box<dyn Fn() + Send + Sync>>,
38}
39
40impl std::fmt::Debug for StreamCallbacks {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.debug_struct("StreamCallbacks")
43            .field(
44                "notification",
45                &self.notification.as_ref().map(|_| "function"),
46            )
47            .field("request", &self.request.as_ref().map(|_| "function"))
48            .field(
49                "connection_lost",
50                &self.connection_lost.as_ref().map(|_| "function"),
51            )
52            .field("error", &self.error.as_ref().map(|_| "function"))
53            .field("heartbeat", &self.heartbeat.as_ref().map(|_| "function"))
54            .finish()
55    }
56}
57
58impl StreamHandler {
59    /// Create a new stream handler
60    pub fn new() -> Self {
61        Self {
62            event_receiver: None,
63            callbacks: Arc::new(parking_lot::Mutex::new(StreamCallbacks::default())),
64            response_sender: None,
65        }
66    }
67
68    /// Set event receiver from transport
69    pub fn set_receiver(&mut self, receiver: mpsc::UnboundedReceiver<ServerEvent>) {
70        self.event_receiver = Some(receiver);
71    }
72
73    /// Set channel for sending JSON-RPC responses back to the server
74    pub fn set_response_sender(&mut self, sender: mpsc::UnboundedSender<Value>) {
75        self.response_sender = Some(sender);
76    }
77
78    /// Set notification callback
79    pub fn on_notification<F>(&self, callback: F)
80    where
81        F: Fn(Value) + Send + Sync + 'static,
82    {
83        self.callbacks.lock().notification = Some(Box::new(callback));
84    }
85
86    /// Set request callback
87    pub fn on_request<F>(&self, callback: F)
88    where
89        F: Fn(Value) -> Result<Value, String> + Send + Sync + 'static,
90    {
91        self.callbacks.lock().request = Some(Box::new(callback));
92    }
93
94    /// Set connection lost callback
95    pub fn on_connection_lost<F>(&self, callback: F)
96    where
97        F: Fn() + Send + Sync + 'static,
98    {
99        self.callbacks.lock().connection_lost = Some(Box::new(callback));
100    }
101
102    /// Set error callback
103    pub fn on_error<F>(&self, callback: F)
104    where
105        F: Fn(String) + Send + Sync + 'static,
106    {
107        self.callbacks.lock().error = Some(Box::new(callback));
108    }
109
110    /// Set heartbeat callback
111    pub fn on_heartbeat<F>(&self, callback: F)
112    where
113        F: Fn() + Send + Sync + 'static,
114    {
115        self.callbacks.lock().heartbeat = Some(Box::new(callback));
116    }
117
118    /// Start processing events
119    pub async fn start(&mut self) -> McpClientResult<()> {
120        let mut receiver = self
121            .event_receiver
122            .take()
123            .ok_or_else(|| McpClientError::generic("No event receiver configured"))?;
124
125        let callbacks = Arc::clone(&self.callbacks);
126        let response_sender = self.response_sender.clone();
127
128        tokio::spawn(async move {
129            info!("Stream handler started");
130
131            while let Some(event) = receiver.recv().await {
132                debug!(event = ?event, "Received server event");
133
134                let callbacks = callbacks.lock();
135
136                match event {
137                    ServerEvent::Notification(notification) => {
138                        if let Some(ref callback) = callbacks.notification {
139                            callback(notification);
140                        }
141                    }
142                    ServerEvent::Request(request) => {
143                        // Per JSON-RPC 2.0, only requests with a non-null id
144                        // expect a response. Messages without id are notifications
145                        // and MUST NOT receive a reply.
146                        let request_id = request.get("id").filter(|id| !id.is_null()).cloned();
147
148                        if request_id.is_none() {
149                            warn!(
150                                "Received server request without valid id, treating as notification"
151                            );
152                            if let Some(ref callback) = callbacks.request {
153                                let _ = callback(request);
154                            }
155                            continue;
156                        }
157
158                        if let Some(ref callback) = callbacks.request {
159                            let response_json = match callback(request) {
160                                Ok(result) => {
161                                    debug!("Request handled successfully");
162                                    serde_json::json!({
163                                        "jsonrpc": "2.0",
164                                        "id": request_id,
165                                        "result": result
166                                    })
167                                }
168                                Err(error) => {
169                                    warn!(error = %error, "Request handler returned error");
170                                    serde_json::json!({
171                                        "jsonrpc": "2.0",
172                                        "id": request_id,
173                                        "error": {
174                                            "code": -32603,
175                                            "message": error
176                                        }
177                                    })
178                                }
179                            };
180
181                            if let Some(ref sender) = response_sender {
182                                if let Err(e) = sender.send(response_json) {
183                                    warn!("Failed to send response via channel: {}", e);
184                                }
185                            } else {
186                                warn!("No response sender configured, response discarded");
187                            }
188                        } else {
189                            warn!("Received server request but no request handler configured");
190                            if let Some(ref sender) = response_sender {
191                                let error_json = serde_json::json!({
192                                    "jsonrpc": "2.0",
193                                    "id": request_id,
194                                    "error": {
195                                        "code": -32601,
196                                        "message": "Method not found: no request handler configured"
197                                    }
198                                });
199                                if let Err(e) = sender.send(error_json) {
200                                    warn!("Failed to send error response via channel: {}", e);
201                                }
202                            }
203                        }
204                    }
205                    ServerEvent::Response(_) => {
206                        // Response to a client-originated request received via SSE.
207                        // Handled by the normal request/response matching path,
208                        // not by the stream handler callback.
209                        debug!("Received async response via event stream");
210                    }
211                    ServerEvent::ConnectionLost => {
212                        warn!("Connection lost");
213                        if let Some(ref callback) = callbacks.connection_lost {
214                            callback();
215                        }
216                    }
217                    ServerEvent::Error(error) => {
218                        warn!(error = %error, "Server error");
219                        if let Some(ref callback) = callbacks.error {
220                            callback(error);
221                        }
222                    }
223                    ServerEvent::Heartbeat => {
224                        debug!("Heartbeat received");
225                        if let Some(ref callback) = callbacks.heartbeat {
226                            callback();
227                        }
228                    }
229                }
230            }
231
232            info!("Stream handler stopped");
233        });
234
235        Ok(())
236    }
237
238    /// Check if handler is active
239    pub fn is_active(&self) -> bool {
240        self.event_receiver.is_some()
241    }
242}
243
244impl Default for StreamHandler {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250/// Progress tracker for long-running operations
251#[derive(Debug, Clone)]
252pub struct ProgressTracker {
253    /// Operation ID
254    pub operation_id: String,
255    /// Total steps (if known)
256    pub total: Option<u64>,
257    /// Completed steps
258    pub completed: u64,
259    /// Progress message
260    pub message: Option<String>,
261    /// Progress metadata
262    pub metadata: Value,
263}
264
265impl ProgressTracker {
266    /// Create a new progress tracker
267    pub fn new(operation_id: String) -> Self {
268        Self {
269            operation_id,
270            total: None,
271            completed: 0,
272            message: None,
273            metadata: Value::Null,
274        }
275    }
276
277    /// Update progress
278    pub fn update(&mut self, completed: u64, message: Option<String>) {
279        self.completed = completed;
280        self.message = message;
281    }
282
283    /// Set total steps
284    pub fn set_total(&mut self, total: u64) {
285        self.total = Some(total);
286    }
287
288    /// Get progress percentage (0.0 to 1.0)
289    pub fn percentage(&self) -> Option<f64> {
290        self.total.map(|total| {
291            if total == 0 {
292                1.0
293            } else {
294                (self.completed as f64) / (total as f64)
295            }
296        })
297    }
298
299    /// Check if operation is complete
300    pub fn is_complete(&self) -> bool {
301        if let Some(total) = self.total {
302            self.completed >= total
303        } else {
304            false
305        }
306    }
307
308    /// Get status summary
309    pub fn status(&self) -> String {
310        match (self.total, &self.message) {
311            (Some(total), Some(msg)) => {
312                format!(
313                    "{}/{} ({}%) - {}",
314                    self.completed,
315                    total,
316                    (self.percentage().unwrap_or(0.0) * 100.0) as u32,
317                    msg
318                )
319            }
320            (Some(total), None) => {
321                format!(
322                    "{}/{} ({}%)",
323                    self.completed,
324                    total,
325                    (self.percentage().unwrap_or(0.0) * 100.0) as u32
326                )
327            }
328            (None, Some(msg)) => {
329                format!("{} steps - {}", self.completed, msg)
330            }
331            (None, None) => {
332                format!("{} steps", self.completed)
333            }
334        }
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    #[test]
343    fn test_progress_tracker() {
344        let mut tracker = ProgressTracker::new("test-op".to_string());
345
346        assert_eq!(tracker.completed, 0);
347        assert_eq!(tracker.percentage(), None);
348
349        tracker.set_total(100);
350        assert_eq!(tracker.percentage(), Some(0.0));
351
352        tracker.update(50, Some("halfway".to_string()));
353        assert_eq!(tracker.percentage(), Some(0.5));
354        assert_eq!(tracker.message, Some("halfway".to_string()));
355
356        tracker.update(100, Some("complete".to_string()));
357        assert_eq!(tracker.percentage(), Some(1.0));
358        assert!(tracker.is_complete());
359    }
360
361    #[tokio::test]
362    async fn test_stream_handler_callbacks() {
363        let handler = StreamHandler::new();
364
365        let notification_received = Arc::new(parking_lot::Mutex::new(false));
366        let notification_received_clone = Arc::clone(&notification_received);
367
368        handler.on_notification(move |_| {
369            *notification_received_clone.lock() = true;
370        });
371
372        // Test that callback is registered
373        assert!(handler.callbacks.lock().notification.is_some());
374    }
375
376    #[tokio::test]
377    async fn test_stream_handler_sends_success_response() {
378        let (event_tx, event_rx) = mpsc::unbounded_channel();
379        let (response_tx, mut response_rx) = mpsc::unbounded_channel();
380
381        let mut handler = StreamHandler::new();
382        handler.set_receiver(event_rx);
383        handler.set_response_sender(response_tx);
384
385        handler.on_request(|_req| Ok(serde_json::json!({"status": "ok"})));
386
387        handler.start().await.unwrap();
388
389        // Send a server-initiated request
390        let request = serde_json::json!({
391            "jsonrpc": "2.0",
392            "id": "srv-1",
393            "method": "sampling/createMessage",
394            "params": {}
395        });
396        event_tx.send(ServerEvent::Request(request)).unwrap();
397
398        // Verify response format
399        let response = tokio::time::timeout(std::time::Duration::from_secs(1), response_rx.recv())
400            .await
401            .unwrap()
402            .unwrap();
403
404        assert_eq!(response["jsonrpc"], "2.0");
405        assert_eq!(response["id"], "srv-1");
406        assert_eq!(response["result"]["status"], "ok");
407        assert!(response.get("error").is_none());
408    }
409
410    #[tokio::test]
411    async fn test_stream_handler_sends_error_response() {
412        let (event_tx, event_rx) = mpsc::unbounded_channel();
413        let (response_tx, mut response_rx) = mpsc::unbounded_channel();
414
415        let mut handler = StreamHandler::new();
416        handler.set_receiver(event_rx);
417        handler.set_response_sender(response_tx);
418
419        handler.on_request(|_req| Err("something went wrong".to_string()));
420
421        handler.start().await.unwrap();
422
423        let request = serde_json::json!({
424            "jsonrpc": "2.0",
425            "id": 42,
426            "method": "sampling/createMessage",
427            "params": {}
428        });
429        event_tx.send(ServerEvent::Request(request)).unwrap();
430
431        let response = tokio::time::timeout(std::time::Duration::from_secs(1), response_rx.recv())
432            .await
433            .unwrap()
434            .unwrap();
435
436        assert_eq!(response["jsonrpc"], "2.0");
437        assert_eq!(response["id"], 42);
438        assert_eq!(response["error"]["code"], -32603);
439        assert_eq!(response["error"]["message"], "something went wrong");
440    }
441
442    #[tokio::test]
443    async fn test_stream_handler_no_callback_sends_method_not_found() {
444        let (event_tx, event_rx) = mpsc::unbounded_channel();
445        let (response_tx, mut response_rx) = mpsc::unbounded_channel();
446
447        let mut handler = StreamHandler::new();
448        handler.set_receiver(event_rx);
449        handler.set_response_sender(response_tx);
450        // No on_request callback registered
451
452        handler.start().await.unwrap();
453
454        let request = serde_json::json!({
455            "jsonrpc": "2.0",
456            "id": "req-99",
457            "method": "unknown/method",
458            "params": {}
459        });
460        event_tx.send(ServerEvent::Request(request)).unwrap();
461
462        let response = tokio::time::timeout(std::time::Duration::from_secs(1), response_rx.recv())
463            .await
464            .unwrap()
465            .unwrap();
466
467        assert_eq!(response["jsonrpc"], "2.0");
468        assert_eq!(response["id"], "req-99");
469        assert_eq!(response["error"]["code"], -32601);
470    }
471
472    #[tokio::test]
473    async fn test_stream_handler_no_id_skips_response() {
474        let (event_tx, event_rx) = mpsc::unbounded_channel();
475        let (response_tx, mut response_rx) = mpsc::unbounded_channel();
476
477        let mut handler = StreamHandler::new();
478        handler.set_receiver(event_rx);
479        handler.set_response_sender(response_tx);
480
481        let callback_called = Arc::new(parking_lot::Mutex::new(false));
482        let callback_called_clone = Arc::clone(&callback_called);
483        handler.on_request(move |_req| {
484            *callback_called_clone.lock() = true;
485            Ok(serde_json::json!({"handled": true}))
486        });
487
488        handler.start().await.unwrap();
489
490        // Request without id — should invoke callback but NOT send response
491        let request = serde_json::json!({
492            "jsonrpc": "2.0",
493            "method": "sampling/createMessage",
494            "params": {}
495        });
496        event_tx.send(ServerEvent::Request(request)).unwrap();
497
498        // Give handler time to process
499        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
500
501        // Callback should have been called
502        assert!(
503            *callback_called.lock(),
504            "callback should be invoked even without id"
505        );
506
507        // But no response should be emitted
508        let result =
509            tokio::time::timeout(std::time::Duration::from_millis(100), response_rx.recv()).await;
510        assert!(
511            result.is_err(),
512            "no response should be sent for request without id"
513        );
514    }
515
516    #[tokio::test]
517    async fn test_stream_handler_null_id_skips_response() {
518        let (event_tx, event_rx) = mpsc::unbounded_channel();
519        let (response_tx, mut response_rx) = mpsc::unbounded_channel();
520
521        let mut handler = StreamHandler::new();
522        handler.set_receiver(event_rx);
523        handler.set_response_sender(response_tx);
524
525        handler.on_request(|_req| Ok(serde_json::json!({"handled": true})));
526
527        handler.start().await.unwrap();
528
529        // Request with explicit null id — also should NOT send response
530        let request = serde_json::json!({
531            "jsonrpc": "2.0",
532            "id": null,
533            "method": "sampling/createMessage",
534            "params": {}
535        });
536        event_tx.send(ServerEvent::Request(request)).unwrap();
537
538        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
539
540        let result =
541            tokio::time::timeout(std::time::Duration::from_millis(100), response_rx.recv()).await;
542        assert!(
543            result.is_err(),
544            "no response should be sent for request with null id"
545        );
546    }
547}