turul_mcp_builders/
notification.rs

1//! Notification Builder for Runtime Notification Creation
2//!
3//! This module provides builder patterns for creating MCP notifications at runtime.
4//! Supports all standard MCP notification types and custom notifications.
5
6use serde_json::Value;
7use std::collections::HashMap;
8
9// Import protocol types
10use turul_mcp_json_rpc_server::types::RequestId;
11use turul_mcp_protocol::logging::LoggingLevel;
12use turul_mcp_protocol::notifications::{
13    CancelledNotification, InitializedNotification, LoggingMessageNotification, Notification,
14    NotificationParams, ProgressNotification, PromptListChangedNotification,
15    ResourceListChangedNotification, ResourceUpdatedNotification, RootsListChangedNotification,
16    ToolListChangedNotification,
17};
18
19// Import framework traits from local crate
20use crate::traits::{HasNotificationMetadata, HasNotificationPayload, HasNotificationRules};
21
22/// Builder for creating notifications at runtime
23pub struct NotificationBuilder {
24    method: String,
25    params: Option<NotificationParams>,
26    priority: u32,
27    can_batch: bool,
28    max_retries: u32,
29}
30
31impl NotificationBuilder {
32    /// Create a new notification builder with the given method
33    pub fn new(method: impl Into<String>) -> Self {
34        Self {
35            method: method.into(),
36            params: None,
37            priority: 0,
38            can_batch: true,
39            max_retries: 3,
40        }
41    }
42
43    /// Set notification parameters
44    pub fn params(mut self, params: NotificationParams) -> Self {
45        self.params = Some(params);
46        self
47    }
48
49    /// Add a parameter to the notification
50    pub fn param(mut self, key: impl Into<String>, value: Value) -> Self {
51        if self.params.is_none() {
52            self.params = Some(NotificationParams::new());
53        }
54        self.params
55            .as_mut()
56            .unwrap()
57            .other
58            .insert(key.into(), value);
59        self
60    }
61
62    /// Set meta information
63    pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
64        if self.params.is_none() {
65            self.params = Some(NotificationParams::new());
66        }
67        self.params.as_mut().unwrap().meta = Some(meta);
68        self
69    }
70
71    /// Add a meta key-value pair
72    pub fn meta_value(mut self, key: impl Into<String>, value: Value) -> Self {
73        if self.params.is_none() {
74            self.params = Some(NotificationParams::new());
75        }
76        let params = self.params.as_mut().unwrap();
77        if params.meta.is_none() {
78            params.meta = Some(HashMap::new());
79        }
80        params.meta.as_mut().unwrap().insert(key.into(), value);
81        self
82    }
83
84    /// Set notification priority (higher = more important)
85    pub fn priority(mut self, priority: u32) -> Self {
86        self.priority = priority;
87        self
88    }
89
90    /// Set whether this notification can be batched with others
91    pub fn can_batch(mut self, can_batch: bool) -> Self {
92        self.can_batch = can_batch;
93        self
94    }
95
96    /// Set maximum retry attempts
97    pub fn max_retries(mut self, max_retries: u32) -> Self {
98        self.max_retries = max_retries;
99        self
100    }
101
102    /// Build the notification
103    pub fn build(self) -> Notification {
104        let mut notification = Notification::new(self.method);
105        if let Some(params) = self.params {
106            notification = notification.with_params(params);
107        }
108        notification
109    }
110
111    /// Build a dynamic notification that implements the definition traits
112    pub fn build_dynamic(self) -> DynamicNotification {
113        DynamicNotification {
114            method: self.method,
115            params: self.params,
116            priority: self.priority,
117            can_batch: self.can_batch,
118            max_retries: self.max_retries,
119        }
120    }
121}
122
123/// Dynamic notification created by NotificationBuilder
124#[derive(Debug)]
125pub struct DynamicNotification {
126    method: String,
127    #[allow(dead_code)]
128    params: Option<NotificationParams>,
129    priority: u32,
130    can_batch: bool,
131    max_retries: u32,
132}
133
134// Implement all fine-grained traits for DynamicNotification
135impl HasNotificationMetadata for DynamicNotification {
136    fn method(&self) -> &str {
137        &self.method
138    }
139
140    fn requires_ack(&self) -> bool {
141        // High priority notifications might require acknowledgment
142        self.priority >= 5
143    }
144}
145
146impl HasNotificationPayload for DynamicNotification {
147    fn payload(&self) -> Option<Value> {
148        // Convert params to a single Value if needed
149        None // For simplicity, custom payloads can be added via trait extension
150    }
151}
152
153impl HasNotificationRules for DynamicNotification {
154    fn priority(&self) -> u32 {
155        self.priority
156    }
157
158    fn can_batch(&self) -> bool {
159        self.can_batch
160    }
161
162    fn max_retries(&self) -> u32 {
163        self.max_retries
164    }
165}
166
167// NotificationDefinition is automatically implemented via blanket impl!
168
169/// Builder for progress notifications
170pub struct ProgressNotificationBuilder {
171    progress_token: String,
172    progress: u64,
173    total: Option<u64>,
174    message: Option<String>,
175    meta: Option<HashMap<String, Value>>,
176}
177
178impl ProgressNotificationBuilder {
179    pub fn new(progress_token: impl Into<String>, progress: u64) -> Self {
180        Self {
181            progress_token: progress_token.into(),
182            progress,
183            total: None,
184            message: None,
185            meta: None,
186        }
187    }
188
189    /// Set total work amount
190    pub fn total(mut self, total: u64) -> Self {
191        self.total = Some(total);
192        self
193    }
194
195    /// Set progress message
196    pub fn message(mut self, message: impl Into<String>) -> Self {
197        self.message = Some(message.into());
198        self
199    }
200
201    /// Set meta information
202    pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
203        self.meta = Some(meta);
204        self
205    }
206
207    /// Add a meta key-value pair
208    pub fn meta_value(mut self, key: impl Into<String>, value: Value) -> Self {
209        if self.meta.is_none() {
210            self.meta = Some(HashMap::new());
211        }
212        self.meta.as_mut().unwrap().insert(key.into(), value);
213        self
214    }
215
216    /// Build the progress notification
217    pub fn build(self) -> ProgressNotification {
218        let mut notification = ProgressNotification::new(self.progress_token, self.progress);
219        if let Some(total) = self.total {
220            notification = notification.with_total(total);
221        }
222        if let Some(message) = self.message {
223            notification = notification.with_message(message);
224        }
225        if let Some(meta) = self.meta {
226            notification = notification.with_meta(meta);
227        }
228        notification
229    }
230}
231
232/// Builder for resource updated notifications
233pub struct ResourceUpdatedNotificationBuilder {
234    uri: String,
235    meta: Option<HashMap<String, Value>>,
236}
237
238impl ResourceUpdatedNotificationBuilder {
239    pub fn new(uri: impl Into<String>) -> Self {
240        Self {
241            uri: uri.into(),
242            meta: None,
243        }
244    }
245
246    /// Set meta information
247    pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
248        self.meta = Some(meta);
249        self
250    }
251
252    /// Add a meta key-value pair
253    pub fn meta_value(mut self, key: impl Into<String>, value: Value) -> Self {
254        if self.meta.is_none() {
255            self.meta = Some(HashMap::new());
256        }
257        self.meta.as_mut().unwrap().insert(key.into(), value);
258        self
259    }
260
261    /// Build the resource updated notification
262    pub fn build(self) -> ResourceUpdatedNotification {
263        let mut notification = ResourceUpdatedNotification::new(self.uri);
264        if let Some(meta) = self.meta {
265            notification = notification.with_meta(meta);
266        }
267        notification
268    }
269}
270
271/// Builder for cancelled notifications
272pub struct CancelledNotificationBuilder {
273    request_id: RequestId,
274    reason: Option<String>,
275    meta: Option<HashMap<String, Value>>,
276}
277
278impl CancelledNotificationBuilder {
279    pub fn new(request_id: RequestId) -> Self {
280        Self {
281            request_id,
282            reason: None,
283            meta: None,
284        }
285    }
286
287    /// Set cancellation reason
288    pub fn reason(mut self, reason: impl Into<String>) -> Self {
289        self.reason = Some(reason.into());
290        self
291    }
292
293    /// Set meta information
294    pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
295        self.meta = Some(meta);
296        self
297    }
298
299    /// Add a meta key-value pair
300    pub fn meta_value(mut self, key: impl Into<String>, value: Value) -> Self {
301        if self.meta.is_none() {
302            self.meta = Some(HashMap::new());
303        }
304        self.meta.as_mut().unwrap().insert(key.into(), value);
305        self
306    }
307
308    /// Build the cancelled notification
309    pub fn build(self) -> CancelledNotification {
310        let mut notification = CancelledNotification::new(self.request_id);
311        if let Some(reason) = self.reason {
312            notification = notification.with_reason(reason);
313        }
314        if let Some(meta) = self.meta {
315            notification = notification.with_meta(meta);
316        }
317        notification
318    }
319}
320
321/// Convenience methods for common notification patterns
322impl NotificationBuilder {
323    /// Create a resource list changed notification
324    pub fn resource_list_changed() -> ResourceListChangedNotification {
325        ResourceListChangedNotification::new()
326    }
327
328    /// Create a tool list changed notification
329    pub fn tool_list_changed() -> ToolListChangedNotification {
330        ToolListChangedNotification::new()
331    }
332
333    /// Create a prompt list changed notification
334    pub fn prompt_list_changed() -> PromptListChangedNotification {
335        PromptListChangedNotification::new()
336    }
337
338    /// Create a roots list changed notification
339    pub fn roots_list_changed() -> RootsListChangedNotification {
340        RootsListChangedNotification::new()
341    }
342
343    /// Create an initialized notification
344    pub fn initialized() -> InitializedNotification {
345        InitializedNotification::new()
346    }
347
348    /// Create a progress notification builder
349    pub fn progress(
350        progress_token: impl Into<String>,
351        progress: u64,
352    ) -> ProgressNotificationBuilder {
353        ProgressNotificationBuilder::new(progress_token, progress)
354    }
355
356    /// Create a resource updated notification builder
357    pub fn resource_updated(uri: impl Into<String>) -> ResourceUpdatedNotificationBuilder {
358        ResourceUpdatedNotificationBuilder::new(uri)
359    }
360
361    /// Create a cancelled notification builder
362    pub fn cancelled(request_id: RequestId) -> CancelledNotificationBuilder {
363        CancelledNotificationBuilder::new(request_id)
364    }
365
366    /// Create a logging message notification builder
367    pub fn logging_message(level: LoggingLevel, data: Value) -> LoggingMessageNotification {
368        LoggingMessageNotification::new(level, data)
369    }
370
371    /// Create a custom notification
372    pub fn custom(method: impl Into<String>) -> Self {
373        Self::new(method)
374    }
375
376    /// Create a server-to-client notification
377    pub fn server_notification(method: impl Into<String>) -> Self {
378        let method = method.into();
379        // Ensure it follows MCP notification method pattern
380        if !method.starts_with("notifications/") {
381            Self::new(format!("notifications/{}", method))
382        } else {
383            Self::new(method)
384        }
385    }
386}
387
388/// Collection of common notification methods as constants
389pub mod methods {
390    pub const RESOURCE_LIST_CHANGED: &str = "notifications/resources/listChanged";
391    pub const TOOL_LIST_CHANGED: &str = "notifications/tools/listChanged";
392    pub const PROMPT_LIST_CHANGED: &str = "notifications/prompts/listChanged";
393    pub const ROOTS_LIST_CHANGED: &str = "notifications/roots/listChanged";
394    pub const PROGRESS: &str = "notifications/progress";
395    pub const RESOURCE_UPDATED: &str = "notifications/resources/updated";
396    pub const CANCELLED: &str = "notifications/cancelled";
397    pub const INITIALIZED: &str = "notifications/initialized";
398    pub const MESSAGE: &str = "notifications/message";
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404    use serde_json::json;
405    use crate::traits::NotificationDefinition;
406
407    #[test]
408    fn test_notification_builder_basic() {
409        let notification = NotificationBuilder::new("notifications/test")
410            .param("key1", json!("value1"))
411            .param("key2", json!(42))
412            .priority(3)
413            .can_batch(false)
414            .build();
415
416        assert_eq!(notification.method, "notifications/test");
417        assert!(notification.params.is_some());
418
419        let params = notification.params.unwrap();
420        assert_eq!(params.other.get("key1"), Some(&json!("value1")));
421        assert_eq!(params.other.get("key2"), Some(&json!(42)));
422    }
423
424    #[test]
425    fn test_notification_builder_meta() {
426        let mut meta = HashMap::new();
427        meta.insert("source".to_string(), json!("test"));
428        meta.insert("timestamp".to_string(), json!("2025-01-01T00:00:00Z"));
429
430        let notification = NotificationBuilder::new("notifications/test")
431            .meta(meta.clone())
432            .build();
433
434        let params = notification.params.expect("Expected params");
435        assert_eq!(params.meta, Some(meta));
436    }
437
438    #[test]
439    fn test_notification_builder_fluent_meta() {
440        let notification = NotificationBuilder::new("notifications/test")
441            .meta_value("request_id", json!("req-123"))
442            .meta_value("user_id", json!("user-456"))
443            .build();
444
445        let params = notification.params.expect("Expected params");
446        let meta = params.meta.expect("Expected meta");
447        assert_eq!(meta.get("request_id"), Some(&json!("req-123")));
448        assert_eq!(meta.get("user_id"), Some(&json!("user-456")));
449    }
450
451    #[test]
452    fn test_progress_notification_builder() {
453        let notification = ProgressNotificationBuilder::new("token-123", 75)
454            .total(100)
455            .message("Processing files...")
456            .meta_value("stage", json!("validation"))
457            .build();
458
459        assert_eq!(notification.method, "notifications/progress");
460        assert_eq!(notification.params.progress_token, "token-123");
461        assert_eq!(notification.params.progress, 75);
462        assert_eq!(notification.params.total, Some(100));
463        assert_eq!(
464            notification.params.message,
465            Some("Processing files...".to_string())
466        );
467
468        let meta = notification.params.meta.expect("Expected meta");
469        assert_eq!(meta.get("stage"), Some(&json!("validation")));
470    }
471
472    #[test]
473    fn test_resource_updated_notification_builder() {
474        let notification = ResourceUpdatedNotificationBuilder::new("file:///test.txt")
475            .meta_value("change_type", json!("modified"))
476            .build();
477
478        assert_eq!(notification.method, "notifications/resources/updated");
479        assert_eq!(notification.params.uri, "file:///test.txt");
480
481        let meta = notification.params.meta.expect("Expected meta");
482        assert_eq!(meta.get("change_type"), Some(&json!("modified")));
483    }
484
485    #[test]
486    fn test_cancelled_notification_builder() {
487        let notification = CancelledNotificationBuilder::new(RequestId::Number(123))
488            .reason("User cancelled operation")
489            .meta_value("cancellation_time", json!("2025-01-01T00:00:00Z"))
490            .build();
491
492        assert_eq!(notification.method, "notifications/cancelled");
493        assert_eq!(notification.params.request_id, RequestId::Number(123));
494        assert_eq!(
495            notification.params.reason,
496            Some("User cancelled operation".to_string())
497        );
498
499        let meta = notification.params.meta.expect("Expected meta");
500        assert_eq!(
501            meta.get("cancellation_time"),
502            Some(&json!("2025-01-01T00:00:00Z"))
503        );
504    }
505
506    #[test]
507    fn test_convenience_methods() {
508        // Test standard list changed notifications
509        let resource_list = NotificationBuilder::resource_list_changed();
510        assert_eq!(resource_list.method, "notifications/resources/listChanged");
511
512        let tool_list = NotificationBuilder::tool_list_changed();
513        assert_eq!(tool_list.method, "notifications/tools/listChanged");
514
515        let prompt_list = NotificationBuilder::prompt_list_changed();
516        assert_eq!(prompt_list.method, "notifications/prompts/listChanged");
517
518        let roots_list = NotificationBuilder::roots_list_changed();
519        assert_eq!(roots_list.method, "notifications/roots/listChanged");
520
521        let initialized = NotificationBuilder::initialized();
522        assert_eq!(initialized.method, "notifications/initialized");
523
524        // Test logging message
525        let logging = NotificationBuilder::logging_message(
526            LoggingLevel::Info,
527            json!({"message": "Test log"}),
528        );
529        assert_eq!(logging.method, "notifications/message");
530    }
531
532    #[test]
533    fn test_custom_notifications() {
534        // Custom notification
535        let custom = NotificationBuilder::custom("custom/event")
536            .param("event_type", json!("user_action"))
537            .build();
538        assert_eq!(custom.method, "custom/event");
539
540        // Server notification (auto-prefixes)
541        let server = NotificationBuilder::server_notification("server/status")
542            .param("status", json!("ready"))
543            .build();
544        assert_eq!(server.method, "notifications/server/status");
545
546        // Already prefixed - should not double-prefix
547        let already_prefixed =
548            NotificationBuilder::server_notification("notifications/already/prefixed").build();
549        assert_eq!(already_prefixed.method, "notifications/already/prefixed");
550    }
551
552    #[test]
553    fn test_dynamic_notification_traits() {
554        let notification = NotificationBuilder::new("notifications/test")
555            .priority(7)
556            .can_batch(false)
557            .max_retries(5)
558            .build_dynamic();
559
560        // Test HasNotificationMetadata
561        assert_eq!(notification.method(), "notifications/test");
562        assert!(notification.requires_ack()); // Priority >= 5
563
564        // Test HasNotificationRules
565        assert_eq!(notification.priority(), 7);
566        assert!(!notification.can_batch());
567        assert_eq!(notification.max_retries(), 5);
568
569        // Test NotificationDefinition (auto-implemented)
570        assert!(notification.validate().is_ok());
571        let base_notification = notification.to_notification();
572        assert_eq!(base_notification.method, "notifications/test");
573    }
574
575    #[test]
576    fn test_notification_validation() {
577        let valid = NotificationBuilder::new("notifications/valid").build_dynamic();
578        assert!(valid.validate().is_ok());
579
580        let invalid_empty = NotificationBuilder::new("").build_dynamic();
581        assert!(invalid_empty.validate().is_err());
582
583        let invalid_prefix = NotificationBuilder::new("invalid/method").build_dynamic();
584        assert!(invalid_prefix.validate().is_err());
585    }
586
587    #[test]
588    fn test_method_constants() {
589        use super::methods::*;
590
591        assert_eq!(RESOURCE_LIST_CHANGED, "notifications/resources/listChanged");
592        assert_eq!(TOOL_LIST_CHANGED, "notifications/tools/listChanged");
593        assert_eq!(PROMPT_LIST_CHANGED, "notifications/prompts/listChanged");
594        assert_eq!(ROOTS_LIST_CHANGED, "notifications/roots/listChanged");
595        assert_eq!(PROGRESS, "notifications/progress");
596        assert_eq!(RESOURCE_UPDATED, "notifications/resources/updated");
597        assert_eq!(CANCELLED, "notifications/cancelled");
598        assert_eq!(INITIALIZED, "notifications/initialized");
599        assert_eq!(MESSAGE, "notifications/message");
600    }
601}