mcp_probe_core/messages/
logging.rs

1//! Logging-related message types for MCP server-to-client logging and progress.
2//!
3//! This module provides types for:
4//! - Server logging messages to client
5//! - Log level configuration
6//! - Progress notifications for long-running operations
7//! - Resource change notifications
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::HashMap;
12
13/// Log level enumeration for MCP logging.
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum LogLevel {
17    /// Debug level logging (most verbose)
18    Debug,
19    /// Info level logging
20    Info,
21    /// Notice level logging
22    Notice,
23    /// Warning level logging
24    Warning,
25    /// Error level logging
26    Error,
27    /// Critical level logging (least verbose)
28    Critical,
29}
30
31impl LogLevel {
32    /// Get all log levels in order from most to least verbose.
33    pub fn all() -> Vec<Self> {
34        vec![
35            Self::Debug,
36            Self::Info,
37            Self::Notice,
38            Self::Warning,
39            Self::Error,
40            Self::Critical,
41        ]
42    }
43
44    /// Check if this log level is more verbose than another.
45    pub fn is_more_verbose_than(&self, other: &Self) -> bool {
46        self < other
47    }
48
49    /// Check if this log level is less verbose than another.
50    pub fn is_less_verbose_than(&self, other: &Self) -> bool {
51        self > other
52    }
53}
54
55impl std::fmt::Display for LogLevel {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        let s = match self {
58            Self::Debug => "debug",
59            Self::Info => "info",
60            Self::Notice => "notice",
61            Self::Warning => "warning",
62            Self::Error => "error",
63            Self::Critical => "critical",
64        };
65        write!(f, "{}", s)
66    }
67}
68
69/// Request to set the logging level for the server.
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct SetLevelRequest {
72    /// The logging level to set
73    pub level: LogLevel,
74}
75
76impl SetLevelRequest {
77    /// Create a new set level request.
78    pub fn new(level: LogLevel) -> Self {
79        Self { level }
80    }
81}
82
83/// Notification containing a log message from the server.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct LoggingNotification {
86    /// The log level
87    pub level: LogLevel,
88
89    /// The log message
90    pub data: Value,
91
92    /// Optional logger name
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub logger: Option<String>,
95}
96
97impl LoggingNotification {
98    /// Create a new logging notification.
99    pub fn new(level: LogLevel, data: Value) -> Self {
100        Self {
101            level,
102            data,
103            logger: None,
104        }
105    }
106
107    /// Create a logging notification with a logger name.
108    pub fn with_logger(level: LogLevel, data: Value, logger: impl Into<String>) -> Self {
109        Self {
110            level,
111            data,
112            logger: Some(logger.into()),
113        }
114    }
115
116    /// Create a debug log notification.
117    pub fn debug(message: impl Into<String>) -> Self {
118        Self::new(LogLevel::Debug, Value::String(message.into()))
119    }
120
121    /// Create an info log notification.
122    pub fn info(message: impl Into<String>) -> Self {
123        Self::new(LogLevel::Info, Value::String(message.into()))
124    }
125
126    /// Create a notice log notification.
127    pub fn notice(message: impl Into<String>) -> Self {
128        Self::new(LogLevel::Notice, Value::String(message.into()))
129    }
130
131    /// Create a warning log notification.
132    pub fn warning(message: impl Into<String>) -> Self {
133        Self::new(LogLevel::Warning, Value::String(message.into()))
134    }
135
136    /// Create an error log notification.
137    pub fn error(message: impl Into<String>) -> Self {
138        Self::new(LogLevel::Error, Value::String(message.into()))
139    }
140
141    /// Create a critical log notification.
142    pub fn critical(message: impl Into<String>) -> Self {
143        Self::new(LogLevel::Critical, Value::String(message.into()))
144    }
145}
146
147/// Progress notification for long-running operations.
148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
149pub struct ProgressNotification {
150    /// Progress token for this operation
151    pub progress_token: ProgressToken,
152
153    /// Current progress (0.0 to 1.0)
154    pub progress: f64,
155
156    /// Total number of items (if known)
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub total: Option<u64>,
159}
160
161impl ProgressNotification {
162    /// Create a new progress notification.
163    pub fn new(progress_token: impl Into<ProgressToken>, progress: f64) -> Self {
164        Self {
165            progress_token: progress_token.into(),
166            progress,
167            total: None,
168        }
169    }
170
171    /// Create a progress notification with a total count.
172    pub fn with_total(progress_token: impl Into<ProgressToken>, progress: f64, total: u64) -> Self {
173        Self {
174            progress_token: progress_token.into(),
175            progress,
176            total: Some(total),
177        }
178    }
179}
180
181/// Progress token for tracking long-running operations.
182#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
183#[serde(untagged)]
184pub enum ProgressToken {
185    /// String-based progress token
186    String(String),
187    /// Numeric progress token
188    Number(i64),
189}
190
191impl From<String> for ProgressToken {
192    fn from(s: String) -> Self {
193        Self::String(s)
194    }
195}
196
197impl From<&str> for ProgressToken {
198    fn from(s: &str) -> Self {
199        Self::String(s.to_string())
200    }
201}
202
203impl From<i64> for ProgressToken {
204    fn from(n: i64) -> Self {
205        Self::Number(n)
206    }
207}
208
209impl From<i32> for ProgressToken {
210    fn from(n: i32) -> Self {
211        Self::Number(n as i64)
212    }
213}
214
215impl std::fmt::Display for ProgressToken {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        match self {
218            Self::String(s) => write!(f, "{}", s),
219            Self::Number(n) => write!(f, "{}", n),
220        }
221    }
222}
223
224/// Notification that a resource has been updated.
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226pub struct ResourceUpdatedNotification {
227    /// URI of the updated resource
228    pub uri: String,
229
230    /// Additional metadata about the update
231    #[serde(flatten)]
232    pub metadata: HashMap<String, Value>,
233}
234
235impl ResourceUpdatedNotification {
236    /// Create a new resource updated notification.
237    pub fn new(uri: impl Into<String>) -> Self {
238        Self {
239            uri: uri.into(),
240            metadata: HashMap::new(),
241        }
242    }
243
244    /// Add metadata to the notification.
245    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
246        self.metadata.insert(key.into(), value);
247        self
248    }
249}
250
251/// Notification that the list of resources has changed.
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
253pub struct ResourceListChangedNotification {
254    /// Additional metadata about the change
255    #[serde(flatten)]
256    pub metadata: HashMap<String, Value>,
257}
258
259impl ResourceListChangedNotification {
260    /// Create a new resource list changed notification.
261    pub fn new() -> Self {
262        Self::default()
263    }
264
265    /// Add metadata to the notification.
266    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
267        self.metadata.insert(key.into(), value);
268        self
269    }
270}
271
272/// Notification that the list of tools has changed.
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
274pub struct ToolListChangedNotification {
275    /// Additional metadata about the change
276    #[serde(flatten)]
277    pub metadata: HashMap<String, Value>,
278}
279
280impl ToolListChangedNotification {
281    /// Create a new tool list changed notification.
282    pub fn new() -> Self {
283        Self::default()
284    }
285
286    /// Add metadata to the notification.
287    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
288        self.metadata.insert(key.into(), value);
289        self
290    }
291}
292
293/// Notification that the list of prompts has changed.
294#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
295pub struct PromptListChangedNotification {
296    /// Additional metadata about the change
297    #[serde(flatten)]
298    pub metadata: HashMap<String, Value>,
299}
300
301impl PromptListChangedNotification {
302    /// Create a new prompt list changed notification.
303    pub fn new() -> Self {
304        Self::default()
305    }
306
307    /// Add metadata to the notification.
308    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
309        self.metadata.insert(key.into(), value);
310        self
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use serde_json::json;
318
319    #[test]
320    fn test_log_level_ordering() {
321        assert!(LogLevel::Debug < LogLevel::Info);
322        assert!(LogLevel::Info < LogLevel::Notice);
323        assert!(LogLevel::Notice < LogLevel::Warning);
324        assert!(LogLevel::Warning < LogLevel::Error);
325        assert!(LogLevel::Error < LogLevel::Critical);
326
327        assert!(LogLevel::Debug.is_more_verbose_than(&LogLevel::Error));
328        assert!(LogLevel::Error.is_less_verbose_than(&LogLevel::Debug));
329    }
330
331    #[test]
332    fn test_log_level_serialization() {
333        let levels = LogLevel::all();
334        let expected = ["debug", "info", "notice", "warning", "error", "critical"];
335
336        for (level, expected) in levels.iter().zip(expected.iter()) {
337            let json = serde_json::to_string(level).unwrap();
338            assert_eq!(json, format!("\"{}\"", expected));
339            assert_eq!(level.to_string(), *expected);
340        }
341    }
342
343    #[test]
344    fn test_set_level_request() {
345        let request = SetLevelRequest::new(LogLevel::Warning);
346        assert_eq!(request.level, LogLevel::Warning);
347
348        let json = serde_json::to_string(&request).unwrap();
349        let deserialized: SetLevelRequest = serde_json::from_str(&json).unwrap();
350        assert_eq!(request, deserialized);
351    }
352
353    #[test]
354    fn test_logging_notification() {
355        let notification = LoggingNotification::with_logger(
356            LogLevel::Info,
357            json!("This is a test message"),
358            "test_logger",
359        );
360
361        assert_eq!(notification.level, LogLevel::Info);
362        assert_eq!(notification.data, json!("This is a test message"));
363        assert_eq!(notification.logger, Some("test_logger".to_string()));
364    }
365
366    #[test]
367    fn test_logging_notification_helpers() {
368        let debug = LoggingNotification::debug("Debug message");
369        let info = LoggingNotification::info("Info message");
370        let warning = LoggingNotification::warning("Warning message");
371        let error = LoggingNotification::error("Error message");
372
373        assert_eq!(debug.level, LogLevel::Debug);
374        assert_eq!(info.level, LogLevel::Info);
375        assert_eq!(warning.level, LogLevel::Warning);
376        assert_eq!(error.level, LogLevel::Error);
377    }
378
379    #[test]
380    fn test_progress_notification() {
381        let progress = ProgressNotification::new("operation-1", 0.5);
382        assert_eq!(
383            progress.progress_token,
384            ProgressToken::String("operation-1".to_string())
385        );
386        assert_eq!(progress.progress, 0.5);
387        assert_eq!(progress.total, None);
388
389        let progress_with_total = ProgressNotification::with_total("operation-2", 0.75, 100);
390        assert_eq!(progress_with_total.total, Some(100));
391    }
392
393    #[test]
394    fn test_progress_token() {
395        let string_token = ProgressToken::from("test");
396        let number_token = ProgressToken::from(42i64);
397
398        assert_eq!(string_token.to_string(), "test");
399        assert_eq!(number_token.to_string(), "42");
400
401        // Test serialization
402        let json_string = serde_json::to_string(&string_token).unwrap();
403        let json_number = serde_json::to_string(&number_token).unwrap();
404
405        assert_eq!(json_string, "\"test\"");
406        assert_eq!(json_number, "42");
407    }
408
409    #[test]
410    fn test_notification_with_metadata() {
411        let notification = ResourceUpdatedNotification::new("file:///test.txt")
412            .with_metadata("timestamp", json!("2024-01-01T00:00:00Z"))
413            .with_metadata("size", json!(1024));
414
415        assert_eq!(notification.uri, "file:///test.txt");
416        assert_eq!(
417            notification.metadata.get("timestamp"),
418            Some(&json!("2024-01-01T00:00:00Z"))
419        );
420        assert_eq!(notification.metadata.get("size"), Some(&json!(1024)));
421    }
422}