mcp_proxy/
ws_transport.rs1use std::sync::Arc;
20use std::sync::atomic::{AtomicBool, Ordering};
21
22use async_trait::async_trait;
23use futures_util::{SinkExt, StreamExt};
24use tokio::sync::Mutex;
25use tokio_tungstenite::tungstenite::Message;
26
27type WsStream =
28 tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
29
30pub struct WebSocketClientTransport {
35 sink: Arc<Mutex<futures_util::stream::SplitSink<WsStream, Message>>>,
36 stream: Arc<Mutex<futures_util::stream::SplitStream<WsStream>>>,
37 connected: Arc<AtomicBool>,
38}
39
40impl WebSocketClientTransport {
41 pub async fn connect(url: &str) -> anyhow::Result<Self> {
47 let (ws_stream, _response) = tokio_tungstenite::connect_async(url)
48 .await
49 .map_err(|e| anyhow::anyhow!("WebSocket connection failed: {e}"))?;
50
51 let (sink, stream) = ws_stream.split();
52
53 Ok(Self {
54 sink: Arc::new(Mutex::new(sink)),
55 stream: Arc::new(Mutex::new(stream)),
56 connected: Arc::new(AtomicBool::new(true)),
57 })
58 }
59
60 pub async fn connect_with_bearer_token(url: &str, token: &str) -> anyhow::Result<Self> {
64 Self::connect_with_headers(url, Some(token), &Default::default()).await
65 }
66
67 pub async fn connect_with_headers(
70 url: &str,
71 token: Option<&str>,
72 headers: &std::collections::HashMap<String, String>,
73 ) -> anyhow::Result<Self> {
74 use tokio_tungstenite::tungstenite::client::IntoClientRequest;
75 use tokio_tungstenite::tungstenite::http::{HeaderName, HeaderValue};
76 let mut request = url.into_client_request()?;
77 if let Some(token) = token {
78 request.headers_mut().insert(
79 "authorization",
80 HeaderValue::try_from(format!("Bearer {token}"))?,
81 );
82 }
83 for (name, value) in headers {
84 request.headers_mut().insert(
85 HeaderName::try_from(name.as_str())?,
86 HeaderValue::try_from(value.as_str())?,
87 );
88 }
89
90 let (ws_stream, _response) = tokio_tungstenite::connect_async(request)
91 .await
92 .map_err(|e| anyhow::anyhow!("WebSocket connection failed: {e}"))?;
93
94 let (sink, stream) = ws_stream.split();
95
96 Ok(Self {
97 sink: Arc::new(Mutex::new(sink)),
98 stream: Arc::new(Mutex::new(stream)),
99 connected: Arc::new(AtomicBool::new(true)),
100 })
101 }
102}
103
104#[async_trait]
105impl tower_mcp::client::ClientTransport for WebSocketClientTransport {
106 async fn send(&mut self, message: &str) -> tower_mcp::error::Result<()> {
107 let mut sink = self.sink.lock().await;
108 sink.send(Message::Text(message.into()))
109 .await
110 .map_err(|e| tower_mcp::error::Error::Transport(e.to_string()))?;
111 Ok(())
112 }
113
114 async fn recv(&mut self) -> tower_mcp::error::Result<Option<String>> {
115 let mut stream = self.stream.lock().await;
116 loop {
117 match stream.next().await {
118 Some(Ok(Message::Text(text))) => return Ok(Some(text.as_str().to_owned())),
119 Some(Ok(Message::Close(_))) | None => {
120 self.connected.store(false, Ordering::SeqCst);
121 return Ok(None);
122 }
123 Some(Ok(Message::Ping(_) | Message::Pong(_) | Message::Frame(_))) => {
124 continue;
126 }
127 Some(Ok(Message::Binary(data))) => {
128 let text = std::str::from_utf8(&data)
130 .map_err(|e| tower_mcp::error::Error::Transport(e.to_string()))?;
131 return Ok(Some(text.to_string()));
132 }
133 Some(Err(e)) => {
134 self.connected.store(false, Ordering::SeqCst);
135 return Err(tower_mcp::error::Error::Transport(e.to_string()));
136 }
137 }
138 }
139 }
140
141 fn is_connected(&self) -> bool {
142 self.connected.load(Ordering::SeqCst)
143 }
144
145 async fn close(&mut self) -> tower_mcp::error::Result<()> {
146 self.connected.store(false, Ordering::SeqCst);
147 let mut sink = self.sink.lock().await;
148 let _ = sink.send(Message::Close(None)).await;
149 Ok(())
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[tokio::test]
158 async fn connect_fails_with_invalid_url() {
159 let result = WebSocketClientTransport::connect("ws://127.0.0.1:1").await;
160 let err = result.err().expect("should fail").to_string();
161 assert!(
162 err.contains("WebSocket connection failed"),
163 "unexpected error: {err}"
164 );
165 }
166
167 #[tokio::test]
168 async fn connect_with_bearer_token_fails_with_invalid_url() {
169 let result =
170 WebSocketClientTransport::connect_with_bearer_token("ws://127.0.0.1:1", "tok").await;
171 let err = result.err().expect("should fail").to_string();
172 assert!(
173 err.contains("WebSocket connection failed"),
174 "unexpected error: {err}"
175 );
176 }
177}