mcp_host/
logging.rs

1//! Logging infrastructure for MCP notifications
2//!
3//! Provides helpers for sending log messages via MCP `notifications/message`
4//! that are visible to the LLM. Supports RFC 5424 severity levels and optional
5//! rate limiting via throttle_machines.
6
7use std::str::FromStr;
8use std::sync::{Arc, Mutex};
9use tokio::sync::mpsc;
10
11use crate::server::middleware::RateLimiter;
12use crate::transport::traits::JsonRpcNotification;
13
14/// Log level for MCP notifications (RFC 5424 compatible)
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
16pub enum LogLevel {
17    /// System is unusable (0)
18    Emergency,
19    /// Action must be taken immediately (1)
20    Alert,
21    /// Critical conditions (2)
22    Critical,
23    /// Error conditions (3)
24    Error,
25    /// Warning conditions (4)
26    Warning,
27    /// Normal but significant events (5)
28    Notice,
29    /// Informational messages (6)
30    Info,
31    /// Detailed debugging information (7)
32    Debug,
33}
34
35impl LogLevel {
36    /// Get the string representation for JSON-RPC
37    pub fn as_str(&self) -> &'static str {
38        match self {
39            LogLevel::Emergency => "emergency",
40            LogLevel::Alert => "alert",
41            LogLevel::Critical => "critical",
42            LogLevel::Error => "error",
43            LogLevel::Warning => "warning",
44            LogLevel::Notice => "notice",
45            LogLevel::Info => "info",
46            LogLevel::Debug => "debug",
47        }
48    }
49}
50
51impl FromStr for LogLevel {
52    type Err = String;
53
54    /// Parse level from string (case-insensitive)
55    fn from_str(s: &str) -> Result<Self, Self::Err> {
56        match s.to_lowercase().as_str() {
57            "emergency" => Ok(Self::Emergency),
58            "alert" => Ok(Self::Alert),
59            "critical" => Ok(Self::Critical),
60            "error" => Ok(Self::Error),
61            "warning" => Ok(Self::Warning),
62            "notice" => Ok(Self::Notice),
63            "info" => Ok(Self::Info),
64            "debug" => Ok(Self::Debug),
65            _ => Err(format!("Invalid log level: {}", s)),
66        }
67    }
68}
69
70impl std::fmt::Display for LogLevel {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        write!(f, "{}", self.as_str())
73    }
74}
75
76/// Logger configuration with rate limiting and minimum level
77#[derive(Clone)]
78pub struct LoggerConfig {
79    /// Minimum log level to send (default: Info)
80    pub min_level: LogLevel,
81    /// Optional rate limiter (uses throttle_machines GCRA)
82    pub rate_limiter: Option<Arc<RateLimiter>>,
83}
84
85impl Default for LoggerConfig {
86    fn default() -> Self {
87        Self {
88            min_level: LogLevel::Info,
89            rate_limiter: None, // Disabled by default to avoid circular dependency
90        }
91    }
92}
93
94/// Logger for sending MCP notifications visible to the LLM
95///
96/// This logger sends `notifications/message` notifications that the LLM can see,
97/// allowing tools and resources to communicate status information.
98///
99/// # Example
100///
101/// ```rust,ignore
102/// let logger = McpLogger::new(notification_tx, "my-tool");
103/// logger.info("Tool initialized successfully");
104/// logger.warning("API rate limit approaching");
105/// logger.error("Failed to connect to database");
106/// ```
107#[derive(Clone)]
108pub struct McpLogger {
109    tx: mpsc::UnboundedSender<JsonRpcNotification>,
110    logger_name: String,
111    config: Arc<Mutex<LoggerConfig>>,
112}
113
114impl McpLogger {
115    /// Create a new MCP logger with default configuration
116    pub fn new(
117        tx: mpsc::UnboundedSender<JsonRpcNotification>,
118        logger_name: impl Into<String>,
119    ) -> Self {
120        Self::with_config(tx, logger_name, LoggerConfig::default())
121    }
122
123    /// Create a new MCP logger with custom configuration
124    pub fn with_config(
125        tx: mpsc::UnboundedSender<JsonRpcNotification>,
126        logger_name: impl Into<String>,
127        config: LoggerConfig,
128    ) -> Self {
129        Self {
130            tx,
131            logger_name: logger_name.into(),
132            config: Arc::new(Mutex::new(config)),
133        }
134    }
135
136    /// Send a log message at the specified level (returns true if sent)
137    pub fn log(&self, level: LogLevel, message: impl Into<String>) -> bool {
138        // Check filtering
139        {
140            let config = self.config.lock().unwrap();
141            if level > config.min_level {
142                return false;
143            }
144            if let Some(ref limiter) = config.rate_limiter
145                && limiter.check().is_err()
146            {
147                return false; // Rate limited
148            }
149        }
150
151        let notification = JsonRpcNotification::new(
152            "notifications/message",
153            Some(serde_json::json!({
154                "level": level.as_str(),
155                "logger": self.logger_name,
156                "data": message.into()
157            })),
158        );
159        let _ = self.tx.send(notification);
160        true
161    }
162
163    /// Set minimum log level
164    pub fn set_min_level(&self, level: LogLevel) {
165        self.config.lock().unwrap().min_level = level;
166    }
167
168    /// Send a custom notification (like notifications/tools/list_changed)
169    ///
170    /// This allows tools/resources to broadcast custom events to clients.
171    ///
172    /// # Example
173    /// ```rust,ignore
174    /// // Notify client that data has changed
175    /// ctx.logger.send_notification("notifications/data/updated", Some(serde_json::json!({
176    ///     "entity": "users",
177    ///     "count": 42
178    /// })));
179    ///
180    /// // Notify without params
181    /// ctx.logger.send_notification("notifications/cache/cleared", None);
182    /// ```
183    pub fn send_notification(
184        &self,
185        method: impl Into<String>,
186        params: Option<serde_json::Value>,
187    ) -> bool {
188        let notification = JsonRpcNotification::new(method, params);
189        self.tx.send(notification).is_ok()
190    }
191
192    /// Log an emergency message
193    pub fn emergency(&self, message: impl Into<String>) -> bool {
194        self.log(LogLevel::Emergency, message)
195    }
196
197    /// Log an alert message
198    pub fn alert(&self, message: impl Into<String>) -> bool {
199        self.log(LogLevel::Alert, message)
200    }
201
202    /// Log a critical message
203    pub fn critical(&self, message: impl Into<String>) -> bool {
204        self.log(LogLevel::Critical, message)
205    }
206
207    /// Log an error message
208    pub fn error(&self, message: impl Into<String>) -> bool {
209        self.log(LogLevel::Error, message)
210    }
211
212    /// Log a warning message
213    pub fn warning(&self, message: impl Into<String>) -> bool {
214        self.log(LogLevel::Warning, message)
215    }
216
217    /// Log a notice message
218    pub fn notice(&self, message: impl Into<String>) -> bool {
219        self.log(LogLevel::Notice, message)
220    }
221
222    /// Log an info message
223    pub fn info(&self, message: impl Into<String>) -> bool {
224        self.log(LogLevel::Info, message)
225    }
226
227    /// Log a debug message
228    pub fn debug(&self, message: impl Into<String>) -> bool {
229        self.log(LogLevel::Debug, message)
230    }
231
232    /// Create a child logger with a sub-name
233    ///
234    /// # Example
235    ///
236    /// ```rust,ignore
237    /// let parent = McpLogger::new(tx, "tool");
238    /// let child = parent.child("database");
239    /// // child's logger name is "tool.database"
240    /// ```
241    pub fn child(&self, name: impl Into<String>) -> Self {
242        Self {
243            tx: self.tx.clone(),
244            logger_name: format!("{}.{}", self.logger_name, name.into()),
245            config: Arc::clone(&self.config),
246        }
247    }
248}
249
250/// Send a single log notification without maintaining a logger instance
251///
252/// # Arguments
253///
254/// * `tx` - Notification channel
255/// * `level` - Log level
256/// * `logger` - Logger name
257/// * `message` - Log message
258pub fn send_log_notification(
259    tx: &mpsc::UnboundedSender<JsonRpcNotification>,
260    level: LogLevel,
261    logger: &str,
262    message: impl Into<String>,
263) {
264    let notification = JsonRpcNotification::new(
265        "notifications/message",
266        Some(serde_json::json!({
267            "level": level.as_str(),
268            "logger": logger,
269            "data": message.into()
270        })),
271    );
272    let _ = tx.send(notification);
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_log_level_as_str() {
281        assert_eq!(LogLevel::Debug.as_str(), "debug");
282        assert_eq!(LogLevel::Info.as_str(), "info");
283        assert_eq!(LogLevel::Warning.as_str(), "warning");
284        assert_eq!(LogLevel::Error.as_str(), "error");
285    }
286
287    #[test]
288    fn test_log_level_display() {
289        assert_eq!(format!("{}", LogLevel::Info), "info");
290    }
291
292    #[test]
293    fn test_logger_creation() {
294        let (tx, _rx) = mpsc::unbounded_channel();
295        let logger = McpLogger::new(tx, "test-logger");
296        assert_eq!(logger.logger_name, "test-logger");
297    }
298
299    #[test]
300    fn test_child_logger() {
301        let (tx, _rx) = mpsc::unbounded_channel();
302        let parent = McpLogger::new(tx, "parent");
303        let child = parent.child("child");
304        assert_eq!(child.logger_name, "parent.child");
305    }
306
307    #[test]
308    fn test_log_methods() {
309        let (tx, mut rx) = mpsc::unbounded_channel();
310        let config = LoggerConfig {
311            min_level: LogLevel::Debug,
312            rate_limiter: None,
313        };
314        let logger = McpLogger::with_config(tx, "test", config);
315
316        logger.debug("debug message");
317        logger.info("info message");
318        logger.warning("warning message");
319        logger.error("error message");
320
321        // Verify we received 4 notifications
322        let mut count = 0;
323        while rx.try_recv().is_ok() {
324            count += 1;
325        }
326        assert_eq!(count, 4);
327    }
328
329    #[test]
330    fn test_send_log_notification() {
331        let (tx, mut rx) = mpsc::unbounded_channel();
332        send_log_notification(&tx, LogLevel::Info, "test", "test message");
333
334        let notification = rx.try_recv().unwrap();
335        assert_eq!(notification.method, "notifications/message");
336    }
337}