use dashmap::DashMap;
use futures::future::join_all;
use rmcp::{
ClientHandler, RoleClient, RoleServer,
model::{
ClientCapabilities, ClientInfo, Implementation, ProgressNotificationParam, ProgressToken,
ResourceUpdatedNotificationParam, ServerNotification, TaskStatus, TaskStatusNotification,
TaskStatusNotificationParams,
},
service::{NotificationContext, Peer, ServiceError},
};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, RwLock};
use std::time::Duration;
use tracing::{debug, warn};
pub const PROGRESS_ROUTE_GRACE: Duration = Duration::from_millis(500);
#[derive(Clone, Default, Debug)]
pub struct UpstreamPeerSlot {
peer: Arc<RwLock<Option<Peer<RoleServer>>>>,
}
impl UpstreamPeerSlot {
pub fn new() -> Self {
Self::default()
}
pub fn set(&self, peer: Peer<RoleServer>) {
match self.peer.write() {
Ok(mut guard) => *guard = Some(peer),
Err(poisoned) => *poisoned.into_inner() = Some(peer),
}
}
pub fn get(&self) -> Option<Peer<RoleServer>> {
match self.peer.read() {
Ok(guard) => guard.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
}
}
pub fn clear(&self) {
match self.peer.write() {
Ok(mut guard) => *guard = None,
Err(poisoned) => *poisoned.into_inner() = None,
}
}
}
#[derive(Clone, Debug)]
pub enum NotifyTarget {
Shared(Arc<UpstreamPeerRegistry>),
Session(UpstreamPeerSlot),
}
#[derive(Debug, Default)]
pub struct UpstreamPeerRegistry {
next_id: AtomicU64,
peers: DashMap<u64, Peer<RoleServer>>,
progress_routes: DashMap<ProgressToken, Peer<RoleServer>>,
task_routes: DashMap<String, Peer<RoleServer>>,
}
impl UpstreamPeerRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&self, peer: Peer<RoleServer>) -> u64 {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
self.peers.insert(id, peer);
id
}
pub fn unregister(&self, id: u64) {
self.peers.remove(&id);
}
pub fn len(&self) -> usize {
self.peers.len()
}
pub fn is_empty(&self) -> bool {
self.peers.is_empty()
}
pub fn register_progress(&self, token: ProgressToken, peer: Peer<RoleServer>) {
self.progress_routes.insert(token, peer);
}
pub fn unregister_progress(&self, token: &ProgressToken) {
self.progress_routes.remove(token);
}
pub fn register_task(&self, task_id: impl Into<String>, peer: Peer<RoleServer>) {
self.task_routes.insert(task_id.into(), peer);
}
pub fn unregister_task(&self, task_id: &str) {
self.task_routes.remove(task_id);
}
pub fn reap_closed_peers(&self) {
self.peers.retain(|_, peer| !peer.is_transport_closed());
}
pub async fn deliver_progress(&self, params: ProgressNotificationParam) {
let token = params.progress_token.clone();
let Some(peer) = self
.progress_routes
.get(&token)
.map(|entry| entry.value().clone())
else {
debug!(
?token,
"No progress route for token; dropping backend progress notification"
);
return;
};
if peer.is_transport_closed() {
self.progress_routes.remove(&token);
return;
}
if let Err(err) = peer.notify_progress(params).await {
warn!(?token, error = ?err, "Failed to deliver progress; removing route");
self.progress_routes.remove(&token);
}
}
pub async fn deliver_task_status(&self, params: TaskStatusNotificationParams) {
let task_id = params.task.task.task_id.clone();
let terminal = matches!(
params.task.status(),
TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Cancelled
);
let Some(peer) = self.task_routes.get(&task_id).map(|e| e.value().clone()) else {
debug!(
%task_id,
"No task route for id; dropping backend task status notification"
);
return;
};
if peer.is_transport_closed() {
self.task_routes.remove(&task_id);
return;
}
let send_result = peer
.send_notification(ServerNotification::TaskStatusNotification(
TaskStatusNotification::new(params),
))
.await;
if let Err(err) = send_result {
warn!(%task_id, error = ?err, "Failed to deliver task status; removing route");
self.task_routes.remove(&task_id);
return;
}
if terminal {
self.task_routes.remove(&task_id);
}
}
pub async fn fan_out<F, Fut>(&self, mut send: F)
where
F: FnMut(Peer<RoleServer>) -> Fut,
Fut: std::future::Future<Output = Result<(), ServiceError>>,
{
let snapshot: Vec<(u64, Peer<RoleServer>)> = self
.peers
.iter()
.map(|entry| (*entry.key(), entry.value().clone()))
.collect();
let mut dead = Vec::new();
let mut live = Vec::new();
for (id, peer) in snapshot {
if peer.is_transport_closed() {
dead.push(id);
} else {
live.push((id, peer));
}
}
let results = join_all(live.into_iter().map(|(id, peer)| {
let fut = send(peer);
async move { (id, fut.await) }
}))
.await;
for (id, result) in results {
if let Err(err) = result {
warn!(
peer_id = id,
error = ?err,
"Failed to fan-out notification to upstream peer; removing"
);
dead.push(id);
}
}
for id in dead {
self.peers.remove(&id);
}
}
}
pub struct ProgressRouteGuard {
registry: Arc<UpstreamPeerRegistry>,
token: Option<ProgressToken>,
grace: Duration,
}
impl ProgressRouteGuard {
pub fn register(
registry: Arc<UpstreamPeerRegistry>,
token: ProgressToken,
peer: Peer<RoleServer>,
) -> Self {
registry.register_progress(token.clone(), peer);
Self {
registry,
token: Some(token),
grace: PROGRESS_ROUTE_GRACE,
}
}
pub fn try_from_meta(
registry: Arc<UpstreamPeerRegistry>,
meta: &rmcp::model::RequestMetaObject,
peer: Peer<RoleServer>,
) -> Option<Self> {
let token = meta.get_progress_token()?;
Some(Self::register(registry, token, peer))
}
}
impl Drop for ProgressRouteGuard {
fn drop(&mut self) {
let Some(token) = self.token.take() else {
return;
};
let registry = Arc::clone(&self.registry);
let grace = self.grace;
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
handle.spawn(async move {
tokio::time::sleep(grace).await;
registry.unregister_progress(&token);
});
}
Err(_) => {
registry.unregister_progress(&token);
}
}
}
}
#[derive(Clone, Debug)]
pub struct BackendNotificationBridge {
info: ClientInfo,
target: NotifyTarget,
}
impl BackendNotificationBridge {
pub fn new(info: ClientInfo, target: NotifyTarget) -> Self {
Self { info, target }
}
pub fn with_default_info(upstream_peers: Arc<UpstreamPeerRegistry>) -> Self {
Self::new(
default_backend_client_info(),
NotifyTarget::Shared(upstream_peers),
)
}
pub fn for_session(slot: UpstreamPeerSlot) -> Self {
Self::new(default_backend_client_info(), NotifyTarget::Session(slot))
}
pub fn upstream_peers(&self) -> Option<&Arc<UpstreamPeerRegistry>> {
match &self.target {
NotifyTarget::Shared(reg) => Some(reg),
NotifyTarget::Session(_) => None,
}
}
pub fn session_slot(&self) -> Option<&UpstreamPeerSlot> {
match &self.target {
NotifyTarget::Session(slot) => Some(slot),
NotifyTarget::Shared(_) => None,
}
}
}
pub fn default_backend_client_info() -> ClientInfo {
let capabilities = ClientCapabilities::builder().enable_tasks().build();
ClientInfo::new(
capabilities,
Implementation::new("mcp-streamable-proxy-client", env!("CARGO_PKG_VERSION")),
)
}
async fn with_session_peer<F, Fut>(slot: &UpstreamPeerSlot, send: F)
where
F: FnOnce(Peer<RoleServer>) -> Fut,
Fut: std::future::Future<Output = Result<(), ServiceError>>,
{
let Some(peer) = slot.get() else {
debug!("Session notify slot empty; dropping backend notification");
return;
};
if peer.is_transport_closed() {
slot.clear();
return;
}
if let Err(err) = send(peer).await {
warn!(error = ?err, "Failed to deliver notification to session peer");
slot.clear();
}
}
impl ClientHandler for BackendNotificationBridge {
fn get_info(&self) -> ClientInfo {
self.info.clone()
}
async fn on_resource_updated(
&self,
params: ResourceUpdatedNotificationParam,
_context: NotificationContext<RoleClient>,
) {
match &self.target {
NotifyTarget::Shared(reg) => {
debug!(uri = %params.uri, "Fan-out resources/updated to upstream peers");
reg.fan_out(|peer| {
let params = params.clone();
async move { peer.notify_resource_updated(params).await }
})
.await;
}
NotifyTarget::Session(slot) => {
with_session_peer(slot, |peer| async move {
peer.notify_resource_updated(params).await
})
.await;
}
}
}
async fn on_resource_list_changed(&self, _context: NotificationContext<RoleClient>) {
match &self.target {
NotifyTarget::Shared(reg) => {
reg.fan_out(|peer| async move { peer.notify_resource_list_changed().await })
.await;
}
NotifyTarget::Session(slot) => {
with_session_peer(slot, |peer| async move {
peer.notify_resource_list_changed().await
})
.await;
}
}
}
async fn on_tool_list_changed(&self, _context: NotificationContext<RoleClient>) {
match &self.target {
NotifyTarget::Shared(reg) => {
reg.fan_out(|peer| async move { peer.notify_tool_list_changed().await })
.await;
}
NotifyTarget::Session(slot) => {
with_session_peer(
slot,
|peer| async move { peer.notify_tool_list_changed().await },
)
.await;
}
}
}
async fn on_prompt_list_changed(&self, _context: NotificationContext<RoleClient>) {
match &self.target {
NotifyTarget::Shared(reg) => {
reg.fan_out(|peer| async move { peer.notify_prompt_list_changed().await })
.await;
}
NotifyTarget::Session(slot) => {
with_session_peer(slot, |peer| async move {
peer.notify_prompt_list_changed().await
})
.await;
}
}
}
#[allow(deprecated)]
async fn on_logging_message(
&self,
params: rmcp::model::LoggingMessageNotificationParam,
_context: NotificationContext<RoleClient>,
) {
match &self.target {
NotifyTarget::Shared(reg) => {
reg.fan_out(|peer| {
let params = params.clone();
async move { peer.notify_logging_message(params).await }
})
.await;
}
NotifyTarget::Session(slot) => {
with_session_peer(slot, |peer| async move {
peer.notify_logging_message(params).await
})
.await;
}
}
}
async fn on_task_status(
&self,
params: TaskStatusNotificationParams,
_context: NotificationContext<RoleClient>,
) {
match &self.target {
NotifyTarget::Shared(reg) => {
reg.deliver_task_status(params).await;
}
NotifyTarget::Session(slot) => {
with_session_peer(slot, |peer| async move {
peer.send_notification(ServerNotification::TaskStatusNotification(
TaskStatusNotification::new(params),
))
.await
})
.await;
}
}
}
async fn on_progress(
&self,
params: ProgressNotificationParam,
_context: NotificationContext<RoleClient>,
) {
match &self.target {
NotifyTarget::Shared(reg) => {
reg.deliver_progress(params).await;
}
NotifyTarget::Session(slot) => {
with_session_peer(
slot,
|peer| async move { peer.notify_progress(params).await },
)
.await;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn progress_route_register_unregister() {
let registry = UpstreamPeerRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
registry.unregister(999);
registry.unregister_progress(&ProgressToken(rmcp::model::NumberOrString::Number(1)));
registry.unregister_task("nope");
}
#[test]
fn upstream_peer_slot_set_clear() {
let slot = UpstreamPeerSlot::new();
assert!(slot.get().is_none());
slot.clear();
assert!(slot.get().is_none());
}
}