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