fix(mcp): prune dead notification subscribers (#11032)

This commit is contained in:
Teddy Tennant
2026-08-10 17:29:29 -04:00
committed by GitHub
parent 7200970cb7
commit 6cf0bf122e
+83 -19
View File
@@ -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<Sender<ServerNotification>>,
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<rmcp::RoleClient>,
) {
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<RoleClient>) {
@@ -390,16 +399,12 @@ impl ClientHandler for GooseClient {
params: rmcp::model::LoggingMessageNotificationParam,
context: rmcp::service::NotificationContext<rmcp::RoleClient>,
) {
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(_)
));
}
}