Skip to main content

pmcp/types/
notifications.rs

1//! Notification types for MCP protocol.
2//!
3//! This module contains notification-related types including progress,
4//! cancellation, logging, and resource update notifications.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9/// Progress notification.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[non_exhaustive]
12#[serde(rename_all = "camelCase")]
13pub struct ProgressNotification {
14    /// Progress token from the original request
15    pub progress_token: ProgressToken,
16    /// Current progress value (must increase with each notification)
17    ///
18    /// This can represent percentage (0-100), count, or any increasing metric.
19    pub progress: f64,
20    /// Optional total value for the operation
21    ///
22    /// When combined with `progress`, allows expressing "5 of 10 items processed".
23    /// Both `progress` and `total` may be floating point values.
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub total: Option<f64>,
26    /// Optional human-readable progress message
27    ///
28    /// Should provide relevant context about the current operation.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub message: Option<String>,
31}
32
33impl ProgressNotification {
34    /// Create a new progress notification with no total value.
35    ///
36    /// Convenience constructor to reduce boilerplate when the total is unknown.
37    pub fn new(progress_token: ProgressToken, progress: f64, message: Option<String>) -> Self {
38        Self {
39            progress_token,
40            progress,
41            total: None,
42            message,
43        }
44    }
45
46    /// Set the total value for the operation.
47    pub fn with_total(mut self, total: f64) -> Self {
48        self.total = Some(total);
49        self
50    }
51}
52
53/// Progress token type.
54#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
55#[serde(untagged)]
56pub enum ProgressToken {
57    /// String token
58    String(String),
59    /// Numeric token
60    Number(i64),
61}
62
63/// Client notification types.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(tag = "method", content = "params", rename_all = "camelCase")]
66pub enum ClientNotification {
67    /// Notification that client has been initialized
68    #[serde(rename = "notifications/initialized")]
69    Initialized,
70    /// Notification that roots have changed
71    #[serde(rename = "notifications/roots/list_changed")]
72    RootsListChanged,
73    /// Notification that a request was cancelled
74    #[serde(rename = "notifications/cancelled")]
75    Cancelled(CancelledNotification),
76    /// Progress update
77    #[serde(rename = "notifications/progress")]
78    Progress(ProgressNotification),
79}
80
81/// Cancelled notification.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83#[non_exhaustive]
84#[serde(rename_all = "camelCase")]
85pub struct CancelledNotification {
86    /// The request ID that was cancelled
87    pub request_id: super::RequestId,
88    /// Optional reason for cancellation
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub reason: Option<String>,
91}
92
93impl CancelledNotification {
94    /// Create a cancelled notification for the given request ID.
95    ///
96    /// `reason` defaults to `None`.
97    pub fn new(request_id: super::RequestId) -> Self {
98        Self {
99            request_id,
100            reason: None,
101        }
102    }
103
104    /// Set the reason for cancellation.
105    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
106        self.reason = Some(reason.into());
107        self
108    }
109}
110
111/// Server notification types.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(tag = "method", content = "params", rename_all = "camelCase")]
114pub enum ServerNotification {
115    /// Progress update
116    #[serde(rename = "notifications/progress")]
117    Progress(ProgressNotification),
118    /// Tools have changed
119    #[serde(rename = "notifications/tools/list_changed")]
120    ToolsChanged,
121    /// Prompts have changed
122    #[serde(rename = "notifications/prompts/list_changed")]
123    PromptsChanged,
124    /// Resources have changed
125    #[serde(rename = "notifications/resources/list_changed")]
126    ResourcesChanged,
127    /// Roots have changed
128    #[serde(rename = "notifications/roots/list_changed")]
129    RootsListChanged,
130    /// Resource was updated
131    #[serde(rename = "notifications/resources/updated")]
132    ResourceUpdated(ResourceUpdatedParams),
133    /// Log message
134    #[serde(rename = "notifications/message")]
135    LogMessage(LogMessageParams),
136    /// Task status changed (MCP 2025-11-25)
137    #[serde(rename = "notifications/tasks/status")]
138    TaskStatus(super::tasks::TaskStatusNotification),
139}
140
141/// Resource updated notification.
142#[derive(Debug, Clone, Serialize, Deserialize)]
143#[non_exhaustive]
144#[serde(rename_all = "camelCase")]
145pub struct ResourceUpdatedParams {
146    /// Resource URI that was updated
147    pub uri: String,
148}
149
150impl ResourceUpdatedParams {
151    /// Create a resource updated notification for the given URI.
152    pub fn new(uri: impl Into<String>) -> Self {
153        Self { uri: uri.into() }
154    }
155}
156
157/// Log message notification.
158#[derive(Debug, Clone, Serialize, Deserialize)]
159#[non_exhaustive]
160#[serde(rename_all = "camelCase")]
161pub struct LogMessageParams {
162    /// Log level
163    pub level: LoggingLevel,
164    /// Logger name/category
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub logger: Option<String>,
167    /// Log message
168    pub message: String,
169    /// Additional data
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub data: Option<Value>,
172}
173
174impl LogMessageParams {
175    /// Create a log message with level and message.
176    ///
177    /// `logger` and `data` default to `None`.
178    pub fn new(level: LoggingLevel, message: impl Into<String>) -> Self {
179        Self {
180            level,
181            logger: None,
182            message: message.into(),
183            data: None,
184        }
185    }
186
187    /// Set the logger name/category.
188    pub fn with_logger(mut self, logger: impl Into<String>) -> Self {
189        self.logger = Some(logger.into());
190        self
191    }
192
193    /// Set additional data.
194    pub fn with_data(mut self, data: Value) -> Self {
195        self.data = Some(data);
196        self
197    }
198}
199
200/// Combined notification types (client or server).
201#[derive(Debug, Clone, Serialize, Deserialize)]
202#[serde(untagged)]
203pub enum Notification {
204    /// Client notification
205    Client(ClientNotification),
206    /// Server notification
207    Server(ServerNotification),
208    /// Progress notification
209    Progress(ProgressNotification),
210    /// Cancelled notification
211    Cancelled(CancelledNotification),
212}
213
214/// Logging level (MCP 2025-11-25 -- full syslog severity).
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(rename_all = "lowercase")]
217pub enum LoggingLevel {
218    /// Debug messages
219    Debug,
220    /// Informational messages
221    Info,
222    /// Notice-level messages
223    Notice,
224    /// Warnings
225    Warning,
226    /// Errors
227    Error,
228    /// Critical errors
229    Critical,
230    /// Alerts requiring immediate action
231    Alert,
232    /// System emergency
233    Emergency,
234}
235
236/// Deprecated: Use [`LoggingLevel`] instead.
237///
238/// This type alias is provided for backward compatibility during
239/// the v2.0 transition. It will be removed in a future release.
240pub type LogLevel = LoggingLevel;
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use serde_json::json;
246
247    #[test]
248    fn test_all_notification_types() {
249        let progress = ServerNotification::Progress(ProgressNotification::new(
250            ProgressToken::String("token123".to_string()),
251            50.0,
252            Some("Processing...".to_string()),
253        ));
254        let json = serde_json::to_value(&progress).unwrap();
255        assert_eq!(json["method"], "notifications/progress");
256
257        let tools_changed = ServerNotification::ToolsChanged;
258        let json = serde_json::to_value(&tools_changed).unwrap();
259        assert_eq!(json["method"], "notifications/tools/list_changed");
260
261        let prompts_changed = ServerNotification::PromptsChanged;
262        let json = serde_json::to_value(&prompts_changed).unwrap();
263        assert_eq!(json["method"], "notifications/prompts/list_changed");
264
265        let resources_changed = ServerNotification::ResourcesChanged;
266        let json = serde_json::to_value(&resources_changed).unwrap();
267        assert_eq!(json["method"], "notifications/resources/list_changed");
268
269        let roots_changed = ServerNotification::RootsListChanged;
270        let json = serde_json::to_value(&roots_changed).unwrap();
271        assert_eq!(json["method"], "notifications/roots/list_changed");
272
273        let resource_updated =
274            ServerNotification::ResourceUpdated(ResourceUpdatedParams::new("file://test.txt"));
275        let json = serde_json::to_value(&resource_updated).unwrap();
276        assert_eq!(json["method"], "notifications/resources/updated");
277
278        let log_msg = ServerNotification::LogMessage(
279            LogMessageParams::new(LoggingLevel::Info, "Test log message")
280                .with_data(json!({"extra": "data"})),
281        );
282        let json = serde_json::to_value(&log_msg).unwrap();
283        assert_eq!(json["method"], "notifications/message");
284    }
285
286    #[test]
287    fn test_logging_level_all_8_values() {
288        assert_eq!(serde_json::to_value(LoggingLevel::Debug).unwrap(), "debug");
289        assert_eq!(serde_json::to_value(LoggingLevel::Info).unwrap(), "info");
290        assert_eq!(
291            serde_json::to_value(LoggingLevel::Notice).unwrap(),
292            "notice"
293        );
294        assert_eq!(
295            serde_json::to_value(LoggingLevel::Warning).unwrap(),
296            "warning"
297        );
298        assert_eq!(serde_json::to_value(LoggingLevel::Error).unwrap(), "error");
299        assert_eq!(
300            serde_json::to_value(LoggingLevel::Critical).unwrap(),
301            "critical"
302        );
303        assert_eq!(serde_json::to_value(LoggingLevel::Alert).unwrap(), "alert");
304        assert_eq!(
305            serde_json::to_value(LoggingLevel::Emergency).unwrap(),
306            "emergency"
307        );
308    }
309
310    #[test]
311    fn test_log_level_alias_works() {
312        // LogLevel is now a type alias for LoggingLevel
313        let level: LogLevel = LoggingLevel::Warning;
314        assert_eq!(serde_json::to_value(level).unwrap(), "warning");
315    }
316
317    #[test]
318    fn test_cancelled_notification() {
319        use crate::types::RequestId;
320
321        let cancelled =
322            CancelledNotification::new(RequestId::Number(123)).with_reason("User cancelled");
323
324        let json = serde_json::to_value(&cancelled).unwrap();
325        assert_eq!(json["requestId"], 123);
326        assert_eq!(json["reason"], "User cancelled");
327    }
328
329    #[test]
330    fn test_task_status_notification() {
331        use crate::types::tasks::{Task, TaskStatus as TStatus, TaskStatusNotification};
332
333        let notif = ServerNotification::TaskStatus(TaskStatusNotification {
334            task: Task::new("t-789", TStatus::Completed)
335                .with_timestamps("2025-11-25T00:00:00Z", "2025-11-25T00:05:00Z")
336                .with_status_message("Done"),
337        });
338        let json = serde_json::to_value(&notif).unwrap();
339        assert_eq!(json["method"], "notifications/tasks/status");
340        assert_eq!(json["params"]["task"]["taskId"], "t-789");
341        assert_eq!(json["params"]["task"]["status"], "completed");
342    }
343}