From 6cf0bf122ea2420bb6e4cd25b47771576b6c33b8 Mon Sep 17 00:00:00 2001 From: Teddy Tennant Date: Mon, 10 Aug 2026 17:29:29 -0400 Subject: [PATCH] fix(mcp): prune dead notification subscribers (#11032) --- crates/goose/src/agents/mcp_client.rs | 102 +++++++++++++++++++++----- 1 file changed, 83 insertions(+), 19 deletions(-) diff --git a/crates/goose/src/agents/mcp_client.rs b/crates/goose/src/agents/mcp_client.rs index f3fff9bf1..052d71cc1 100644 --- a/crates/goose/src/agents/mcp_client.rs +++ b/crates/goose/src/agents/mcp_client.rs @@ -355,6 +355,18 @@ fn working_dir_roots(dir: &std::path::Path) -> ListRootsResult { ListRootsResult::new(vec![Root::new(uri).with_name("working_directory")]) } +/// Fan out a notification to all subscribers, dropping senders whose receivers are gone. +fn fan_out_notification( + handlers: &mut Vec>, + notification: ServerNotification, +) { + handlers.retain(|handler| match handler.try_send(notification.clone()) { + Ok(()) => true, + Err(mpsc::error::TrySendError::Full(_)) => true, + Err(mpsc::error::TrySendError::Closed(_)) => false, + }); +} + impl ClientHandler for GooseClient { #[expect(deprecated)] async fn list_roots( @@ -369,15 +381,12 @@ impl ClientHandler for GooseClient { params: rmcp::model::ProgressNotificationParam, context: rmcp::service::NotificationContext, ) { - self.notification_handlers - .lock() - .await - .iter() - .for_each(|handler| { - let mut not = Notification::new(params.clone()); - not.extensions = context.extensions.clone(); - let _ = handler.try_send(ServerNotification::ProgressNotification(not)); - }); + let mut not = Notification::new(params); + not.extensions = context.extensions; + fan_out_notification( + &mut *self.notification_handlers.lock().await, + ServerNotification::ProgressNotification(not), + ); } async fn on_tool_list_changed(&self, _context: rmcp::service::NotificationContext) { @@ -390,16 +399,12 @@ impl ClientHandler for GooseClient { params: rmcp::model::LoggingMessageNotificationParam, context: rmcp::service::NotificationContext, ) { - self.notification_handlers - .lock() - .await - .iter() - .for_each(|handler| { - let mut notification = LoggingMessageNotification::new(params.clone()); - notification.extensions = context.extensions.clone(); - let _ = - handler.try_send(ServerNotification::LoggingMessageNotification(notification)); - }); + let mut notification = LoggingMessageNotification::new(params); + notification.extensions = context.extensions; + fan_out_notification( + &mut *self.notification_handlers.lock().await, + ServerNotification::LoggingMessageNotification(notification), + ); } #[expect(deprecated)] @@ -1616,4 +1621,63 @@ mod tests { assert_eq!(result.roots[0].uri, "file:///tmp/test-project"); assert_eq!(result.roots[0].name.as_deref(), Some("working_directory")); } + + #[tokio::test] + async fn fan_out_notification_prunes_closed_subscribers() { + use rmcp::model::{NumberOrString, ProgressNotificationParam, ProgressToken}; + + let handlers = Arc::new(Mutex::new(Vec::new())); + let mut receivers = Vec::new(); + for _ in 0..5 { + let (tx, rx) = mpsc::channel(16); + handlers.lock().await.push(tx); + receivers.push(rx); + } + assert_eq!(handlers.lock().await.len(), 5); + + // Drop all receivers, simulating tool-call completion. + drop(receivers); + + let notification = ServerNotification::ProgressNotification(Notification::new( + ProgressNotificationParam::new( + ProgressToken(NumberOrString::String(Arc::from("token"))), + 1.0, + ), + )); + fan_out_notification(&mut *handlers.lock().await, notification); + assert!( + handlers.lock().await.is_empty(), + "closed subscribers must be pruned on fan-out" + ); + + // A live subscriber survives fan-out; subsequent closed ones still prune. + let mut live_rx = { + let (tx, rx) = mpsc::channel(16); + handlers.lock().await.push(tx); + rx + }; + for _ in 0..3 { + let (tx, rx) = mpsc::channel(16); + handlers.lock().await.push(tx); + drop(rx); + } + assert_eq!(handlers.lock().await.len(), 4); + + let notification = ServerNotification::ProgressNotification(Notification::new( + ProgressNotificationParam::new( + ProgressToken(NumberOrString::String(Arc::from("token-2"))), + 2.0, + ), + )); + fan_out_notification(&mut *handlers.lock().await, notification.clone()); + assert_eq!(handlers.lock().await.len(), 1); + let received = live_rx + .recv() + .await + .expect("live subscriber should receive"); + assert!(matches!( + received, + ServerNotification::ProgressNotification(_) + )); + } }