turul_mcp_protocol_2025_06_18/
notifications.rs

1//! MCP Notifications Protocol Types
2//!
3//! This module defines types for notifications in MCP according to the 2025-06-18 specification.
4//! MCP notifications are JSON-RPC notifications that inform clients about server state changes.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::HashMap;
9
10use crate::logging::LoggingLevel;
11use turul_mcp_json_rpc_server::types::RequestId;
12
13/// Base notification parameters that can include _meta
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct NotificationParams {
17    /// Optional MCP meta information
18    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
19    pub meta: Option<HashMap<String, Value>>,
20    /// All other notification-specific parameters
21    #[serde(flatten)]
22    pub other: HashMap<String, Value>,
23}
24
25impl Default for NotificationParams {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl NotificationParams {
32    pub fn new() -> Self {
33        Self {
34            meta: None,
35            other: HashMap::new(),
36        }
37    }
38
39    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
40        self.meta = Some(meta);
41        self
42    }
43
44    pub fn with_param(mut self, key: impl Into<String>, value: Value) -> Self {
45        self.other.insert(key.into(), value);
46        self
47    }
48}
49
50/// Base notification structure following MCP TypeScript specification
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct Notification {
54    /// Notification method
55    pub method: String,
56    /// Optional notification parameters with _meta support
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub params: Option<NotificationParams>,
59}
60
61impl Notification {
62    pub fn new(method: impl Into<String>) -> Self {
63        Self {
64            method: method.into(),
65            params: None,
66        }
67    }
68
69    pub fn with_params(mut self, params: NotificationParams) -> Self {
70        self.params = Some(params);
71        self
72    }
73}
74
75// ==== Specific Notification Types Following MCP Specification ====
76
77/// Method: "notifications/resources/listChanged" (per MCP spec)
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct ResourceListChangedNotification {
81    /// Method name (always "notifications/resources/listChanged")
82    pub method: String,
83    /// Optional empty params with _meta support
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub params: Option<NotificationParams>,
86}
87
88impl Default for ResourceListChangedNotification {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94impl ResourceListChangedNotification {
95    pub fn new() -> Self {
96        Self {
97            method: "notifications/resources/listChanged".to_string(),
98            params: None,
99        }
100    }
101
102    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
103        self.params = Some(NotificationParams::new().with_meta(meta));
104        self
105    }
106}
107
108/// Method: "notifications/tools/listChanged" (per MCP spec)
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct ToolListChangedNotification {
112    /// Method name (always "notifications/tools/listChanged")
113    pub method: String,
114    /// Optional empty params with _meta support
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub params: Option<NotificationParams>,
117}
118
119impl Default for ToolListChangedNotification {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl ToolListChangedNotification {
126    pub fn new() -> Self {
127        Self {
128            method: "notifications/tools/listChanged".to_string(),
129            params: None,
130        }
131    }
132
133    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
134        self.params = Some(NotificationParams::new().with_meta(meta));
135        self
136    }
137}
138
139/// Method: "notifications/prompts/listChanged" (per MCP spec)
140#[derive(Debug, Clone, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct PromptListChangedNotification {
143    /// Method name (always "notifications/prompts/listChanged")
144    pub method: String,
145    /// Optional empty params with _meta support
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub params: Option<NotificationParams>,
148}
149
150impl Default for PromptListChangedNotification {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156impl PromptListChangedNotification {
157    pub fn new() -> Self {
158        Self {
159            method: "notifications/prompts/listChanged".to_string(),
160            params: None,
161        }
162    }
163
164    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
165        self.params = Some(NotificationParams::new().with_meta(meta));
166        self
167    }
168}
169
170/// Method: "notifications/roots/listChanged" (per MCP spec)
171#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct RootsListChangedNotification {
174    /// Method name (always "notifications/roots/listChanged")
175    pub method: String,
176    /// Optional empty params with _meta support
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub params: Option<NotificationParams>,
179}
180
181impl Default for RootsListChangedNotification {
182    fn default() -> Self {
183        Self::new()
184    }
185}
186
187impl RootsListChangedNotification {
188    pub fn new() -> Self {
189        Self {
190            method: "notifications/roots/listChanged".to_string(),
191            params: None,
192        }
193    }
194
195    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
196        self.params = Some(NotificationParams::new().with_meta(meta));
197        self
198    }
199}
200
201/// Method: "notifications/progress"
202#[derive(Debug, Clone, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase")]
204pub struct ProgressNotification {
205    /// Method name (always "notifications/progress")
206    pub method: String,
207    /// Progress parameters
208    pub params: ProgressNotificationParams,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct ProgressNotificationParams {
214    /// Token to correlate with the original request
215    pub progress_token: String,
216    /// Amount of work completed so far
217    pub progress: u64,
218    /// Optional total work count
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub total: Option<u64>,
221    /// Optional human-readable message
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub message: Option<String>,
224    /// Optional MCP meta information
225    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
226    pub meta: Option<HashMap<String, Value>>,
227}
228
229impl ProgressNotification {
230    pub fn new(progress_token: impl Into<String>, progress: u64) -> Self {
231        Self {
232            method: "notifications/progress".to_string(),
233            params: ProgressNotificationParams {
234                progress_token: progress_token.into(),
235                progress,
236                total: None,
237                message: None,
238                meta: None,
239            },
240        }
241    }
242
243    pub fn with_total(mut self, total: u64) -> Self {
244        self.params.total = Some(total);
245        self
246    }
247
248    pub fn with_message(mut self, message: impl Into<String>) -> Self {
249        self.params.message = Some(message.into());
250        self
251    }
252
253    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
254        self.params.meta = Some(meta);
255        self
256    }
257}
258
259/// Method: "notifications/resources/updated"
260#[derive(Debug, Clone, Serialize, Deserialize)]
261#[serde(rename_all = "camelCase")]
262pub struct ResourceUpdatedNotification {
263    /// Method name (always "notifications/resources/updated")
264    pub method: String,
265    /// Parameters with URI and optional _meta
266    pub params: ResourceUpdatedNotificationParams,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
270#[serde(rename_all = "camelCase")]
271pub struct ResourceUpdatedNotificationParams {
272    /// The URI of the resource that was updated
273    pub uri: String,
274    /// Optional MCP meta information
275    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
276    pub meta: Option<HashMap<String, Value>>,
277}
278
279impl ResourceUpdatedNotification {
280    pub fn new(uri: impl Into<String>) -> Self {
281        Self {
282            method: "notifications/resources/updated".to_string(),
283            params: ResourceUpdatedNotificationParams {
284                uri: uri.into(),
285                meta: None,
286            },
287        }
288    }
289
290    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
291        self.params.meta = Some(meta);
292        self
293    }
294}
295
296/// Method: "notifications/cancelled"
297#[derive(Debug, Clone, Serialize, Deserialize)]
298#[serde(rename_all = "camelCase")]
299pub struct CancelledNotification {
300    /// Method name (always "notifications/cancelled")
301    pub method: String,
302    /// Cancellation parameters
303    pub params: CancelledNotificationParams,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
307#[serde(rename_all = "camelCase")]
308pub struct CancelledNotificationParams {
309    /// The ID of the request to cancel
310    pub request_id: RequestId,
311    /// An optional reason for cancelling
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub reason: Option<String>,
314    /// Optional MCP meta information
315    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
316    pub meta: Option<HashMap<String, Value>>,
317}
318
319impl CancelledNotification {
320    pub fn new(request_id: RequestId) -> Self {
321        Self {
322            method: "notifications/cancelled".to_string(),
323            params: CancelledNotificationParams {
324                request_id,
325                reason: None,
326                meta: None,
327            },
328        }
329    }
330
331    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
332        self.params.reason = Some(reason.into());
333        self
334    }
335
336    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
337        self.params.meta = Some(meta);
338        self
339    }
340}
341
342/// Method: "notifications/initialized"
343#[derive(Debug, Clone, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub struct InitializedNotification {
346    /// Method name (always "notifications/initialized")
347    pub method: String,
348    /// Optional empty params with _meta support
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub params: Option<NotificationParams>,
351}
352
353impl Default for InitializedNotification {
354    fn default() -> Self {
355        Self::new()
356    }
357}
358
359impl InitializedNotification {
360    pub fn new() -> Self {
361        Self {
362            method: "notifications/initialized".to_string(),
363            params: None,
364        }
365    }
366
367    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
368        self.params = Some(NotificationParams::new().with_meta(meta));
369        self
370    }
371}
372
373/// Method: "notifications/message"
374#[derive(Debug, Clone, Serialize, Deserialize)]
375#[serde(rename_all = "camelCase")]
376pub struct LoggingMessageNotification {
377    /// Method name (always "notifications/message")
378    pub method: String,
379    /// Logging parameters
380    pub params: LoggingMessageNotificationParams,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct LoggingMessageNotificationParams {
386    /// Log level
387    pub level: LoggingLevel,
388    /// Optional logger name
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub logger: Option<String>,
391    /// Log data (per MCP spec - any serializable type)
392    pub data: Value,
393    /// Optional MCP meta information
394    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
395    pub meta: Option<HashMap<String, Value>>,
396}
397
398impl LoggingMessageNotification {
399    pub fn new(level: LoggingLevel, data: Value) -> Self {
400        Self {
401            method: "notifications/message".to_string(),
402            params: LoggingMessageNotificationParams {
403                level,
404                logger: None,
405                data,
406                meta: None,
407            },
408        }
409    }
410
411    pub fn with_logger(mut self, logger: impl Into<String>) -> Self {
412        self.params.logger = Some(logger.into());
413        self
414    }
415
416    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
417        self.params.meta = Some(meta);
418        self
419    }
420}
421
422// ==== Notification Trait Implementations ====
423
424use crate::traits::*;
425
426// Trait implementations for NotificationParams
427impl Params for NotificationParams {}
428
429impl HasMetaParam for NotificationParams {
430    fn meta(&self) -> Option<&HashMap<String, Value>> {
431        self.meta.as_ref()
432    }
433}
434
435// ===========================================
436// === Fine-Grained Notification Traits ===
437// ===========================================
438
439/// Trait for notification metadata (method, type info)
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use serde_json::json;
445
446    #[test]
447    fn test_resource_list_changed() {
448        let notification = ResourceListChangedNotification::new();
449        assert_eq!(notification.method, "notifications/resources/listChanged");
450    }
451
452    #[test]
453    fn test_tool_list_changed() {
454        let notification = ToolListChangedNotification::new();
455        assert_eq!(notification.method, "notifications/tools/listChanged");
456    }
457
458    #[test]
459    fn test_prompt_list_changed() {
460        let notification = PromptListChangedNotification::new();
461        assert_eq!(notification.method, "notifications/prompts/listChanged");
462    }
463
464    #[test]
465    fn test_roots_list_changed() {
466        let notification = RootsListChangedNotification::new();
467        assert_eq!(notification.method, "notifications/roots/listChanged");
468    }
469
470    #[test]
471    fn test_progress_notification() {
472        let notification = ProgressNotification::new("token123", 50)
473            .with_total(100)
474            .with_message("Processing...");
475
476        assert_eq!(notification.method, "notifications/progress");
477        assert_eq!(notification.params.progress_token, "token123");
478        assert_eq!(notification.params.progress, 50);
479        assert_eq!(notification.params.total, Some(100));
480        assert_eq!(
481            notification.params.message,
482            Some("Processing...".to_string())
483        );
484    }
485
486    #[test]
487    fn test_resource_updated() {
488        let notification = ResourceUpdatedNotification::new("file:///test.txt");
489        assert_eq!(notification.method, "notifications/resources/updated");
490        assert_eq!(notification.params.uri, "file:///test.txt");
491    }
492
493    #[test]
494    fn test_cancelled_notification() {
495        use turul_mcp_json_rpc_server::types::RequestId;
496        let notification =
497            CancelledNotification::new(RequestId::Number(123)).with_reason("User cancelled");
498
499        assert_eq!(notification.method, "notifications/cancelled");
500        assert_eq!(notification.params.request_id, RequestId::Number(123));
501        assert_eq!(
502            notification.params.reason,
503            Some("User cancelled".to_string())
504        );
505    }
506
507    #[test]
508    fn test_initialized_notification() {
509        let notification = InitializedNotification::new();
510        assert_eq!(notification.method, "notifications/initialized");
511    }
512
513    #[test]
514    fn test_logging_message_notification() {
515        use crate::logging::LoggingLevel;
516        let data = json!({"message": "Test log message", "context": "test"});
517        let notification = LoggingMessageNotification::new(LoggingLevel::Info, data.clone())
518            .with_logger("test-logger");
519
520        assert_eq!(notification.method, "notifications/message");
521        assert_eq!(notification.params.level, LoggingLevel::Info);
522        assert_eq!(notification.params.logger, Some("test-logger".to_string()));
523        assert_eq!(notification.params.data, data);
524    }
525
526    #[test]
527    fn test_serialization() {
528        let notification = InitializedNotification::new();
529        let json = serde_json::to_string(&notification).unwrap();
530        assert!(json.contains("notifications/initialized"));
531
532        let parsed: InitializedNotification = serde_json::from_str(&json).unwrap();
533        assert_eq!(parsed.method, "notifications/initialized");
534    }
535}