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 TaskStatusNotificationParam, TasksCapability,
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: TaskStatusNotificationParam) {
171 let task_id = params.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 #[allow(deprecated)]
354 let capabilities = ClientCapabilities::builder()
355 .enable_experimental()
356 .enable_tasks_with(TasksCapability::client_default())
357 .build();
358 ClientInfo::new(
359 capabilities,
360 Implementation::new("mcp-streamable-proxy-client", env!("CARGO_PKG_VERSION")),
361 )
362}
363
364async fn with_session_peer<F, Fut>(slot: &UpstreamPeerSlot, send: F)
365where
366 F: FnOnce(Peer<RoleServer>) -> Fut,
367 Fut: std::future::Future<Output = Result<(), ServiceError>>,
368{
369 let Some(peer) = slot.get() else {
370 debug!("Session notify slot empty; dropping backend notification");
371 return;
372 };
373 if peer.is_transport_closed() {
374 slot.clear();
375 return;
376 }
377 if let Err(err) = send(peer).await {
378 warn!(error = ?err, "Failed to deliver notification to session peer");
379 slot.clear();
380 }
381}
382
383impl ClientHandler for BackendNotificationBridge {
384 fn get_info(&self) -> ClientInfo {
385 self.info.clone()
386 }
387
388 async fn on_resource_updated(
389 &self,
390 params: ResourceUpdatedNotificationParam,
391 _context: NotificationContext<RoleClient>,
392 ) {
393 match &self.target {
394 NotifyTarget::Shared(reg) => {
395 debug!(uri = %params.uri, "Fan-out resources/updated to upstream peers");
396 reg.fan_out(|peer| {
397 let params = params.clone();
398 async move { peer.notify_resource_updated(params).await }
399 })
400 .await;
401 }
402 NotifyTarget::Session(slot) => {
403 with_session_peer(slot, |peer| async move {
404 peer.notify_resource_updated(params).await
405 })
406 .await;
407 }
408 }
409 }
410
411 async fn on_resource_list_changed(&self, _context: NotificationContext<RoleClient>) {
412 match &self.target {
413 NotifyTarget::Shared(reg) => {
414 reg.fan_out(|peer| async move { peer.notify_resource_list_changed().await })
415 .await;
416 }
417 NotifyTarget::Session(slot) => {
418 with_session_peer(slot, |peer| async move {
419 peer.notify_resource_list_changed().await
420 })
421 .await;
422 }
423 }
424 }
425
426 async fn on_tool_list_changed(&self, _context: NotificationContext<RoleClient>) {
427 match &self.target {
428 NotifyTarget::Shared(reg) => {
429 reg.fan_out(|peer| async move { peer.notify_tool_list_changed().await })
430 .await;
431 }
432 NotifyTarget::Session(slot) => {
433 with_session_peer(
434 slot,
435 |peer| async move { peer.notify_tool_list_changed().await },
436 )
437 .await;
438 }
439 }
440 }
441
442 async fn on_prompt_list_changed(&self, _context: NotificationContext<RoleClient>) {
443 match &self.target {
444 NotifyTarget::Shared(reg) => {
445 reg.fan_out(|peer| async move { peer.notify_prompt_list_changed().await })
446 .await;
447 }
448 NotifyTarget::Session(slot) => {
449 with_session_peer(slot, |peer| async move {
450 peer.notify_prompt_list_changed().await
451 })
452 .await;
453 }
454 }
455 }
456
457 #[allow(deprecated)]
458 async fn on_logging_message(
459 &self,
460 params: rmcp::model::LoggingMessageNotificationParam,
461 _context: NotificationContext<RoleClient>,
462 ) {
463 match &self.target {
464 NotifyTarget::Shared(reg) => {
465 reg.fan_out(|peer| {
466 let params = params.clone();
467 async move { peer.notify_logging_message(params).await }
468 })
469 .await;
470 }
471 NotifyTarget::Session(slot) => {
472 with_session_peer(slot, |peer| async move {
473 peer.notify_logging_message(params).await
474 })
475 .await;
476 }
477 }
478 }
479
480 async fn on_task_status(
481 &self,
482 params: TaskStatusNotificationParam,
483 _context: NotificationContext<RoleClient>,
484 ) {
485 match &self.target {
486 NotifyTarget::Shared(reg) => {
487 reg.deliver_task_status(params).await;
488 }
489 NotifyTarget::Session(slot) => {
490 with_session_peer(slot, |peer| async move {
491 peer.send_notification(ServerNotification::TaskStatusNotification(
492 TaskStatusNotification::new(params),
493 ))
494 .await
495 })
496 .await;
497 }
498 }
499 }
500
501 async fn on_progress(
502 &self,
503 params: ProgressNotificationParam,
504 _context: NotificationContext<RoleClient>,
505 ) {
506 match &self.target {
507 NotifyTarget::Shared(reg) => {
508 reg.deliver_progress(params).await;
509 }
510 NotifyTarget::Session(slot) => {
511 with_session_peer(
512 slot,
513 |peer| async move { peer.notify_progress(params).await },
514 )
515 .await;
516 }
517 }
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524
525 #[test]
526 fn progress_route_register_unregister() {
527 let registry = UpstreamPeerRegistry::new();
528 assert!(registry.is_empty());
529 assert_eq!(registry.len(), 0);
530 registry.unregister(999);
531 registry.unregister_progress(&ProgressToken(rmcp::model::NumberOrString::Number(1)));
532 registry.unregister_task("nope");
533 }
534
535 #[test]
536 fn upstream_peer_slot_set_clear() {
537 let slot = UpstreamPeerSlot::new();
538 assert!(slot.get().is_none());
539 slot.clear();
540 assert!(slot.get().is_none());
541 }
542}