Skip to main content

protocol_droid/application/server/
live.rs

1//! What a server can still change once it is serving, and who hears about it.
2//!
3//! # Why this is separate from the builder
4//!
5//! The builder describes a server before anything is connected to it. This
6//! module is the part that outlives that moment: the handler table a request
7//! looks up, the registry `*/list` reads, and the sessions that asked to be
8//! told when either changes. All three move together, which is why they live
9//! behind one [`Arc`] and why a [`ServerHandle`] is a clone of that `Arc`
10//! rather than a borrow of the [`Server`](super::Server).
11//!
12//! # The ordering that keeps a listing honest
13//!
14//! A tool that is listed and not callable is a server telling a peer something
15//! false. So an addition installs the handler *before* it publishes the
16//! listing, and a removal withdraws the listing *before* it drops the handler.
17//! In between, a request can find a handler for something not yet listed —
18//! harmless, because nobody was told it exists — but never the reverse.
19//!
20//! # Why a change is announced after it is published
21//!
22//! The notification tells a peer to re-list. A peer that re-lists before the
23//! publish lands reads the old set and is never told again, so the announcement
24//! follows the publish, and only for kinds the publish actually touched — a
25//! removal of something that was never there announces nothing.
26
27use std::sync::{Arc, Mutex, PoisonError, Weak};
28
29use arc_swap::ArcSwap;
30
31use crate::application::registry::store::{Published, RegistryStore};
32use crate::application::registry::{Fanout, Subscriber, SubscriberSet};
33use crate::application::server::builder::{PromptHandler, ResourceHandler, ToolHandler};
34use crate::application::server::watch::Watchers;
35use crate::application::session::DynFrameSink;
36use crate::domain::registry::{
37    Primitive, PrimitiveId, PrimitiveKind, Prompt, Registered, Resource, ResourceTemplate, Tool,
38};
39use crate::domain::{Feature, FeatureProfile};
40
41/// A tool's name and the closure that runs it.
42///
43/// Handwritten `Debug`: the handler is a boxed closure, and the derived form
44/// would print a vtable pointer nobody reading a log line can use.
45#[derive(Clone)]
46pub(crate) struct ToolEntry {
47    pub(crate) name: PrimitiveId,
48    pub(crate) handler: ToolHandler,
49}
50
51impl core::fmt::Debug for ToolEntry {
52    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
53        f.debug_struct("ToolEntry")
54            .field("name", &self.name)
55            .finish_non_exhaustive()
56    }
57}
58
59/// Every handler a request can reach, as one immutable value.
60///
61/// Replaced whole on every change rather than edited in place, for the reason
62/// the registry is: a request that loaded this table keeps a consistent view
63/// for as long as it runs, however many changes land meanwhile.
64#[derive(Clone, Default)]
65pub(crate) struct Handlers {
66    pub(crate) tools: Vec<ToolEntry>,
67    pub(crate) resources: Vec<(PrimitiveId, ResourceHandler)>,
68    pub(crate) prompts: Vec<(PrimitiveId, PromptHandler)>,
69}
70
71/// The shared, mutable half of a server.
72pub(crate) struct Live {
73    registry: RegistryStore,
74    handlers: ArcSwap<Handlers>,
75    subscribers: SubscriberSet,
76    watchers: Watchers,
77    /// Serialises writers, so a handler swap and the publish that goes with it
78    /// are never interleaved with another writer's pair.
79    ///
80    /// Readers never take it: both halves are `ArcSwap`, and a lookup is a
81    /// load. The lock is held across two synchronous swaps and never across an
82    /// `.await`, so it cannot stall a request.
83    writer: Mutex<()>,
84}
85
86impl Live {
87    pub(crate) fn new(registry: RegistryStore, handlers: Handlers) -> Self {
88        Self {
89            registry,
90            handlers: ArcSwap::from_pointee(handlers),
91            subscribers: SubscriberSet::new(),
92            watchers: Watchers::default(),
93            writer: Mutex::new(()),
94        }
95    }
96
97    pub(crate) const fn registry(&self) -> &RegistryStore {
98        &self.registry
99    }
100
101    pub(crate) fn tool_count(&self) -> usize {
102        self.handlers.load().tools.len()
103    }
104
105    pub(crate) fn tool(&self, name: &str) -> Option<ToolHandler> {
106        self.handlers
107            .load()
108            .tools
109            .iter()
110            .find(|entry| entry.name.as_str() == name)
111            .map(|entry| Arc::clone(&entry.handler))
112    }
113
114    pub(crate) fn reader(&self, uri: &str) -> Option<ResourceHandler> {
115        self.handlers
116            .load()
117            .resources
118            .iter()
119            .find(|(registered, _)| registered.as_str() == uri)
120            .map(|(_, handler)| Arc::clone(handler))
121    }
122
123    pub(crate) fn getter(&self, name: &str) -> Option<PromptHandler> {
124        self.handlers
125            .load()
126            .prompts
127            .iter()
128            .find(|(registered, _)| registered.as_str() == name)
129            .map(|(_, handler)| Arc::clone(handler))
130    }
131
132    fn swap_handlers(&self, edit: impl FnOnce(&mut Handlers)) {
133        let mut next = Handlers::clone(&self.handlers.load());
134        edit(&mut next);
135        self.handlers.store(Arc::new(next));
136    }
137
138    /// Installs a handler, then lists what it serves.
139    pub(crate) fn add(
140        &self,
141        registered: Registered,
142        edit: impl FnOnce(&mut Handlers),
143    ) -> Published {
144        let _writer = self.writer.lock().unwrap_or_else(PoisonError::into_inner);
145        self.swap_handlers(edit);
146        self.registry.register(registered)
147    }
148
149    /// Withdraws a listing, then drops its handler.
150    pub(crate) fn remove(
151        &self,
152        kind: PrimitiveKind,
153        id: &PrimitiveId,
154        edit: impl FnOnce(&mut Handlers),
155    ) -> Published {
156        let _writer = self.writer.lock().unwrap_or_else(PoisonError::into_inner);
157        let published = self.registry.unregister(kind, id);
158        self.swap_handlers(edit);
159        published
160    }
161
162    async fn announce(&self, published: &Published) -> Fanout {
163        self.subscribers
164            .announce(published.snapshot(), published.changed())
165            .await
166    }
167}
168
169/// The list notifications a session can ask for.
170///
171/// Three, not four: resource templates are announced under
172/// `notifications/resources/list_changed`, because the specification files
173/// them under the `resources` capability and gives them no notification of
174/// their own.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum ListKind {
177    /// `notifications/tools/list_changed`.
178    Tools,
179    /// `notifications/resources/list_changed`, for resources and templates.
180    Resources,
181    /// `notifications/prompts/list_changed`.
182    Prompts,
183}
184
185impl ListKind {
186    /// All three, for a session that negotiated whatever its profile allows.
187    pub const ALL: [Self; 3] = [Self::Tools, Self::Resources, Self::Prompts];
188
189    /// The notification method.
190    #[must_use]
191    pub const fn method(self) -> &'static str {
192        match self {
193            Self::Tools => "notifications/tools/list_changed",
194            Self::Resources => "notifications/resources/list_changed",
195            Self::Prompts => "notifications/prompts/list_changed",
196        }
197    }
198
199    /// The feature a session's profile must hold to be told.
200    #[must_use]
201    pub const fn feature(self) -> Feature {
202        match self {
203            Self::Tools => Feature::ToolsListChanged,
204            Self::Resources => Feature::ResourcesListChanged,
205            Self::Prompts => Feature::PromptsListChanged,
206        }
207    }
208
209    fn subscriber(
210        self,
211        frame: &str,
212        profile: FeatureProfile,
213        sink: Arc<dyn DynFrameSink>,
214    ) -> Subscriber {
215        let primary = match self {
216            Self::Tools => PrimitiveKind::Tool,
217            Self::Resources => PrimitiveKind::Resource,
218            Self::Prompts => PrimitiveKind::Prompt,
219        };
220        let subscriber =
221            Subscriber::new(primary, self.feature(), self.method(), frame, profile, sink);
222        if self == Self::Resources {
223            subscriber.also_for(PrimitiveKind::ResourceTemplate)
224        } else {
225            subscriber
226        }
227    }
228
229    /// The notification, optionally carrying a `_meta` object.
230    ///
231    /// `_meta` is how a `2026-07-28` subscription tags its frames with the
232    /// subscription they belong to; every other caller passes `None` and gets
233    /// the bare notification every revision defines.
234    fn frame(self, meta: Option<&serde_json::Value>) -> String {
235        let mut frame = serde_json::json!({
236            "jsonrpc": "2.0",
237            "method": self.method(),
238        });
239        if let Some(meta) = meta
240            && let Some(object) = frame.as_object_mut()
241        {
242            object.insert("params".to_owned(), serde_json::json!({ "_meta": meta }));
243        }
244        frame.to_string()
245    }
246}
247
248/// A session's standing interest in list changes, withdrawn on drop.
249///
250/// A guard rather than an explicit unsubscribe, because the moments a session
251/// ends — a dropped connection, an expired id, a client that closed its
252/// stream — are moments no handler is running to remember to call anything.
253/// Holding this in the session's own state makes "the session is gone" and
254/// "the session is no longer told" the same event.
255pub struct ListSubscription {
256    live: Weak<Live>,
257    sink: Arc<dyn DynFrameSink>,
258}
259
260impl core::fmt::Debug for ListSubscription {
261    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
262        f.debug_struct("ListSubscription").finish_non_exhaustive()
263    }
264}
265
266impl Drop for ListSubscription {
267    fn drop(&mut self) {
268        if let Some(live) = self.live.upgrade() {
269            live.subscribers.remove_by_sink(&self.sink);
270            live.watchers.forget(&self.sink);
271        }
272    }
273}
274
275/// A clone-able handle that changes a server while it serves.
276///
277/// What a [`Server`](super::Server) hands out from
278/// [`Server::handle`](super::Server::handle). It is cheap to clone and `Send +
279/// Sync`, so it can be stored in a server's own state and reached from inside a
280/// tool: a tool that installs another tool is the shape a plugin loader takes.
281///
282/// Every change publishes the registry and then tells each session whose
283/// negotiated profile includes the matching `listChanged` capability. A session
284/// that never negotiated it hears nothing, so a change is never a message the
285/// peer has no contract for.
286///
287/// # Examples
288///
289/// ```
290/// use std::sync::Arc;
291///
292/// use protocol_droid::application::server::Server;
293/// use protocol_droid::domain::ir::JsonObject;
294/// use protocol_droid::domain::registry::Tool;
295///
296/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
297/// let server = Server::builder("plugins").build();
298/// let handle = server.handle();
299///
300/// let fanout = handle
301///     .add_tool(
302///         Tool::new("late", r#"{"type":"object"}"#)?,
303///         Arc::new(|_ctx, _args| Box::pin(async { JsonObject::default() })),
304///     )
305///     .await;
306/// assert!(fanout.is_silent(), "nobody is connected yet");
307/// assert_eq!(server.tool_count(), 1);
308/// # Ok(())
309/// # }
310/// ```
311#[derive(Clone)]
312pub struct ServerHandle {
313    live: Arc<Live>,
314}
315
316impl core::fmt::Debug for ServerHandle {
317    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
318        f.debug_struct("ServerHandle")
319            .field("tool_count", &self.live.tool_count())
320            .finish_non_exhaustive()
321    }
322}
323
324impl ServerHandle {
325    pub(crate) const fn new(live: Arc<Live>) -> Self {
326        Self { live }
327    }
328
329    /// Adds or replaces a tool, and announces it.
330    pub async fn add_tool(&self, tool: Tool, handler: ToolHandler) -> Fanout {
331        self.add_restricted_tool(Registered::from(Primitive::from(tool)), handler)
332            .await
333    }
334
335    /// Adds or replaces a tool carrying its own visibility requirements.
336    ///
337    /// A `Registered` that is not a tool is listed and given no handler, which
338    /// is the builder's rule for the same input.
339    pub async fn add_restricted_tool(
340        &self,
341        registered: Registered,
342        handler: ToolHandler,
343    ) -> Fanout {
344        let name = match registered.primitive() {
345            Primitive::Tool(tool) => Some(tool.name.clone()),
346            _ => None,
347        };
348        let published = self.live.add(registered, |table| {
349            if let Some(name) = name {
350                table.tools.retain(|entry| entry.name != name);
351                table.tools.push(ToolEntry { name, handler });
352            }
353        });
354        self.live.announce(&published).await
355    }
356
357    /// Removes a tool, and announces it if there was one.
358    pub async fn remove_tool(&self, name: &str) -> Fanout {
359        let id = PrimitiveId::from(name);
360        let published = self.live.remove(PrimitiveKind::Tool, &id, |table| {
361            table.tools.retain(|entry| entry.name != id);
362        });
363        self.live.announce(&published).await
364    }
365
366    /// Adds or replaces a resource with its reader, and announces it.
367    pub async fn add_resource(&self, resource: Resource, reader: ResourceHandler) -> Fanout {
368        let uri = resource.uri.clone();
369        let published = self
370            .live
371            .add(Registered::from(Primitive::from(resource)), |table| {
372                table.resources.retain(|(registered, _)| *registered != uri);
373                table.resources.push((uri, reader));
374            });
375        self.live.announce(&published).await
376    }
377
378    /// Removes a resource, and announces it if there was one.
379    pub async fn remove_resource(&self, uri: &str) -> Fanout {
380        let id = PrimitiveId::from(uri);
381        let published = self.live.remove(PrimitiveKind::Resource, &id, |table| {
382            table.resources.retain(|(registered, _)| *registered != id);
383        });
384        self.live.announce(&published).await
385    }
386
387    /// Adds or replaces a resource template, and announces it.
388    pub async fn add_resource_template(&self, template: ResourceTemplate) -> Fanout {
389        let published = self
390            .live
391            .add(Registered::from(Primitive::from(template)), |_| {});
392        self.live.announce(&published).await
393    }
394
395    /// Removes a resource template by its URI template, and announces it.
396    pub async fn remove_resource_template(&self, uri_template: &str) -> Fanout {
397        let id = PrimitiveId::from(uri_template);
398        let published = self
399            .live
400            .remove(PrimitiveKind::ResourceTemplate, &id, |_| {});
401        self.live.announce(&published).await
402    }
403
404    /// Adds or replaces a prompt with its getter, and announces it.
405    pub async fn add_prompt(&self, prompt: Prompt, getter: PromptHandler) -> Fanout {
406        let name = prompt.name.clone();
407        let published = self
408            .live
409            .add(Registered::from(Primitive::from(prompt)), |table| {
410                table.prompts.retain(|(registered, _)| *registered != name);
411                table.prompts.push((name, getter));
412            });
413        self.live.announce(&published).await
414    }
415
416    /// Removes a prompt, and announces it if there was one.
417    pub async fn remove_prompt(&self, name: &str) -> Fanout {
418        let id = PrimitiveId::from(name);
419        let published = self.live.remove(PrimitiveKind::Prompt, &id, |table| {
420            table.prompts.retain(|(registered, _)| *registered != id);
421        });
422        self.live.announce(&published).await
423    }
424
425    /// Subscribes `sink` to the list notifications in `kinds`.
426    ///
427    /// For a transport, once per session: the session's negotiated `profile`
428    /// gates each notification, and `meta` — when present — is carried as the
429    /// notification's `params._meta`. The subscription lasts until the
430    /// returned guard is dropped.
431    #[must_use]
432    pub fn subscribe(
433        &self,
434        sink: &Arc<dyn DynFrameSink>,
435        profile: FeatureProfile,
436        kinds: &[ListKind],
437        meta: Option<&serde_json::Value>,
438    ) -> ListSubscription {
439        for kind in kinds {
440            self.live.subscribers.add(kind.subscriber(
441                &kind.frame(meta),
442                profile,
443                Arc::clone(sink),
444            ));
445        }
446        ListSubscription {
447            live: Arc::downgrade(&self.live),
448            sink: Arc::clone(sink),
449        }
450    }
451
452    /// Tells every session subscribed to `uri` that the resource changed.
453    ///
454    /// `notifications/resources/updated`, sent to each session that asked for
455    /// `uri` — through `resources/subscribe` before `2026-07-28`, or through
456    /// `resourceSubscriptions` on a listen stream from it on. A resource nobody
457    /// subscribed to tells nobody.
458    pub async fn notify_resource_updated(&self, uri: &str) -> Fanout {
459        self.live.watchers.notify(uri).await
460    }
461
462    /// Adds `uri` to what `sink`'s session is told about.
463    ///
464    /// The watch ends with the session's [`ListSubscription`], or with
465    /// [`Self::unwatch_resource`].
466    pub fn watch_resource(
467        &self,
468        sink: &Arc<dyn DynFrameSink>,
469        uri: &str,
470        meta: Option<&serde_json::Value>,
471    ) {
472        self.live.watchers.watch(sink, uri, meta);
473    }
474
475    /// Stops telling `sink`'s session about `uri`.
476    pub fn unwatch_resource(&self, sink: &Arc<dyn DynFrameSink>, uri: &str) {
477        self.live.watchers.unwatch(sink, uri);
478    }
479
480    /// How many sessions watch at least one resource.
481    #[must_use]
482    pub fn watcher_count(&self) -> usize {
483        self.live.watchers.len()
484    }
485
486    /// Whether a primitive of `kind` is listed under `id` right now.
487    ///
488    /// A read of the live snapshot, so it can be stale the moment it returns
489    /// if another handle is changing the server concurrently — fine for a
490    /// toggle, wrong for anything that needs the answer to still hold.
491    #[must_use]
492    pub fn is_listed(&self, kind: PrimitiveKind, id: &str) -> bool {
493        self.live
494            .registry
495            .snapshot()
496            .contains(kind, &PrimitiveId::from(id))
497    }
498
499    /// How many sessions are currently subscribed, counting one per kind.
500    #[must_use]
501    pub fn subscriber_count(&self) -> usize {
502        self.live.subscribers.len()
503    }
504}
505
506#[cfg(test)]
507mod tests;