use std::sync::Arc;
use serde_json::{Map, Value};
use crate::application::bridge::{BridgeError, BridgePeer, SendFuture, Unsolicited};
use super::{ClientFrame, LegacySseClient, LegacySseError};
#[derive(Debug)]
pub struct LegacySsePeer {
inner: Arc<LegacySseClient>,
unsolicited: Unsolicited,
}
impl LegacySsePeer {
#[must_use]
pub fn new(inner: Arc<LegacySseClient>) -> Self {
Self {
inner,
unsolicited: Unsolicited::new(),
}
}
#[must_use]
pub const fn client(&self) -> &Arc<LegacySseClient> {
&self.inner
}
pub fn take_pending(&self) -> Vec<Map<String, Value>> {
self.unsolicited.take()
}
}
impl BridgePeer for LegacySsePeer {
fn send(
&self,
envelope: Map<String, Value>,
) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>> {
Box::pin(async move {
let Some(id) = envelope.get("id").cloned() else {
return Err(BridgeError::Unreachable {
reason: "a notification was sent through the request path".to_owned(),
});
};
self.inner
.send(&encode(&envelope)?)
.await
.map_err(|error| unreachable(&error))?;
loop {
match self
.inner
.next()
.await
.map_err(|error| unreachable(&error))?
{
None => {
return Err(BridgeError::Unreachable {
reason: "the stream closed before the peer answered".to_owned(),
});
}
Some(ClientFrame::Endpoint(_)) => {}
Some(ClientFrame::Message(value)) => {
let Some(answer) = value.as_object().cloned() else {
return Err(BridgeError::Unreachable {
reason: "the peer answered with something that is not a JSON-RPC message"
.to_owned(),
});
};
if answer.get("id") == Some(&id) {
return Ok(answer);
}
self.unsolicited.hold(answer);
}
}
}
})
}
fn notify(&self, envelope: Map<String, Value>) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async move {
self.inner
.send(&encode(&envelope)?)
.await
.map_err(|error| unreachable(&error))
})
}
fn close(&self) -> SendFuture<'_, Result<(), BridgeError>> {
Box::pin(async { Ok(()) })
}
fn take_pending(&self) -> Vec<Map<String, Value>> {
Self::take_pending(self)
}
fn wait_pending(&self) -> SendFuture<'static, ()> {
Box::pin(self.unsolicited.wait())
}
}
fn encode(envelope: &Map<String, Value>) -> Result<Vec<u8>, BridgeError> {
serde_json::to_vec(&Value::Object(envelope.clone())).map_err(|error| BridgeError::Unreachable {
reason: format!("the envelope could not be encoded for the wire: {error}"),
})
}
fn unreachable(error: &LegacySseError) -> BridgeError {
BridgeError::Unreachable {
reason: error.to_string(),
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use serde_json::{Map, Value, json};
use crate::adapters::primary::transport::http::{Refusal, ServerConfig, SessionStore};
use crate::adapters::primary::transport::legacy_sse::{
DEFAULT_STREAM_PATH, LegacyDispatcher, LegacyRouter, LegacyTail,
};
use crate::application::bridge::{BridgeError, BridgePeer};
use crate::application::session::IdSource;
use crate::domain::McpSessionId;
use crate::domain::registry::AuthContext;
use crate::domain::session::SessionId;
use super::LegacySsePeer;
use crate::adapters::secondary::transport::legacy_sse::{LegacySseClient, LegacySseConfig};
type TestError = Box<dyn std::error::Error>;
struct Counting(AtomicU64);
impl IdSource for Counting {
fn next_session_id(&self) -> SessionId {
SessionId::new(self.0.fetch_add(1, Ordering::SeqCst))
}
}
struct Pushes {
unsolicited: Option<Value>,
decline: bool,
}
impl LegacyDispatcher for Pushes {
type Session = ();
fn open(&self, _id: SessionId, _tail: &LegacyTail) {}
fn dispatch(
&self,
_auth: &AuthContext,
_session: &McpSessionId,
_state: &(),
frame: &Value,
) -> impl core::future::Future<Output = Result<Vec<Value>, Refusal>> + Send {
core::future::ready({
let id = frame.get("id").cloned().unwrap_or(Value::Null);
let answer = if self.decline {
json!({"jsonrpc": "2.0", "id": id, "error": {"code": -32601, "message": "no"}})
} else {
json!({"jsonrpc": "2.0", "id": id, "result": {"ok": true}})
};
match self.unsolicited.clone() {
Some(extra) => Ok(vec![extra, answer]),
None => Ok(vec![answer]),
}
})
}
}
async fn peer(dispatcher: Pushes) -> Result<LegacySsePeer, TestError> {
let router = LegacyRouter::new(
ServerConfig::new()?,
Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0))))),
dispatcher,
)
.into_router()?;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let port = listener.local_addr()?.port();
tokio::spawn(async move {
let _ = axum::serve(listener, router).await;
});
let config =
LegacySseConfig::new(&format!("http://127.0.0.1:{port}{DEFAULT_STREAM_PATH}"))?;
let client = LegacySseClient::connect(config).await?;
Ok(LegacySsePeer::new(Arc::new(client)))
}
fn envelope(text: &str) -> Result<Map<String, Value>, TestError> {
Ok(serde_json::from_str(text)?)
}
#[tokio::test]
async fn an_answer_comes_back_on_the_stream_carrying_the_callers_id() -> Result<(), TestError> {
let peer = peer(Pushes {
unsolicited: None,
decline: false,
})
.await?;
let answer = peer
.send(envelope(r#"{"jsonrpc":"2.0","id":41,"method":"ping"}"#)?)
.await?;
assert_eq!(answer.get("id"), Some(&json!(41)));
assert_eq!(answer.get("result"), Some(&json!({"ok": true})));
Ok(())
}
#[tokio::test]
async fn an_unsolicited_frame_is_held_rather_than_dropped() -> Result<(), TestError> {
let pushed = json!({
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
});
let peer = peer(Pushes {
unsolicited: Some(pushed.clone()),
decline: false,
})
.await?;
let answer = peer
.send(envelope(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#)?)
.await?;
assert_eq!(answer.get("id"), Some(&json!(1)));
let held = peer.take_pending();
assert_eq!(held.len(), 1, "the pushed frame was not held");
assert_eq!(
held.first().and_then(|frame| frame.get("method")),
Some(&json!("notifications/tools/list_changed"))
);
assert!(peer.take_pending().is_empty());
Ok(())
}
#[tokio::test]
async fn a_refusal_comes_back_through_ok_carrying_the_error_member() -> Result<(), TestError> {
let peer = peer(Pushes {
unsolicited: None,
decline: true,
})
.await?;
let answer = peer
.send(envelope(
r#"{"jsonrpc":"2.0","id":9,"method":"tools/call"}"#,
)?)
.await?;
assert_eq!(
answer
.get("error")
.and_then(|error| error.get("code"))
.and_then(Value::as_i64),
Some(-32601)
);
Ok(())
}
#[tokio::test]
async fn a_notification_on_the_request_path_is_refused() -> Result<(), TestError> {
let peer = peer(Pushes {
unsolicited: None,
decline: false,
})
.await?;
let outcome = peer
.send(envelope(
r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
)?)
.await;
assert!(
matches!(outcome, Err(BridgeError::Unreachable { .. })),
"a notification on the request path was not refused"
);
Ok(())
}
#[tokio::test]
async fn a_notification_posts_without_waiting_for_a_reply() -> Result<(), TestError> {
let peer = peer(Pushes {
unsolicited: None,
decline: false,
})
.await?;
peer.notify(envelope(
r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
)?)
.await?;
Ok(())
}
}