Skip to main content

connector_client/
lib.rs

1//! Shared WebSocket client for connecting to tauri-plugin-connector.
2//!
3//! Both the MCP server and CLI use this crate to communicate with the
4//! running Tauri app's connector plugin over WebSocket.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::Duration;
9
10use futures_util::{SinkExt, StreamExt};
11use serde_json::Value;
12use tokio::net::TcpStream;
13use tokio::sync::{mpsc, oneshot, Mutex};
14use tokio_tungstenite::tungstenite::Message;
15use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
16
17pub mod discovery;
18
19const DEFAULT_TIMEOUT_MS: u64 = 35_000;
20
21type _WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
22
23struct PendingRequest {
24    tx: oneshot::Sender<Result<Value, String>>,
25}
26
27/// WebSocket client that communicates with tauri-plugin-connector.
28pub struct ConnectorClient {
29    write_tx: Option<mpsc::UnboundedSender<String>>,
30    pending: Arc<Mutex<HashMap<String, PendingRequest>>>,
31    _reader_handle: Option<tokio::task::JoinHandle<()>>,
32}
33
34impl ConnectorClient {
35    pub fn new() -> Self {
36        Self {
37            write_tx: None,
38            pending: Arc::new(Mutex::new(HashMap::new())),
39            _reader_handle: None,
40        }
41    }
42
43    /// Connect to the plugin's WebSocket server.
44    pub async fn connect(&mut self, host: &str, port: u16) -> Result<(), String> {
45        self.disconnect().await;
46
47        let url = format!("ws://{host}:{port}");
48        let (ws, _) = tokio_tungstenite::connect_async(&url)
49            .await
50            .map_err(|e| format!("WebSocket connection failed: {e}"))?;
51
52        let (ws_write, ws_read) = ws.split();
53
54        // Writer task: forwards messages from channel to WebSocket
55        let (write_tx, mut write_rx) = mpsc::unbounded_channel::<String>();
56        let writer_handle = tokio::spawn(async move {
57            let mut ws_write = ws_write;
58            while let Some(msg) = write_rx.recv().await {
59                if ws_write.send(Message::Text(msg.into())).await.is_err() {
60                    break;
61                }
62            }
63        });
64
65        // Reader task: receives messages from WebSocket and resolves pending requests
66        let pending = self.pending.clone();
67        let reader_handle = tokio::spawn(async move {
68            let mut ws_read = ws_read;
69            while let Some(Ok(msg)) = ws_read.next().await {
70                if let Message::Text(text) = msg {
71                    let text: &str = text.as_ref();
72                    if let Ok(response) = serde_json::from_str::<Value>(text) {
73                        let id = response
74                            .get("id")
75                            .and_then(|v| v.as_str())
76                            .unwrap_or("")
77                            .to_string();
78
79                        let mut pending = pending.lock().await;
80                        if let Some(req) = pending.remove(&id) {
81                            let result = if let Some(error) = response.get("error") {
82                                Err(error.as_str().unwrap_or("Unknown error").to_string())
83                            } else {
84                                Ok(response.get("result").cloned().unwrap_or(Value::Null))
85                            };
86                            let _ = req.tx.send(result);
87                        }
88                    }
89                }
90            }
91            // Connection closed — reject all pending
92            let mut pending = pending.lock().await;
93            for (_, req) in pending.drain() {
94                let _ = req.tx.send(Err("Connection closed".to_string()));
95            }
96            drop(writer_handle);
97        });
98
99        self.write_tx = Some(write_tx);
100        self._reader_handle = Some(reader_handle);
101
102        Ok(())
103    }
104
105    /// Disconnect from the WebSocket server.
106    pub async fn disconnect(&mut self) {
107        self.write_tx = None;
108        if let Some(handle) = self._reader_handle.take() {
109            handle.abort();
110        }
111        let mut pending = self.pending.lock().await;
112        for (_, req) in pending.drain() {
113            let _ = req.tx.send(Err("Disconnected".to_string()));
114        }
115    }
116
117    /// Check if connected.
118    pub fn is_connected(&self) -> bool {
119        self.write_tx.is_some()
120    }
121
122    /// Send a command and wait for a response.
123    pub async fn send(&self, command: Value) -> Result<Value, String> {
124        self.send_with_timeout(command, DEFAULT_TIMEOUT_MS).await
125    }
126
127    /// Send a command with a custom timeout.
128    pub async fn send_with_timeout(
129        &self,
130        command: Value,
131        timeout_ms: u64,
132    ) -> Result<Value, String> {
133        let write_tx = self
134            .write_tx
135            .as_ref()
136            .ok_or_else(|| "Not connected".to_string())?;
137
138        let id = uuid::Uuid::new_v4().to_string();
139        let (tx, rx) = oneshot::channel();
140
141        {
142            let mut pending = self.pending.lock().await;
143            pending.insert(id.clone(), PendingRequest { tx });
144        }
145
146        // Build the message with the id
147        let mut msg = match command {
148            Value::Object(map) => map,
149            _ => return Err("Command must be a JSON object".to_string()),
150        };
151        msg.insert("id".to_string(), Value::String(id.clone()));
152
153        let json = serde_json::to_string(&msg).map_err(|e| e.to_string())?;
154        write_tx
155            .send(json)
156            .map_err(|_| "Send failed: connection closed".to_string())?;
157
158        // Wait for response with timeout
159        match tokio::time::timeout(Duration::from_millis(timeout_ms), rx).await {
160            Ok(Ok(result)) => result,
161            Ok(Err(_)) => {
162                self.pending.lock().await.remove(&id);
163                Err("Response channel closed".to_string())
164            }
165            Err(_) => {
166                self.pending.lock().await.remove(&id);
167                Err("Request timeout".to_string())
168            }
169        }
170    }
171}
172
173impl Default for ConnectorClient {
174    fn default() -> Self {
175        Self::new()
176    }
177}