use async_trait::async_trait;
use futures::{Stream, StreamExt, SinkExt, Sink};
use std::{pin::Pin, sync::Arc, fmt::Display};
use tokio::sync::Mutex;
use tokio_tungstenite::{
connect_async,
tungstenite::{
protocol::Message as WsMessage,
protocol::CloseFrame,
error::Error as WsError,
},
WebSocketStream,
};
use tokio::io::{AsyncRead, AsyncWrite};
use url::Url;
use crate::{
error::Error,
transport::{Message, Transport},
};
const SUBPROTOCOL: &str = "mcp";
type WebSocketConnection<S> = WebSocketStream<S>;
pub struct WebSocketTransport<S> {
connection: Arc<Mutex<WebSocketConnection<S>>>,
}
impl<S> WebSocketTransport<S> {
pub fn from_stream(stream: WebSocketConnection<S>) -> Self {
Self {
connection: Arc::new(Mutex::new(stream)),
}
}
fn convert_to_ws_message(message: &Message) -> Result<WsMessage, Error> {
let json = serde_json::to_string(message)
.map_err(|e| Error::Serialization(e.to_string()))?;
Ok(WsMessage::Text(json))
}
fn parse_ws_message(ws_message: WsMessage) -> Result<Message, Error> {
match ws_message {
WsMessage::Text(text) => {
serde_json::from_str(&text).map_err(|e| Error::Serialization(e.to_string()))
}
WsMessage::Binary(_) => Err(Error::Transport("Binary messages not supported".to_string())),
WsMessage::Ping(_) => Ok(Message::Notification(crate::protocol::Notification {
jsonrpc: crate::protocol::JSONRPC_VERSION.to_string(),
method: "ping".to_string(),
params: None,
})),
WsMessage::Pong(_) => Ok(Message::Notification(crate::protocol::Notification {
jsonrpc: crate::protocol::JSONRPC_VERSION.to_string(),
method: "pong".to_string(),
params: None,
})),
WsMessage::Close(_) => Err(Error::Transport("Connection closed".to_string())),
WsMessage::Frame(_) => Err(Error::Transport("Raw frames not supported".to_string())),
}
}
async fn handle_ws_message<T, E>(connection: &mut T, message: WsMessage) -> Result<Option<Message>, Error>
where
T: Sink<WsMessage, Error = E> + Unpin,
E: Display,
{
match message {
WsMessage::Ping(data) => {
connection.send(WsMessage::Pong(data)).await
.map_err(|e| Error::Transport(e.to_string()))?;
Ok(None)
}
WsMessage::Pong(_) => {
Ok(None)
}
_ => Self::parse_ws_message(message).map(Some),
}
}
}
#[async_trait]
impl<S> Transport for WebSocketTransport<S>
where
S: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static,
{
async fn send(&self, message: Message) -> Result<(), Error> {
let ws_message = Self::convert_to_ws_message(&message)?;
let mut connection = self.connection.lock().await;
connection
.send(ws_message)
.await
.map_err(|e| Error::Transport(e.to_string()))
}
fn receive(&self) -> Pin<Box<dyn Stream<Item = Result<Message, Error>> + Send>> {
let connection = self.connection.clone();
Box::pin(futures::stream::unfold(connection, move |connection| {
let connection = connection.clone();
async move {
let mut guard = connection.lock().await;
loop {
match guard.next().await {
Some(Ok(ws_message)) => {
match Self::handle_ws_message(&mut *guard, ws_message).await {
Ok(Some(message)) => return Some((Ok(message), connection.clone())),
Ok(None) => continue, Err(e) => return Some((Err(e), connection.clone())),
}
}
Some(Err(e)) => return Some((Err(Error::Transport(e.to_string())), connection.clone())),
None => return None,
}
}
}
}))
}
async fn close(&self) -> Result<(), Error> {
let mut connection = self.connection.lock().await;
connection
.send(WsMessage::Close(Some(CloseFrame {
code: 1000u16.into(), reason: "Client initiated close".into(),
})))
.await
.map_err(|e| Error::Transport(e.to_string()))?;
while let Some(msg) = connection.next().await {
match msg {
Ok(WsMessage::Close(_)) => break,
Ok(_) => continue,
Err(e) => {
if matches!(e, WsError::ConnectionClosed) {
break;
}
return Err(Error::Transport(e.to_string()));
}
}
}
Ok(())
}
}
impl WebSocketTransport<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>> {
pub async fn new(url: &str) -> Result<Self, Error> {
let url = Url::parse(url).map_err(|e| Error::Transport(e.to_string()))?;
let (ws_stream, _) = connect_async(url)
.await
.map_err(|e| Error::Transport(e.to_string()))?;
Ok(Self {
connection: Arc::new(Mutex::new(ws_stream)),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::{JSONRPC_VERSION, Notification};
#[tokio::test]
async fn test_message_conversion() {
let message = Message::Notification(Notification {
jsonrpc: JSONRPC_VERSION.to_string(),
method: "test/method".to_string(),
params: Some(serde_json::json!({
"key": "value"
})),
});
let ws_message = WebSocketTransport::<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>::convert_to_ws_message(&message).unwrap();
assert!(matches!(ws_message, WsMessage::Text(_)));
let parsed_message = WebSocketTransport::<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>::parse_ws_message(ws_message).unwrap();
assert!(matches!(parsed_message, Message::Notification(_)));
if let Message::Notification(notification) = parsed_message {
assert_eq!(notification.jsonrpc, JSONRPC_VERSION);
assert_eq!(notification.method, "test/method");
assert!(notification.params.is_some());
}
}
}