use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use serde_json::{Value, json};
use turbomcp_core::{JsonRpcMessage, JsonRpcNotification, RequestId, meta};
use turbomcp_protocol::methods;
use turbomcp_protocol::v2026_07_28::types as v0728;
use turbomcp_service::outbound;
pub(crate) const COALESCE_WINDOW_MS: u64 = 50;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum ListChangedKind {
Tools,
Resources,
Prompts,
}
impl ListChangedKind {
fn method(self) -> &'static str {
match self {
Self::Tools => methods::notification::TOOLS_LIST_CHANGED,
Self::Resources => methods::notification::RESOURCES_LIST_CHANGED,
Self::Prompts => methods::notification::PROMPTS_LIST_CHANGED,
}
}
fn slot(self) -> usize {
match self {
Self::Tools => 0,
Self::Resources => 1,
Self::Prompts => 2,
}
}
fn wants(self, filter: &v0728::SubscriptionFilter) -> bool {
match self {
Self::Tools => filter.tools_list_changed == Some(true),
Self::Resources => filter.resources_list_changed == Some(true),
Self::Prompts => filter.prompts_list_changed == Some(true),
}
}
}
pub(crate) const MAX_LEGACY_ROUTES: usize = 4096;
#[derive(Default)]
struct LegacyRoute {
connection: String,
uris: HashSet<String>,
}
#[derive(Default)]
pub(crate) struct SubscriptionRegistry {
inner: Mutex<HashMap<(String, RequestId), v0728::SubscriptionFilter>>,
legacy: Mutex<HashMap<String, LegacyRoute>>,
pending: [AtomicBool; 3],
}
impl SubscriptionRegistry {
pub(crate) fn insert(
&self,
connection: &str,
id: &RequestId,
filter: v0728::SubscriptionFilter,
) {
self.lock()
.insert((connection.to_owned(), id.clone()), filter);
}
pub(crate) fn remove(&self, connection: &str, id: &RequestId) -> bool {
self.lock()
.remove(&(connection.to_owned(), id.clone()))
.is_some()
}
pub(crate) fn legacy_touch(&self, session: &str, connection: Option<&str>) {
let mut routes = self.lock_legacy();
if !routes.contains_key(session) && routes.len() >= MAX_LEGACY_ROUTES {
if let Some(victim) = routes.keys().next().cloned() {
routes.remove(&victim);
}
}
let route = routes.entry(session.to_owned()).or_default();
if let Some(conn) = connection {
conn.clone_into(&mut route.connection);
}
}
pub(crate) fn legacy_subscribe(&self, session: &str, connection: Option<&str>, uri: String) {
self.legacy_touch(session, connection);
self.lock_legacy()
.get_mut(session)
.expect("touched above")
.uris
.insert(uri);
}
pub(crate) fn legacy_unsubscribe(&self, session: &str, uri: &str) {
if let Some(route) = self.lock_legacy().get_mut(session) {
route.uris.remove(uri);
}
}
pub(crate) fn legacy_remove(&self, session: &str) -> bool {
self.lock_legacy().remove(session).is_some()
}
pub(crate) fn schedule_list_changed(self: &Arc<Self>, kind: ListChangedKind) {
if self.pending[kind.slot()].swap(true, Ordering::AcqRel) {
return; }
let registry = Arc::clone(self);
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(COALESCE_WINDOW_MS)).await;
registry.pending[kind.slot()].store(false, Ordering::Release);
registry
.publish(kind.method(), None, |f| kind.wants(f))
.await;
registry.publish_legacy(kind.method(), None, |_| true).await;
});
}
pub(crate) async fn publish_resource_updated(&self, uri: &str) {
self.publish(
methods::notification::RESOURCES_UPDATED,
Some(("uri", json!(uri))),
|f| f.resource_subscriptions.iter().any(|u| u == uri),
)
.await;
self.publish_legacy(
methods::notification::RESOURCES_UPDATED,
Some(("uri", json!(uri))),
|route| route.uris.contains(uri),
)
.await;
}
async fn publish_legacy(
&self,
method: &str,
extra: Option<(&str, Value)>,
wants: impl Fn(&LegacyRoute) -> bool,
) {
let targets: Vec<(String, String)> = self
.lock_legacy()
.iter()
.filter(|(_, route)| wants(route))
.map(|(session, route)| (session.clone(), route.connection.clone()))
.collect();
for (session, connection) in targets {
let Some(writer) = legacy_writer(&session, &connection) else {
continue; };
let params = extra
.as_ref()
.map(|(key, value)| json!({ *key: value.clone() }));
let note = JsonRpcNotification::new(method, params);
let _ = writer.send(note.into()).await;
}
}
async fn publish(
&self,
method: &str,
extra: Option<(&str, Value)>,
wants: impl Fn(&v0728::SubscriptionFilter) -> bool,
) {
let targets: Vec<(String, RequestId)> = self
.lock()
.iter()
.filter(|(_, filter)| wants(filter))
.map(|(key, _)| key.clone())
.collect();
for (connection, id) in targets {
let Some(writer) = outbound::writer(&connection) else {
self.remove(&connection, &id);
continue;
};
let note = subscription_notification(method, &id, extra.clone());
if writer.send(note).await.is_err() {
self.remove(&connection, &id);
}
}
}
pub(crate) async fn close_all(&self) {
let targets: Vec<(String, RequestId)> = {
let mut live = self.lock();
let targets = live.keys().cloned().collect();
live.clear();
targets
};
for (connection, id) in targets {
let Some(writer) = outbound::writer(&connection) else {
continue;
};
let result = json!({
"resultType": turbomcp_protocol::neutral::result_type::COMPLETE,
"_meta": { meta::keys::SUBSCRIPTION_ID: subscription_id_value(&id) },
});
let _ = writer
.send(turbomcp_core::JsonRpcResponse::success(id, result).into())
.await;
}
}
fn lock(
&self,
) -> std::sync::MutexGuard<'_, HashMap<(String, RequestId), v0728::SubscriptionFilter>> {
self.inner.lock().expect("subscription registry poisoned")
}
fn lock_legacy(&self) -> std::sync::MutexGuard<'_, HashMap<String, LegacyRoute>> {
self.legacy.lock().expect("legacy route registry poisoned")
}
}
pub(crate) fn legacy_writer(
session: &str,
connection: &str,
) -> Option<tokio::sync::mpsc::Sender<JsonRpcMessage>> {
outbound::writer(&outbound::session_stream_id(session)).or_else(|| {
(!connection.is_empty())
.then(|| outbound::writer(connection))
.flatten()
})
}
pub(crate) fn request_writer(
connection: &str,
session: &str,
) -> Option<tokio::sync::mpsc::Sender<JsonRpcMessage>> {
(!connection.is_empty())
.then(|| outbound::writer(connection))
.flatten()
.or_else(|| {
(!session.is_empty())
.then(|| outbound::writer(&outbound::session_stream_id(session)))
.flatten()
})
}
pub(crate) fn subscription_id_value(id: &RequestId) -> Value {
match id {
RequestId::Number(n) => json!(n),
RequestId::String(s) => json!(s),
}
}
fn subscription_notification(
method: &str,
id: &RequestId,
extra: Option<(&str, Value)>,
) -> JsonRpcMessage {
let mut params = serde_json::Map::new();
params.insert(
"_meta".to_owned(),
json!({ meta::keys::SUBSCRIPTION_ID: subscription_id_value(id) }),
);
if let Some((key, value)) = extra {
params.insert(key.to_owned(), value);
}
JsonRpcNotification::new(method, Some(Value::Object(params))).into()
}
#[derive(Clone)]
pub struct ServerNotifier {
subs: Arc<SubscriptionRegistry>,
}
impl ServerNotifier {
pub(crate) fn new(subs: Arc<SubscriptionRegistry>) -> Self {
Self { subs }
}
pub fn tools_list_changed(&self) {
self.subs.schedule_list_changed(ListChangedKind::Tools);
}
pub fn resources_list_changed(&self) {
self.subs.schedule_list_changed(ListChangedKind::Resources);
}
pub fn prompts_list_changed(&self) {
self.subs.schedule_list_changed(ListChangedKind::Prompts);
}
pub async fn resource_updated(&self, uri: &str) {
self.subs.publish_resource_updated(uri).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn filter(tools: bool, uris: &[&str]) -> v0728::SubscriptionFilter {
v0728::SubscriptionFilter {
tools_list_changed: tools.then_some(true),
resources_list_changed: None,
prompts_list_changed: None,
resource_subscriptions: uris.iter().map(|s| (*s).to_owned()).collect(),
}
}
#[tokio::test]
async fn publish_respects_filters_and_stamps_subscription_id() {
let (tx, mut rx) = tokio::sync::mpsc::channel(8);
let _guard = outbound::register("sub-test-conn", tx);
let reg = Arc::new(SubscriptionRegistry::default());
reg.insert(
"sub-test-conn",
&RequestId::from(1i64),
filter(true, &["file://a"]),
);
reg.insert("sub-test-conn", &RequestId::from(2i64), filter(false, &[]));
reg.publish_resource_updated("file://a").await;
reg.publish(methods::notification::TOOLS_LIST_CHANGED, None, |f| {
f.tools_list_changed == Some(true)
})
.await;
let mut methods_seen = Vec::new();
while let Ok(msg) = rx.try_recv() {
let JsonRpcMessage::Notification(n) = msg else {
panic!("expected notification");
};
let meta = &n.params.as_ref().unwrap()["_meta"];
assert_eq!(
meta[meta::keys::SUBSCRIPTION_ID],
json!(1),
"only subscription 1 opted in to anything — and the id rides verbatim (a number, not \"1\")"
);
methods_seen.push(n.method);
}
assert_eq!(
methods_seen,
vec![
methods::notification::RESOURCES_UPDATED.to_owned(),
methods::notification::TOOLS_LIST_CHANGED.to_owned(),
]
);
}
#[tokio::test]
async fn close_all_answers_every_subscription_and_clears() {
let (tx, mut rx) = tokio::sync::mpsc::channel(8);
let _guard = outbound::register("close-conn", tx);
let reg = Arc::new(SubscriptionRegistry::default());
reg.insert("close-conn", &RequestId::from(7i64), filter(true, &[]));
reg.insert(
"close-conn",
&RequestId::String("listen-a".into()),
filter(true, &[]),
);
reg.close_all().await;
let mut closed = Vec::new();
while let Ok(JsonRpcMessage::Response(r)) = rx.try_recv() {
let result = r.result.expect("a result");
assert_eq!(result["resultType"], "complete");
assert_eq!(
result["_meta"]["io.modelcontextprotocol/subscriptionId"],
subscription_id_value(&r.id)
);
closed.push(r.id);
}
assert_eq!(closed.len(), 2, "every live subscription is answered");
assert!(closed.contains(&RequestId::from(7i64)));
assert!(closed.contains(&RequestId::String("listen-a".into())));
reg.publish(methods::notification::TOOLS_LIST_CHANGED, None, |_| true)
.await;
assert!(rx.try_recv().is_err(), "no subscriptions remain");
}
#[tokio::test]
async fn dead_connections_are_pruned_on_publish() {
let reg = Arc::new(SubscriptionRegistry::default());
reg.insert(
"never-registered",
&RequestId::from(1i64),
filter(true, &[]),
);
reg.publish(methods::notification::TOOLS_LIST_CHANGED, None, |_| true)
.await;
assert!(
!reg.remove("never-registered", &RequestId::from(1i64)),
"publish should have pruned the dead subscription"
);
}
#[tokio::test]
async fn list_changed_bursts_coalesce_into_one_notification() {
let (tx, mut rx) = tokio::sync::mpsc::channel(8);
let _guard = outbound::register("coalesce-conn", tx);
let reg = Arc::new(SubscriptionRegistry::default());
reg.insert("coalesce-conn", &RequestId::from(1i64), filter(true, &[]));
let notifier = ServerNotifier::new(Arc::clone(®));
for _ in 0..5 {
notifier.tools_list_changed();
}
tokio::time::sleep(Duration::from_millis(COALESCE_WINDOW_MS * 3)).await;
let first = rx.try_recv().expect("one coalesced notification");
assert!(matches!(
first,
JsonRpcMessage::Notification(n) if n.method == methods::notification::TOOLS_LIST_CHANGED
));
assert!(rx.try_recv().is_err(), "the burst coalesced into one");
}
}