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 NotificationParams {
26    pub fn new() -> Self {
27        Self {
28            meta: None,
29            other: HashMap::new(),
30        }
31    }
32
33    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
34        self.meta = Some(meta);
35        self
36    }
37
38    pub fn with_param(mut self, key: impl Into<String>, value: Value) -> Self {
39        self.other.insert(key.into(), value);
40        self
41    }
42}
43
44/// Base notification structure following MCP TypeScript specification
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct Notification {
48    /// Notification method
49    pub method: String,
50    /// Optional notification parameters with _meta support
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub params: Option<NotificationParams>,
53}
54
55impl Notification {
56    pub fn new(method: impl Into<String>) -> Self {
57        Self {
58            method: method.into(),
59            params: None,
60        }
61    }
62
63    pub fn with_params(mut self, params: NotificationParams) -> Self {
64        self.params = Some(params);
65        self
66    }
67}
68
69// ==== Specific Notification Types Following MCP Specification ====
70
71/// Method: "notifications/resources/list_changed" (per MCP spec)
72#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct ResourceListChangedNotification {
75    /// Method name (always "notifications/resources/list_changed")
76    pub method: String,
77    /// Optional empty params with _meta support
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub params: Option<NotificationParams>,
80}
81
82impl ResourceListChangedNotification {
83    pub fn new() -> Self {
84        Self {
85            method: "notifications/resources/list_changed".to_string(),
86            params: None,
87        }
88    }
89
90    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
91        self.params = Some(NotificationParams::new().with_meta(meta));
92        self
93    }
94}
95
96/// Method: "notifications/tools/list_changed" (per MCP spec)
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct ToolListChangedNotification {
100    /// Method name (always "notifications/tools/list_changed")
101    pub method: String,
102    /// Optional empty params with _meta support
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub params: Option<NotificationParams>,
105}
106
107impl ToolListChangedNotification {
108    pub fn new() -> Self {
109        Self {
110            method: "notifications/tools/list_changed".to_string(),
111            params: None,
112        }
113    }
114
115    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
116        self.params = Some(NotificationParams::new().with_meta(meta));
117        self
118    }
119}
120
121/// Method: "notifications/prompts/list_changed" (per MCP spec)
122#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct PromptListChangedNotification {
125    /// Method name (always "notifications/prompts/list_changed")
126    pub method: String,
127    /// Optional empty params with _meta support
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub params: Option<NotificationParams>,
130}
131
132impl PromptListChangedNotification {
133    pub fn new() -> Self {
134        Self {
135            method: "notifications/prompts/list_changed".to_string(),
136            params: None,
137        }
138    }
139
140    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
141        self.params = Some(NotificationParams::new().with_meta(meta));
142        self
143    }
144}
145
146/// Method: "notifications/roots/list_changed" (per MCP spec)
147#[derive(Debug, Clone, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct RootsListChangedNotification {
150    /// Method name (always "notifications/roots/list_changed")
151    pub method: String,
152    /// Optional empty params with _meta support
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub params: Option<NotificationParams>,
155}
156
157impl RootsListChangedNotification {
158    pub fn new() -> Self {
159        Self {
160            method: "notifications/roots/list_changed".to_string(),
161            params: None,
162        }
163    }
164
165    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
166        self.params = Some(NotificationParams::new().with_meta(meta));
167        self
168    }
169}
170
171/// Method: "notifications/progress"
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct ProgressNotification {
175    /// Method name (always "notifications/progress")
176    pub method: String,
177    /// Progress parameters
178    pub params: ProgressNotificationParams,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub struct ProgressNotificationParams {
184    /// Token to correlate with the original request
185    pub progress_token: String,
186    /// Amount of work completed so far
187    pub progress: u64,
188    /// Optional total work count
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub total: Option<u64>,
191    /// Optional human-readable message
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub message: Option<String>,
194    /// Optional MCP meta information
195    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
196    pub meta: Option<HashMap<String, Value>>,
197}
198
199impl ProgressNotification {
200    pub fn new(progress_token: impl Into<String>, progress: u64) -> Self {
201        Self {
202            method: "notifications/progress".to_string(),
203            params: ProgressNotificationParams {
204                progress_token: progress_token.into(),
205                progress,
206                total: None,
207                message: None,
208                meta: None,
209            },
210        }
211    }
212
213    pub fn with_total(mut self, total: u64) -> Self {
214        self.params.total = Some(total);
215        self
216    }
217
218    pub fn with_message(mut self, message: impl Into<String>) -> Self {
219        self.params.message = Some(message.into());
220        self
221    }
222
223    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
224        self.params.meta = Some(meta);
225        self
226    }
227}
228
229/// Method: "notifications/resources/updated"
230#[derive(Debug, Clone, Serialize, Deserialize)]
231#[serde(rename_all = "camelCase")]
232pub struct ResourceUpdatedNotification {
233    /// Method name (always "notifications/resources/updated")
234    pub method: String,
235    /// Parameters with URI and optional _meta
236    pub params: ResourceUpdatedNotificationParams,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct ResourceUpdatedNotificationParams {
242    /// The URI of the resource that was updated
243    pub uri: String,
244    /// Optional MCP meta information
245    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
246    pub meta: Option<HashMap<String, Value>>,
247}
248
249impl ResourceUpdatedNotification {
250    pub fn new(uri: impl Into<String>) -> Self {
251        Self {
252            method: "notifications/resources/updated".to_string(),
253            params: ResourceUpdatedNotificationParams {
254                uri: uri.into(),
255                meta: None,
256            },
257        }
258    }
259
260    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
261        self.params.meta = Some(meta);
262        self
263    }
264}
265
266/// Method: "notifications/cancelled"
267#[derive(Debug, Clone, Serialize, Deserialize)]
268#[serde(rename_all = "camelCase")]
269pub struct CancelledNotification {
270    /// Method name (always "notifications/cancelled")
271    pub method: String,
272    /// Cancellation parameters
273    pub params: CancelledNotificationParams,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
277#[serde(rename_all = "camelCase")]
278pub struct CancelledNotificationParams {
279    /// The ID of the request to cancel
280    pub request_id: RequestId,
281    /// An optional reason for cancelling
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub reason: Option<String>,
284    /// Optional MCP meta information
285    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
286    pub meta: Option<HashMap<String, Value>>,
287}
288
289impl CancelledNotification {
290    pub fn new(request_id: RequestId) -> Self {
291        Self {
292            method: "notifications/cancelled".to_string(),
293            params: CancelledNotificationParams {
294                request_id,
295                reason: None,
296                meta: None,
297            },
298        }
299    }
300
301    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
302        self.params.reason = Some(reason.into());
303        self
304    }
305
306    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
307        self.params.meta = Some(meta);
308        self
309    }
310}
311
312/// Method: "notifications/initialized"
313#[derive(Debug, Clone, Serialize, Deserialize)]
314#[serde(rename_all = "camelCase")]
315pub struct InitializedNotification {
316    /// Method name (always "notifications/initialized")
317    pub method: String,
318    /// Optional empty params with _meta support
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub params: Option<NotificationParams>,
321}
322
323impl InitializedNotification {
324    pub fn new() -> Self {
325        Self {
326            method: "notifications/initialized".to_string(),
327            params: None,
328        }
329    }
330
331    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
332        self.params = Some(NotificationParams::new().with_meta(meta));
333        self
334    }
335}
336
337
338/// Method: "notifications/message"
339#[derive(Debug, Clone, Serialize, Deserialize)]
340#[serde(rename_all = "camelCase")]
341pub struct LoggingMessageNotification {
342    /// Method name (always "notifications/message")
343    pub method: String,
344    /// Logging parameters
345    pub params: LoggingMessageNotificationParams,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
349#[serde(rename_all = "camelCase")]
350pub struct LoggingMessageNotificationParams {
351    /// Log level
352    pub level: LoggingLevel,
353    /// Optional logger name
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub logger: Option<String>,
356    /// Log data (per MCP spec - any serializable type)
357    pub data: Value,
358    /// Optional MCP meta information
359    #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
360    pub meta: Option<HashMap<String, Value>>,
361}
362
363impl LoggingMessageNotification {
364    pub fn new(level: LoggingLevel, data: Value) -> Self {
365        Self {
366            method: "notifications/message".to_string(),
367            params: LoggingMessageNotificationParams {
368                level,
369                logger: None,
370                data,
371                meta: None,
372            },
373        }
374    }
375
376    pub fn with_logger(mut self, logger: impl Into<String>) -> Self {
377        self.params.logger = Some(logger.into());
378        self
379    }
380
381    pub fn with_meta(mut self, meta: HashMap<String, Value>) -> Self {
382        self.params.meta = Some(meta);
383        self
384    }
385}
386
387// ==== Notification Trait Implementations ====
388
389use crate::traits::*;
390
391// Trait implementations for NotificationParams
392impl Params for NotificationParams {}
393
394impl HasMetaParam for NotificationParams {
395    fn meta(&self) -> Option<&HashMap<String, Value>> {
396        self.meta.as_ref()
397    }
398}
399
400// ===========================================
401// === Fine-Grained Notification Traits ===
402// ===========================================
403
404/// Trait for notification metadata (method, type info)
405pub trait HasNotificationMetadata {
406    /// The notification method name
407    fn method(&self) -> &str;
408    
409    /// Optional notification type or category
410    fn notification_type(&self) -> Option<&str> {
411        None
412    }
413    
414    /// Whether this notification requires acknowledgment
415    fn requires_ack(&self) -> bool {
416        false
417    }
418}
419
420/// Trait for notification payload and data structure
421pub trait HasNotificationPayload {
422    /// Get the notification payload data
423    fn payload(&self) -> Option<&Value> {
424        None
425    }
426    
427    /// Serialize notification to JSON
428    fn serialize_payload(&self) -> Result<String, String> {
429        match self.payload() {
430            Some(data) => serde_json::to_string(data)
431                .map_err(|e| format!("Serialization error: {}", e)),
432            None => Ok("{}".to_string()),
433        }
434    }
435}
436
437/// Trait for notification delivery rules and filtering
438pub trait HasNotificationRules {
439    /// Optional delivery priority (higher = more important)
440    fn priority(&self) -> u32 {
441        0
442    }
443    
444    /// Whether this notification can be batched with others
445    fn can_batch(&self) -> bool {
446        true
447    }
448    
449    /// Maximum retry attempts for delivery
450    fn max_retries(&self) -> u32 {
451        3
452    }
453    
454    /// Check if notification should be delivered
455    fn should_deliver(&self) -> bool {
456        true
457    }
458}
459
460/// Composed notification definition trait (automatically implemented via blanket impl)
461pub trait NotificationDefinition: 
462    HasNotificationMetadata + 
463    HasNotificationPayload + 
464    HasNotificationRules 
465{
466    /// Convert this notification definition to a base Notification
467    fn to_notification(&self) -> Notification {
468        let mut notification = Notification::new(self.method());
469        if let Some(payload) = self.payload() {
470            let mut params = NotificationParams::new();
471            // Add payload data to params.other
472            if let Ok(obj) = serde_json::from_value::<HashMap<String, Value>>(payload.clone()) {
473                params.other = obj;
474            }
475            notification = notification.with_params(params);
476        }
477        notification
478    }
479    
480    /// Validate this notification
481    fn validate(&self) -> Result<(), String> {
482        if self.method().is_empty() {
483            return Err("Notification method cannot be empty".to_string());
484        }
485        if !self.method().starts_with("notifications/") {
486            return Err("Notification method must start with 'notifications/'".to_string());
487        }
488        Ok(())
489    }
490}
491
492// Blanket implementation: any type implementing the fine-grained traits automatically gets NotificationDefinition
493impl<T> NotificationDefinition for T 
494where 
495    T: HasNotificationMetadata + HasNotificationPayload + HasNotificationRules 
496{}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501    use serde_json::json;
502
503    #[test]
504    fn test_resource_list_changed() {
505        let notification = ResourceListChangedNotification::new();
506        assert_eq!(notification.method, "notifications/resources/list_changed");
507    }
508
509    #[test]
510    fn test_tool_list_changed() {
511        let notification = ToolListChangedNotification::new();
512        assert_eq!(notification.method, "notifications/tools/list_changed");
513    }
514
515    #[test]
516    fn test_prompt_list_changed() {
517        let notification = PromptListChangedNotification::new();
518        assert_eq!(notification.method, "notifications/prompts/list_changed");
519    }
520
521    #[test]
522    fn test_roots_list_changed() {
523        let notification = RootsListChangedNotification::new();
524        assert_eq!(notification.method, "notifications/roots/list_changed");
525    }
526
527    #[test]
528    fn test_progress_notification() {
529        let notification = ProgressNotification::new("token123", 50)
530            .with_total(100)
531            .with_message("Processing...");
532        
533        assert_eq!(notification.method, "notifications/progress");
534        assert_eq!(notification.params.progress_token, "token123");
535        assert_eq!(notification.params.progress, 50);
536        assert_eq!(notification.params.total, Some(100));
537        assert_eq!(notification.params.message, Some("Processing...".to_string()));
538    }
539
540    #[test]
541    fn test_resource_updated() {
542        let notification = ResourceUpdatedNotification::new("file:///test.txt");
543        assert_eq!(notification.method, "notifications/resources/updated");
544        assert_eq!(notification.params.uri, "file:///test.txt");
545    }
546
547    #[test]
548    fn test_cancelled_notification() {
549        use turul_mcp_json_rpc_server::types::RequestId;
550        let notification = CancelledNotification::new(RequestId::Number(123))
551            .with_reason("User cancelled");
552        
553        assert_eq!(notification.method, "notifications/cancelled");
554        assert_eq!(notification.params.request_id, RequestId::Number(123));
555        assert_eq!(notification.params.reason, Some("User cancelled".to_string()));
556    }
557
558    #[test]
559    fn test_initialized_notification() {
560        let notification = InitializedNotification::new();
561        assert_eq!(notification.method, "notifications/initialized");
562    }
563
564    #[test]
565    fn test_logging_message_notification() {
566        use crate::logging::LoggingLevel;
567        let data = json!({"message": "Test log message", "context": "test"});
568        let notification = LoggingMessageNotification::new(LoggingLevel::Info, data.clone())
569            .with_logger("test-logger");
570        
571        assert_eq!(notification.method, "notifications/message");
572        assert_eq!(notification.params.level, LoggingLevel::Info);
573        assert_eq!(notification.params.logger, Some("test-logger".to_string()));
574        assert_eq!(notification.params.data, data);
575    }
576
577    #[test]
578    fn test_serialization() {
579        let notification = InitializedNotification::new();
580        let json = serde_json::to_string(&notification).unwrap();
581        assert!(json.contains("notifications/initialized"));
582        
583        let parsed: InitializedNotification = serde_json::from_str(&json).unwrap();
584        assert_eq!(parsed.method, "notifications/initialized");
585    }
586}