use std::collections::BTreeSet;
use std::future::Future;
use std::sync::{Mutex, OnceLock};
static ACTIVE: OnceLock<Mutex<BTreeSet<String>>> = OnceLock::new();
fn active() -> &'static Mutex<BTreeSet<String>> {
ACTIVE.get_or_init(|| Mutex::new(BTreeSet::new()))
}
pub fn add(uri: &str) -> bool {
active().lock().unwrap().insert(uri.to_string())
}
pub fn remove(uri: &str) -> bool {
active().lock().unwrap().remove(uri)
}
pub fn list() -> Vec<String> {
active().lock().unwrap().iter().cloned().collect()
}
pub fn contains(uri: &str) -> bool {
active().lock().unwrap().contains(uri)
}
pub fn clear() -> usize {
let mut active = active().lock().unwrap();
let count = active.len();
active.clear();
count
}
pub fn server_supports(capabilities: &serde_json::Value) -> bool {
capabilities
.pointer("/resources/subscribe")
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct ReplayReport {
pub restored: usize,
pub failed: Vec<(String, String)>,
}
pub async fn replay<F, Fut, P>(
uris: Vec<String>,
mut request: F,
connection_lost: P,
) -> Result<ReplayReport, tower_mcp::Error>
where
F: FnMut(String) -> Fut,
Fut: Future<Output = Result<(), tower_mcp::Error>>,
P: Fn(&tower_mcp::Error) -> bool,
{
let mut report = ReplayReport::default();
for uri in uris {
match request(uri.clone()).await {
Ok(()) => report.restored += 1,
Err(error) if connection_lost(&error) => return Err(error),
Err(error) => report.failed.push((uri, error.to_string())),
}
}
Ok(report)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subscriptions_are_tracked_deduplicated_and_ordered() {
assert!(add("note://b"));
assert!(add("note://a"));
assert!(!add("note://a"));
assert!(contains("note://a"));
assert_eq!(list(), ["note://a", "note://b"]);
assert!(remove("note://a"));
assert!(!remove("note://a"));
assert!(!contains("note://a"));
assert_eq!(list(), ["note://b"]);
remove("note://b");
assert!(list().is_empty());
}
#[test]
fn capability_is_read_from_the_initialize_result() {
let yes = serde_json::json!({ "resources": { "subscribe": true } });
let no = serde_json::json!({ "resources": { "listChanged": true } });
assert!(server_supports(&yes));
assert!(!server_supports(&no));
assert!(!server_supports(&serde_json::json!({ "tools": {} })));
}
#[tokio::test]
async fn replay_is_ordered_and_distinguishes_resource_errors_from_connection_loss() {
let seen = std::sync::Arc::new(Mutex::new(Vec::new()));
let requests = seen.clone();
let report = replay(
vec!["note://a".to_string(), "note://b".to_string()],
move |uri| {
requests.lock().unwrap().push(uri.clone());
async move {
if uri.ends_with('b') {
Err(tower_mcp::Error::tool("subscription rejected"))
} else {
Ok(())
}
}
},
|_| false,
)
.await
.unwrap();
assert_eq!(*seen.lock().unwrap(), ["note://a", "note://b"]);
assert_eq!(report.restored, 1);
assert_eq!(report.failed.len(), 1);
assert_eq!(report.failed[0].0, "note://b");
let lost = replay(
vec!["note://a".to_string()],
|_uri| async { Err(tower_mcp::Error::SessionExpired) },
|_| true,
)
.await
.unwrap_err();
assert!(matches!(lost, tower_mcp::Error::SessionExpired));
}
}