1use dashmap::DashMap;
12use futures::future::join_all;
13use rmcp::{
14 ClientHandler, RoleClient, RoleServer,
15 model::{
16 ClientCapabilities, ClientInfo, Implementation, ProgressNotificationParam, ProgressToken,
17 ResourceUpdatedNotificationParam, ServerNotification, TaskStatus, TaskStatusNotification,
18 TaskStatusNotificationParams,
19 },
20 service::{NotificationContext, Peer, ServiceError},
21};
22use std::sync::atomic::{AtomicU64, Ordering};
23use std::sync::{Arc, RwLock};
24use std::time::Duration;
25use tracing::{debug, warn};
26
27pub const PROGRESS_ROUTE_GRACE: Duration = Duration::from_millis(500);
32
33#[derive(Clone, Default, Debug)]
35pub struct UpstreamPeerSlot {
36 peer: Arc<RwLock<Option<Peer<RoleServer>>>>,
37}
38
39impl UpstreamPeerSlot {
40 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn set(&self, peer: Peer<RoleServer>) {
47 match self.peer.write() {
48 Ok(mut guard) => *guard = Some(peer),
49 Err(poisoned) => *poisoned.into_inner() = Some(peer),
50 }
51 }
52
53 pub fn get(&self) -> Option<Peer<RoleServer>> {
55 match self.peer.read() {
56 Ok(guard) => guard.clone(),
57 Err(poisoned) => poisoned.into_inner().clone(),
58 }
59 }
60
61 pub fn clear(&self) {
63 match self.peer.write() {
64 Ok(mut guard) => *guard = None,
65 Err(poisoned) => *poisoned.into_inner() = None,
66 }
67 }
68}
69
70#[derive(Clone, Debug)]
72pub enum NotifyTarget {
73 Shared(Arc<UpstreamPeerRegistry>),
75 Session(UpstreamPeerSlot),
77}
78
79#[derive(Debug, Default)]
81pub struct UpstreamPeerRegistry {
82 next_id: AtomicU64,
83 peers: DashMap<u64, Peer<RoleServer>>,
84 progress_routes: DashMap<ProgressToken, Peer<RoleServer>>,
86 task_routes: DashMap<String, Peer<RoleServer>>,
88}
89
90impl UpstreamPeerRegistry {
91 pub fn new() -> Self {
93 Self::default()
94 }
95
96 pub fn register(&self, peer: Peer<RoleServer>) -> u64 {
98 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
99 self.peers.insert(id, peer);
100 id
101 }
102
103 pub fn unregister(&self, id: u64) {
105 self.peers.remove(&id);
106 }
107
108 pub fn len(&self) -> usize {
110 self.peers.len()
111 }
112
113 pub fn is_empty(&self) -> bool {
115 self.peers.is_empty()
116 }
117
118 pub fn register_progress(&self, token: ProgressToken, peer: Peer<RoleServer>) {
120 self.progress_routes.insert(token, peer);
121 }
122
123 pub fn unregister_progress(&self, token: &ProgressToken) {
125 self.progress_routes.remove(token);
126 }
127
128 pub fn register_task(&self, task_id: impl Into<String>, peer: Peer<RoleServer>) {
130 self.task_routes.insert(task_id.into(), peer);
131 }
132
133 pub fn unregister_task(&self, task_id: &str) {
135 self.task_routes.remove(task_id);
136 }
137
138 pub fn reap_closed_peers(&self) {
140 self.peers.retain(|_, peer| !peer.is_transport_closed());
141 }
142
143 pub async fn deliver_progress(&self, params: ProgressNotificationParam) {
145 let token = params.progress_token.clone();
146 let Some(peer) = self
147 .progress_routes
148 .get(&token)
149 .map(|entry| entry.value().clone())
150 else {
151 debug!(
152 ?token,
153 "No progress route for token; dropping backend progress notification"
154 );
155 return;
156 };
157
158 if peer.is_transport_closed() {
159 self.progress_routes.remove(&token);
160 return;
161 }
162
163 if let Err(err) = peer.notify_progress(params).await {
164 warn!(?token, error = ?err, "Failed to deliver progress; removing route");
165 self.progress_routes.remove(&token);
166 }
167 }
168
169 pub async fn deliver_task_status(&self, params: TaskStatusNotificationParams) {
171 let task_id = params.task.task.task_id.clone();
172 let terminal = matches!(
173 params.task.status(),
174 TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Cancelled
175 );
176
177 let Some(peer) = self.task_routes.get(&task_id).map(|e| e.value().clone()) else {
178 debug!(
179 %task_id,
180 "No task route for id; dropping backend task status notification"
181 );
182 return;
183 };
184
185 if peer.is_transport_closed() {
186 self.task_routes.remove(&task_id);
187 return;
188 }
189
190 let send_result = peer
191 .send_notification(ServerNotification::TaskStatusNotification(
192 TaskStatusNotification::new(params),
193 ))
194 .await;
195
196 if let Err(err) = send_result {
197 warn!(%task_id, error = ?err, "Failed to deliver task status; removing route");
198 self.task_routes.remove(&task_id);
199 return;
200 }
201
202 if terminal {
203 self.task_routes.remove(&task_id);
204 }
205 }
206
207 pub async fn fan_out<F, Fut>(&self, mut send: F)
209 where
210 F: FnMut(Peer<RoleServer>) -> Fut,
211 Fut: std::future::Future<Output = Result<(), ServiceError>>,
212 {
213 let snapshot: Vec<(u64, Peer<RoleServer>)> = self
214 .peers
215 .iter()
216 .map(|entry| (*entry.key(), entry.value().clone()))
217 .collect();
218
219 let mut dead = Vec::new();
220 let mut live = Vec::new();
221 for (id, peer) in snapshot {
222 if peer.is_transport_closed() {
223 dead.push(id);
224 } else {
225 live.push((id, peer));
226 }
227 }
228
229 let results = join_all(live.into_iter().map(|(id, peer)| {
230 let fut = send(peer);
231 async move { (id, fut.await) }
232 }))
233 .await;
234
235 for (id, result) in results {
236 if let Err(err) = result {
237 warn!(
238 peer_id = id,
239 error = ?err,
240 "Failed to fan-out notification to upstream peer; removing"
241 );
242 dead.push(id);
243 }
244 }
245
246 for id in dead {
247 self.peers.remove(&id);
248 }
249 }
250}
251
252pub struct ProgressRouteGuard {
254 registry: Arc<UpstreamPeerRegistry>,
255 token: Option<ProgressToken>,
256 grace: Duration,
257}
258
259impl ProgressRouteGuard {
260 pub fn register(
262 registry: Arc<UpstreamPeerRegistry>,
263 token: ProgressToken,
264 peer: Peer<RoleServer>,
265 ) -> Self {
266 registry.register_progress(token.clone(), peer);
267 Self {
268 registry,
269 token: Some(token),
270 grace: PROGRESS_ROUTE_GRACE,
271 }
272 }
273
274 pub fn try_from_meta(
276 registry: Arc<UpstreamPeerRegistry>,
277 meta: &rmcp::model::RequestMetaObject,
278 peer: Peer<RoleServer>,
279 ) -> Option<Self> {
280 let token = meta.get_progress_token()?;
281 Some(Self::register(registry, token, peer))
282 }
283}
284
285impl Drop for ProgressRouteGuard {
286 fn drop(&mut self) {
287 let Some(token) = self.token.take() else {
288 return;
289 };
290 let registry = Arc::clone(&self.registry);
291 let grace = self.grace;
292 match tokio::runtime::Handle::try_current() {
295 Ok(handle) => {
296 handle.spawn(async move {
297 tokio::time::sleep(grace).await;
298 registry.unregister_progress(&token);
299 });
300 }
301 Err(_) => {
302 registry.unregister_progress(&token);
303 }
304 }
305 }
306}
307
308#[derive(Clone, Debug)]
310pub struct BackendNotificationBridge {
311 info: ClientInfo,
312 target: NotifyTarget,
313}
314
315impl BackendNotificationBridge {
316 pub fn new(info: ClientInfo, target: NotifyTarget) -> Self {
318 Self { info, target }
319 }
320
321 pub fn with_default_info(upstream_peers: Arc<UpstreamPeerRegistry>) -> Self {
323 Self::new(
324 default_backend_client_info(),
325 NotifyTarget::Shared(upstream_peers),
326 )
327 }
328
329 pub fn for_session(slot: UpstreamPeerSlot) -> Self {
331 Self::new(default_backend_client_info(), NotifyTarget::Session(slot))
332 }
333
334 pub fn upstream_peers(&self) -> Option<&Arc<UpstreamPeerRegistry>> {
336 match &self.target {
337 NotifyTarget::Shared(reg) => Some(reg),
338 NotifyTarget::Session(_) => None,
339 }
340 }
341
342 pub fn session_slot(&self) -> Option<&UpstreamPeerSlot> {
344 match &self.target {
345 NotifyTarget::Session(slot) => Some(slot),
346 NotifyTarget::Shared(_) => None,
347 }
348 }
349}
350
351pub fn default_backend_client_info() -> ClientInfo {
353 let capabilities = ClientCapabilities::builder().enable_tasks().build();
354 ClientInfo::new(
355 capabilities,
356 Implementation::new("mcp-streamable-proxy-client", env!("CARGO_PKG_VERSION")),
357 )
358}
359
360async fn with_session_peer<F, Fut>(slot: &UpstreamPeerSlot, send: F)
361where
362 F: FnOnce(Peer<RoleServer>) -> Fut,
363 Fut: std::future::Future<Output = Result<(), ServiceError>>,
364{
365 let Some(peer) = slot.get() else {
366 debug!("Session notify slot empty; dropping backend notification");
367 return;
368 };
369 if peer.is_transport_closed() {
370 slot.clear();
371 return;
372 }
373 if let Err(err) = send(peer).await {
374 warn!(error = ?err, "Failed to deliver notification to session peer");
375 slot.clear();
376 }
377}
378
379impl ClientHandler for BackendNotificationBridge {
380 fn get_info(&self) -> ClientInfo {
381 self.info.clone()
382 }
383
384 async fn on_resource_updated(
385 &self,
386 params: ResourceUpdatedNotificationParam,
387 _context: NotificationContext<RoleClient>,
388 ) {
389 match &self.target {
390 NotifyTarget::Shared(reg) => {
391 debug!(uri = %params.uri, "Fan-out resources/updated to upstream peers");
392 reg.fan_out(|peer| {
393 let params = params.clone();
394 async move { peer.notify_resource_updated(params).await }
395 })
396 .await;
397 }
398 NotifyTarget::Session(slot) => {
399 with_session_peer(slot, |peer| async move {
400 peer.notify_resource_updated(params).await
401 })
402 .await;
403 }
404 }
405 }
406
407 async fn on_resource_list_changed(&self, _context: NotificationContext<RoleClient>) {
408 match &self.target {
409 NotifyTarget::Shared(reg) => {
410 reg.fan_out(|peer| async move { peer.notify_resource_list_changed().await })
411 .await;
412 }
413 NotifyTarget::Session(slot) => {
414 with_session_peer(slot, |peer| async move {
415 peer.notify_resource_list_changed().await
416 })
417 .await;
418 }
419 }
420 }
421
422 async fn on_tool_list_changed(&self, _context: NotificationContext<RoleClient>) {
423 match &self.target {
424 NotifyTarget::Shared(reg) => {
425 reg.fan_out(|peer| async move { peer.notify_tool_list_changed().await })
426 .await;
427 }
428 NotifyTarget::Session(slot) => {
429 with_session_peer(
430 slot,
431 |peer| async move { peer.notify_tool_list_changed().await },
432 )
433 .await;
434 }
435 }
436 }
437
438 async fn on_prompt_list_changed(&self, _context: NotificationContext<RoleClient>) {
439 match &self.target {
440 NotifyTarget::Shared(reg) => {
441 reg.fan_out(|peer| async move { peer.notify_prompt_list_changed().await })
442 .await;
443 }
444 NotifyTarget::Session(slot) => {
445 with_session_peer(slot, |peer| async move {
446 peer.notify_prompt_list_changed().await
447 })
448 .await;
449 }
450 }
451 }
452
453 #[allow(deprecated)]
454 async fn on_logging_message(
455 &self,
456 params: rmcp::model::LoggingMessageNotificationParam,
457 _context: NotificationContext<RoleClient>,
458 ) {
459 match &self.target {
460 NotifyTarget::Shared(reg) => {
461 reg.fan_out(|peer| {
462 let params = params.clone();
463 async move { peer.notify_logging_message(params).await }
464 })
465 .await;
466 }
467 NotifyTarget::Session(slot) => {
468 with_session_peer(slot, |peer| async move {
469 peer.notify_logging_message(params).await
470 })
471 .await;
472 }
473 }
474 }
475
476 async fn on_task_status(
477 &self,
478 params: TaskStatusNotificationParams,
479 _context: NotificationContext<RoleClient>,
480 ) {
481 match &self.target {
482 NotifyTarget::Shared(reg) => {
483 reg.deliver_task_status(params).await;
484 }
485 NotifyTarget::Session(slot) => {
486 with_session_peer(slot, |peer| async move {
487 peer.send_notification(ServerNotification::TaskStatusNotification(
488 TaskStatusNotification::new(params),
489 ))
490 .await
491 })
492 .await;
493 }
494 }
495 }
496
497 async fn on_progress(
498 &self,
499 params: ProgressNotificationParam,
500 _context: NotificationContext<RoleClient>,
501 ) {
502 match &self.target {
503 NotifyTarget::Shared(reg) => {
504 reg.deliver_progress(params).await;
505 }
506 NotifyTarget::Session(slot) => {
507 with_session_peer(
508 slot,
509 |peer| async move { peer.notify_progress(params).await },
510 )
511 .await;
512 }
513 }
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520
521 #[test]
522 fn progress_route_register_unregister() {
523 let registry = UpstreamPeerRegistry::new();
524 assert!(registry.is_empty());
525 assert_eq!(registry.len(), 0);
526 registry.unregister(999);
527 registry.unregister_progress(&ProgressToken(rmcp::model::NumberOrString::Number(1)));
528 registry.unregister_task("nope");
529 }
530
531 #[test]
532 fn upstream_peer_slot_set_clear() {
533 let slot = UpstreamPeerSlot::new();
534 assert!(slot.get().is_none());
535 slot.clear();
536 assert!(slot.get().is_none());
537 }
538}