use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use futures::StreamExt;
use tokio::sync::{Mutex, oneshot};
use crate::error::{DaimonError, Result};
use crate::mcp::protocol::{JsonRpcNotification, JsonRpcRequest, JsonRpcResponse};
use crate::mcp::transport::McpTransport;
type PendingMap = Arc<Mutex<HashMap<u64, oneshot::Sender<JsonRpcResponse>>>>;
pub struct SseTransport {
post_url: Arc<Mutex<String>>,
pending: PendingMap,
client: reqwest::Client,
headers: HashMap<String, String>,
reader_task: tokio::task::JoinHandle<()>,
is_closed: Arc<AtomicBool>,
}
impl SseTransport {
pub async fn connect(url: impl Into<String>, headers: HashMap<String, String>) -> Result<Self> {
let url = url.into();
let client = reqwest::Client::new();
let mut req = client.get(&url).header("Accept", "text/event-stream");
for (key, value) in &headers {
req = req.header(key.as_str(), value.as_str());
}
let resp = req
.send()
.await
.map_err(|e| DaimonError::Mcp(format!("SSE connect failed: {e}")))?;
if !resp.status().is_success() {
let status = resp.status();
return Err(DaimonError::Mcp(format!(
"SSE connect failed: HTTP {status}"
)));
}
let post_url = Arc::new(Mutex::new(url.clone()));
let pending: PendingMap = Arc::new(Mutex::new(HashMap::new()));
let reader_task = {
let post_url = post_url.clone();
let pending = pending.clone();
let base_url = url.clone();
tokio::spawn(async move {
let mut stream = resp.bytes_stream();
let mut buf = String::new();
while let Some(chunk) = stream.next().await {
let Ok(bytes) = chunk else { break };
buf.push_str(&String::from_utf8_lossy(&bytes).replace("\r\n", "\n"));
while let Some(idx) = buf.find("\n\n") {
let frame_raw = buf[..idx].to_string();
buf.drain(..idx + 2);
if let Some(frame) = parse_sse_frame(&frame_raw) {
handle_frame(frame, &post_url, &base_url, &pending).await;
}
}
}
pending.lock().await.clear();
})
};
Ok(Self {
post_url,
pending,
client,
headers,
reader_task,
is_closed: Arc::new(AtomicBool::new(false)),
})
}
}
struct SseFrame {
event: String,
data: String,
}
fn parse_sse_frame(raw: &str) -> Option<SseFrame> {
let mut event = String::from("message");
let mut data_lines = Vec::new();
for line in raw.split('\n') {
if line.is_empty() || line.starts_with(':') {
continue;
}
if let Some(rest) = line.strip_prefix("event:") {
event = rest.trim().to_string();
} else if let Some(rest) = line.strip_prefix("data:") {
data_lines.push(rest.trim_start().to_string());
}
}
if data_lines.is_empty() {
return None;
}
Some(SseFrame {
event,
data: data_lines.join("\n"),
})
}
fn resolve_endpoint_url(base_url: &str, endpoint_data: &str) -> String {
match reqwest::Url::parse(base_url).and_then(|base| base.join(endpoint_data)) {
Ok(resolved) => resolved.to_string(),
Err(_) => endpoint_data.to_string(),
}
}
async fn handle_frame(
frame: SseFrame,
post_url: &Arc<Mutex<String>>,
base_url: &str,
pending: &PendingMap,
) {
if frame.event == "endpoint" {
let resolved = resolve_endpoint_url(base_url, frame.data.trim());
*post_url.lock().await = resolved;
return;
}
let Ok(response) = serde_json::from_str::<JsonRpcResponse>(&frame.data) else {
return;
};
let Some(id) = response.id else {
return;
};
if let Some(tx) = pending.lock().await.remove(&id) {
let _ = tx.send(response);
}
}
impl McpTransport for SseTransport {
fn send<'a>(
&'a self,
request: &'a JsonRpcRequest,
) -> Pin<Box<dyn Future<Output = Result<JsonRpcResponse>> + Send + 'a>> {
Box::pin(async move {
let (tx, rx) = oneshot::channel();
self.pending.lock().await.insert(request.id, tx);
if self.is_closed.load(Ordering::SeqCst) {
self.pending.lock().await.remove(&request.id);
return Err(DaimonError::Mcp(
"SSE transport closed before a response arrived".into(),
));
}
let post_url = self.post_url.lock().await.clone();
let mut req = self.client.post(&post_url).json(request);
for (key, value) in &self.headers {
req = req.header(key.as_str(), value.as_str());
}
let send_result = req
.send()
.await
.map_err(|e| DaimonError::Mcp(format!("SSE POST failed: {e}")));
let resp = match send_result {
Ok(resp) => resp,
Err(e) => {
self.pending.lock().await.remove(&request.id);
return Err(e);
}
};
if !resp.status().is_success() {
self.pending.lock().await.remove(&request.id);
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
return Err(DaimonError::Mcp(format!(
"SSE POST failed: HTTP {status}: {text}"
)));
}
rx.await.map_err(|_| {
DaimonError::Mcp("SSE transport closed before a response arrived".into())
})
})
}
fn notify<'a>(
&'a self,
notification: &'a JsonRpcNotification,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
let post_url = self.post_url.lock().await.clone();
let mut req = self.client.post(&post_url).json(notification);
for (key, value) in &self.headers {
req = req.header(key.as_str(), value.as_str());
}
let resp = req
.send()
.await
.map_err(|e| DaimonError::Mcp(format!("SSE POST failed: {e}")))?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
return Err(DaimonError::Mcp(format!(
"SSE POST failed: HTTP {status}: {text}"
)));
}
Ok(())
})
}
fn close<'a>(&'a self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
self.reader_task.abort();
self.is_closed.store(true, Ordering::SeqCst);
self.pending.lock().await.clear();
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::SocketAddr;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc;
async fn write_chunk(stream: &mut TcpStream, data: &[u8]) -> std::io::Result<()> {
stream
.write_all(format!("{:x}\r\n", data.len()).as_bytes())
.await?;
stream.write_all(data).await?;
stream.write_all(b"\r\n").await
}
async fn write_chunk_end(stream: &mut TcpStream) -> std::io::Result<()> {
stream.write_all(b"0\r\n\r\n").await
}
async fn write_sse_frame(
stream: &mut TcpStream,
event: Option<&str>,
data: &str,
) -> std::io::Result<()> {
let mut frame = String::new();
if let Some(event) = event {
frame.push_str(&format!("event: {event}\n"));
}
frame.push_str(&format!("data: {data}\n\n"));
write_chunk(stream, frame.as_bytes()).await
}
async fn write_sse_response_head(stream: &mut TcpStream) -> std::io::Result<()> {
stream
.write_all(
b"HTTP/1.1 200 OK\r\n\
Content-Type: text/event-stream\r\n\
Transfer-Encoding: chunked\r\n\
Connection: keep-alive\r\n\r\n",
)
.await
}
async fn read_http_request(stream: &mut TcpStream) -> (String, String, String) {
let mut reader = BufReader::new(&mut *stream);
let mut request_line = String::new();
reader.read_line(&mut request_line).await.unwrap();
let mut parts = request_line.split_whitespace();
let method = parts.next().unwrap_or("").to_string();
let path = parts.next().unwrap_or("").to_string();
let mut content_length = 0usize;
loop {
let mut line = String::new();
reader.read_line(&mut line).await.unwrap();
let trimmed = line.trim();
if trimmed.is_empty() {
break;
}
if let Some((key, value)) = trimmed.split_once(':')
&& key.eq_ignore_ascii_case("content-length")
{
content_length = value.trim().parse().unwrap_or(0);
}
}
let mut body = vec![0u8; content_length];
if content_length > 0 {
reader.read_exact(&mut body).await.unwrap();
}
(method, path, String::from_utf8_lossy(&body).into_owned())
}
async fn write_202_accepted(stream: &mut TcpStream) -> std::io::Result<()> {
stream
.write_all(b"HTTP/1.1 202 Accepted\r\nContent-Length: 0\r\n\r\n")
.await
}
struct TestSseServer {
addr: SocketAddr,
sse_stream: Option<TcpStream>,
sse_stream_rx: Option<tokio::sync::oneshot::Receiver<TcpStream>>,
posted_ids: mpsc::UnboundedReceiver<u64>,
}
impl TestSseServer {
async fn start(endpoint_frame: Option<&str>) -> Self {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let (posted_tx, posted_rx) = mpsc::unbounded_channel();
let (sse_tx, sse_rx) = tokio::sync::oneshot::channel();
let endpoint_frame = endpoint_frame.map(|s| s.to_string());
tokio::spawn(async move {
let mut sse_tx = Some(sse_tx);
loop {
let Ok((mut conn, _)) = listener.accept().await else {
break;
};
if let Some(tx) = sse_tx.take() {
let (_method, _path, _body) = read_http_request(&mut conn).await;
write_sse_response_head(&mut conn).await.unwrap();
if let Some(data) = &endpoint_frame {
write_sse_frame(&mut conn, Some("endpoint"), data)
.await
.unwrap();
}
let _ = tx.send(conn);
continue;
}
let (method, _path, body) = read_http_request(&mut conn).await;
if method == "POST" {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&body)
&& let Some(id) = json.get("id").and_then(|v| v.as_u64())
{
let _ = posted_tx.send(id);
}
let _ = write_202_accepted(&mut conn).await;
}
}
});
Self {
addr,
sse_stream: None,
sse_stream_rx: Some(sse_rx),
posted_ids: posted_rx,
}
}
fn connect_url(&self) -> String {
format!("http://{}/sse", self.addr)
}
async fn sse_stream(&mut self) -> &mut TcpStream {
if self.sse_stream.is_none() {
let rx = self
.sse_stream_rx
.take()
.expect("sse_stream already consumed");
let stream = tokio::time::timeout(std::time::Duration::from_secs(5), rx)
.await
.expect("timed out waiting for the SSE client to connect")
.expect("sse_stream sender dropped");
self.sse_stream = Some(stream);
}
self.sse_stream.as_mut().unwrap()
}
async fn push_response(&mut self, response_json: &str) {
let stream = self.sse_stream().await;
write_sse_frame(stream, None, response_json).await.unwrap();
}
async fn push_raw(&mut self, raw_data: &str) {
let stream = self.sse_stream().await;
write_sse_frame(stream, None, raw_data).await.unwrap();
}
async fn next_posted_id(&mut self) -> u64 {
tokio::time::timeout(std::time::Duration::from_secs(5), self.posted_ids.recv())
.await
.expect("timed out waiting for a POST")
.expect("posted_ids channel closed")
}
async fn close_sse_stream(mut self) {
let stream = self.sse_stream().await;
let _ = write_chunk_end(stream).await;
let _ = stream.shutdown().await;
}
}
#[tokio::test]
async fn test_endpoint_discovery_updates_post_target() {
let server = TestSseServer::start(Some("/messages?sessionId=abc")).await;
let connect_url = server.connect_url();
let transport = SseTransport::connect(connect_url.clone(), HashMap::new())
.await
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let post_url = transport.post_url.lock().await.clone();
assert_eq!(
post_url,
format!("http://{}/messages?sessionId=abc", server.addr)
);
let _ = transport.close().await;
server.close_sse_stream().await;
}
#[tokio::test]
async fn test_falls_back_to_connect_url_without_endpoint_frame() {
let server = TestSseServer::start(None).await;
let connect_url = server.connect_url();
let transport = SseTransport::connect(connect_url.clone(), HashMap::new())
.await
.unwrap();
let post_url = transport.post_url.lock().await.clone();
assert_eq!(post_url, connect_url);
let _ = transport.close().await;
server.close_sse_stream().await;
}
#[tokio::test]
async fn test_correlates_responses_including_out_of_order() {
let mut server = TestSseServer::start(None).await;
let connect_url = server.connect_url();
let transport = Arc::new(
SseTransport::connect(connect_url, HashMap::new())
.await
.unwrap(),
);
let req_a = JsonRpcRequest::new(1, "tools/list", None);
let req_b = JsonRpcRequest::new(2, "tools/list", None);
let ta = transport.clone();
let tb = transport.clone();
let handle_a = tokio::spawn(async move { ta.send(&req_a).await });
let handle_b = tokio::spawn(async move { tb.send(&req_b).await });
let first_posted = server.next_posted_id().await;
let second_posted = server.next_posted_id().await;
assert_eq!(
[first_posted, second_posted]
.iter()
.collect::<std::collections::HashSet<_>>(),
[1u64, 2u64]
.iter()
.collect::<std::collections::HashSet<_>>()
);
server
.push_response(r#"{"jsonrpc":"2.0","id":2,"result":{"ok":"b"}}"#)
.await;
server
.push_response(r#"{"jsonrpc":"2.0","id":1,"result":{"ok":"a"}}"#)
.await;
let resp_a = handle_a.await.unwrap().unwrap();
let resp_b = handle_b.await.unwrap().unwrap();
assert_eq!(resp_a.result.unwrap()["ok"], "a");
assert_eq!(resp_b.result.unwrap()["ok"], "b");
let _ = transport.close().await;
server.close_sse_stream().await;
}
#[tokio::test]
async fn test_malformed_frame_is_skipped_not_fatal() {
let mut server = TestSseServer::start(None).await;
let connect_url = server.connect_url();
let transport = Arc::new(
SseTransport::connect(connect_url, HashMap::new())
.await
.unwrap(),
);
let req = JsonRpcRequest::new(1, "tools/list", None);
let t = transport.clone();
let handle = tokio::spawn(async move { t.send(&req).await });
let posted_id = server.next_posted_id().await;
assert_eq!(posted_id, 1);
server.push_raw("not valid json at all").await;
server
.push_response(r#"{"jsonrpc":"2.0","id":1,"result":{"ok":true}}"#)
.await;
let resp = handle.await.unwrap().unwrap();
assert_eq!(resp.result.unwrap()["ok"], true);
let _ = transport.close().await;
server.close_sse_stream().await;
}
#[tokio::test]
async fn test_connect_failure_surfaces_as_mcp_error() {
let result = SseTransport::connect("http://127.0.0.1:1/sse", HashMap::new()).await;
assert!(matches!(result, Err(DaimonError::Mcp(_))));
}
#[tokio::test]
async fn test_close_fails_pending_sends() {
let mut server = TestSseServer::start(None).await;
let connect_url = server.connect_url();
let transport = Arc::new(
SseTransport::connect(connect_url, HashMap::new())
.await
.unwrap(),
);
let req = JsonRpcRequest::new(1, "tools/list", None);
let t = transport.clone();
let handle = tokio::spawn(async move { t.send(&req).await });
let _ = server.next_posted_id().await;
transport.close().await.unwrap();
let result = handle.await.unwrap();
assert!(matches!(result, Err(DaimonError::Mcp(_))));
server.close_sse_stream().await;
}
}