Skip to main content

flymcprust_core/
notify.rs

1//! Request-scoped notifications: progress (`notifications/progress`) and,
2//! deprecated in 2026-07-28, logging (`notifications/message`).
3//!
4//! Transports that can deliver notifications put a [`NotifyChannel`] in the
5//! request's transport context: stdio writes them on the connection before
6//! the response; HTTP turns the response into an SSE stream at the first
7//! notification (and stays plain JSON otherwise). The server only builds a
8//! [`Notifier`] when the client asked for notifications (a `progressToken`,
9//! or a log level), so ordinary requests pay nothing.
10//!
11//! Sending never blocks a handler. When the client reads slowly, log messages
12//! beyond the buffer are dropped and progress keeps only the latest value,
13//! which is delivered before the final response. Progress values must
14//! increase (the specification requires it): others are ignored. Nothing is
15//! sent after the response.
16
17#![allow(deprecated)] // Logging is deprecated (SEP-2577) but still specified.
18
19use std::sync::{Mutex, PoisonError};
20use std::time::Duration;
21
22use serde::Serialize;
23use serde_json::{Map, Value, json};
24use tokio::sync::mpsc;
25
26use crate::context::Ctx;
27use crate::spec::{self, LoggingLevel, methods};
28
29/// How long the final progress update may wait for buffer space.
30const FLUSH_TIMEOUT: Duration = Duration::from_secs(1);
31
32/// Where a transport delivers request-scoped notifications (serialized
33/// JSON-RPC messages).
34#[derive(Debug, Clone)]
35pub(crate) struct NotifyChannel(pub(crate) mpsc::Sender<Vec<u8>>);
36
37/// Severity rank of a log level (RFC 5424 order).
38pub(crate) fn severity(level: LoggingLevel) -> u8 {
39    match level {
40        LoggingLevel::Debug => 0,
41        LoggingLevel::Info => 1,
42        LoggingLevel::Notice => 2,
43        LoggingLevel::Warning => 3,
44        LoggingLevel::Error => 4,
45        LoggingLevel::Critical => 5,
46        LoggingLevel::Alert => 6,
47        LoggingLevel::Emergency => 7,
48    }
49}
50
51/// Sends the notifications of one request.
52#[derive(Debug)]
53pub(crate) struct Notifier {
54    out: mpsc::Sender<Vec<u8>>,
55    progress_token: Option<spec::ProgressToken>,
56    /// Minimum level of log messages; `None`: no log messages.
57    log_level: Option<LoggingLevel>,
58    state: Mutex<State>,
59}
60
61#[derive(Debug, Default)]
62struct State {
63    last_progress: Option<f64>,
64    /// The latest progress update that did not fit in the buffer.
65    pending_progress: Option<Vec<u8>>,
66    closed: bool,
67}
68
69impl Notifier {
70    pub(crate) fn new(
71        channel: &NotifyChannel,
72        progress_token: Option<spec::ProgressToken>,
73        log_level: Option<LoggingLevel>,
74    ) -> Option<Self> {
75        (progress_token.is_some() || log_level.is_some()).then(|| Self {
76            out: channel.0.clone(),
77            progress_token,
78            log_level,
79            state: Mutex::default(),
80        })
81    }
82
83    fn state(&self) -> std::sync::MutexGuard<'_, State> {
84        self.state.lock().unwrap_or_else(PoisonError::into_inner)
85    }
86
87    fn progress(&self, progress: f64, total: Option<f64>, message: Option<&str>) {
88        let Some(token) = &self.progress_token else {
89            return;
90        };
91        let mut state = self.state();
92        if state.closed
93            || !progress.is_finite()
94            || state.last_progress.is_some_and(|last| progress <= last)
95        {
96            return;
97        }
98        state.last_progress = Some(progress);
99        let mut params = Map::new();
100        params.insert("progressToken".to_owned(), json!(token));
101        params.insert("progress".to_owned(), json!(progress));
102        if let Some(total) = total.filter(|t| t.is_finite()) {
103            params.insert("total".to_owned(), json!(total));
104        }
105        if let Some(message) = message {
106            params.insert("message".to_owned(), json!(message));
107        }
108        let Some(message) = notification(methods::NOTIFICATIONS_PROGRESS, params) else {
109            return;
110        };
111        // Latest wins: a newer update replaces one still waiting for room.
112        state.pending_progress = None;
113        if let Err(mpsc::error::TrySendError::Full(message)) = self.out.try_send(message) {
114            state.pending_progress = Some(message);
115        }
116    }
117
118    fn log(&self, level: LoggingLevel, logger: Option<&str>, data: Value) {
119        let Some(minimum) = self.log_level else {
120            return;
121        };
122        if severity(level) < severity(minimum) || self.state().closed {
123            return;
124        }
125        let mut params = Map::new();
126        params.insert("level".to_owned(), json!(level));
127        params.insert("data".to_owned(), data);
128        if let Some(logger) = logger {
129            params.insert("logger".to_owned(), json!(logger));
130        }
131        if let Some(message) = notification(methods::NOTIFICATIONS_MESSAGE, params) {
132            // A full buffer drops the message: logging is rate-limited.
133            let _ = self.out.try_send(message);
134        }
135    }
136
137    /// Ends the request's notifications: the latest pending progress update
138    /// is delivered (before the response), and nothing is sent afterwards.
139    pub(crate) async fn finish(&self) {
140        let pending = {
141            let mut state = self.state();
142            state.closed = true;
143            state.pending_progress.take()
144        };
145        if let Some(message) = pending {
146            let _ = tokio::time::timeout(FLUSH_TIMEOUT, self.out.send(message)).await;
147        }
148    }
149}
150
151fn notification(method: &str, params: Map<String, Value>) -> Option<Vec<u8>> {
152    serde_json::to_vec(&json!({ "jsonrpc": "2.0", "method": method, "params": params })).ok()
153}
154
155impl Ctx {
156    /// Reports progress to the client (`notifications/progress`), when it
157    /// asked for it with a `progressToken`; otherwise does nothing.
158    ///
159    /// Never blocks: if the client reads slowly, only the latest update is
160    /// kept, and it arrives before the result. `progress` must increase from
161    /// one call to the next (smaller values are ignored); `total` may be
162    /// omitted with `None`.
163    ///
164    /// ```ignore
165    /// for (i, file) in files.iter().enumerate() {
166    ///     ctx.progress(i as f64, files.len() as f64);
167    ///     load(file).await;
168    /// }
169    /// ```
170    pub fn progress(&self, progress: f64, total: impl Into<Option<f64>>) {
171        if let Some(notifier) = self.notifier() {
172            notifier.progress(progress, total.into(), None);
173        }
174    }
175
176    /// Like [`progress`](Self::progress), with a human-readable message.
177    pub fn progress_message(
178        &self,
179        progress: f64,
180        total: impl Into<Option<f64>>,
181        message: impl AsRef<str>,
182    ) {
183        if let Some(notifier) = self.notifier() {
184            notifier.progress(progress, total.into(), Some(message.as_ref()));
185        }
186    }
187
188    /// Whether the client asked for progress notifications.
189    #[must_use]
190    pub fn wants_progress(&self) -> bool {
191        self.notifier().is_some_and(|n| n.progress_token.is_some())
192    }
193
194    /// Sends a log message to the client (`notifications/message`), when it
195    /// asked for messages at this level or above; otherwise does nothing.
196    /// Never blocks; messages beyond the buffer are dropped.
197    ///
198    /// Do not include secrets or internal details: the client sees `data`.
199    /// For the server's own logs, use `tracing` (stderr).
200    #[deprecated(
201        note = "Logging is deprecated as of protocol version 2026-07-28 (SEP-2577); log to stderr with `tracing` instead."
202    )]
203    pub fn log(&self, level: LoggingLevel, data: impl Serialize) {
204        if let Some(notifier) = self.notifier()
205            && let Ok(data) = serde_json::to_value(data)
206        {
207            notifier.log(level, None, data);
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    fn channel(capacity: usize) -> (NotifyChannel, mpsc::Receiver<Vec<u8>>) {
217        let (tx, rx) = mpsc::channel(capacity);
218        (NotifyChannel(tx), rx)
219    }
220
221    fn decode(bytes: &[u8]) -> Value {
222        serde_json::from_slice(bytes).unwrap()
223    }
224
225    #[test]
226    fn nothing_is_built_without_a_request_for_notifications() {
227        let (tx, _rx) = channel(4);
228        assert!(Notifier::new(&tx, None, None).is_none());
229    }
230
231    #[tokio::test]
232    async fn progress_increases_and_keeps_the_latest_value() {
233        let (tx, mut rx) = channel(1);
234        let n = Notifier::new(&tx, Some(spec::ProgressToken::Integer(7)), None).unwrap();
235        n.progress(10.0, Some(100.0), None);
236        n.progress(5.0, Some(100.0), None); // not increasing: ignored
237        n.progress(50.0, Some(100.0), Some("half")); // buffer full: pending
238        n.progress(90.0, Some(100.0), None); // replaces the pending one
239        let first = decode(&rx.recv().await.unwrap());
240        assert_eq!(first["method"], "notifications/progress");
241        assert_eq!(
242            first["params"],
243            json!({ "progressToken": 7, "progress": 10.0, "total": 100.0 })
244        );
245        n.finish().await;
246        let last = decode(&rx.recv().await.unwrap());
247        assert_eq!(last["params"]["progress"], 90.0);
248        // Nothing after the end.
249        n.progress(100.0, Some(100.0), None);
250        assert!(rx.try_recv().is_err());
251    }
252
253    #[test]
254    fn logs_respect_the_level() {
255        let (tx, mut rx) = channel(8);
256        let n = Notifier::new(&tx, None, Some(LoggingLevel::Warning)).unwrap();
257        n.log(LoggingLevel::Info, None, json!("hidden"));
258        n.log(LoggingLevel::Error, Some("db"), json!({ "e": 1 }));
259        let message = decode(&rx.try_recv().unwrap());
260        assert_eq!(
261            message["params"],
262            json!({ "level": "error", "logger": "db", "data": { "e": 1 } })
263        );
264        assert!(rx.try_recv().is_err());
265        // No progress token: progress is ignored.
266        n.progress(1.0, None, None);
267        assert!(rx.try_recv().is_err());
268    }
269}