Skip to main content

fastmcp_console/
config.rs

1//! Centralized configuration for FastMCP console output.
2//!
3//! `ConsoleConfig` provides a single point of configuration for all aspects
4//! of rich console output, supporting both programmatic and environment
5//! variable-based configuration.
6
7use crate::detection::DisplayContext;
8use std::env;
9
10/// Comprehensive configuration for FastMCP console output
11#[derive(Debug, Clone)]
12pub struct ConsoleConfig {
13    // Display mode
14    /// Override display context (None = auto-detect)
15    pub context: Option<DisplayContext>,
16    /// Force color output even in non-TTY
17    pub force_color: Option<bool>,
18    /// Force plain text mode (no styling)
19    pub force_plain: bool,
20
21    // Theme
22    /// Custom color overrides (theme accessed via crate::theme::theme())
23    pub custom_colors: Option<CustomColors>,
24
25    // Startup
26    /// Show startup banner
27    pub show_banner: bool,
28    /// Show capabilities list in banner
29    pub show_capabilities: bool,
30    /// Banner display style
31    pub banner_style: BannerStyle,
32
33    // Logging
34    /// Log level filter
35    pub log_level: Option<log::Level>,
36    /// Show timestamps in logs
37    pub log_timestamps: bool,
38    /// Show target module in logs
39    pub log_targets: bool,
40    /// Show file and line in logs
41    pub log_file_line: bool,
42
43    // Runtime
44    /// Show periodic stats
45    pub show_stats_periodic: bool,
46    /// Stats display interval in seconds
47    pub stats_interval_secs: u64,
48    /// Show request/response traffic
49    pub show_request_traffic: bool,
50    /// Traffic logging verbosity
51    pub traffic_verbosity: TrafficVerbosity,
52
53    // Errors
54    /// Show fix suggestions for errors
55    pub show_suggestions: bool,
56    /// Show error codes
57    pub show_error_codes: bool,
58    /// Show backtraces for errors
59    pub show_backtrace: bool,
60
61    // Output limits
62    /// Maximum rows in tables
63    pub max_table_rows: usize,
64    /// Maximum depth for JSON display
65    pub max_json_depth: usize,
66    /// Truncate long strings at this length
67    pub truncate_at: usize,
68}
69
70/// Style variants for the startup banner
71#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
72pub enum BannerStyle {
73    /// Full banner with logo, info panel, and capabilities
74    #[default]
75    Full,
76    /// Compact banner without logo
77    Compact,
78    /// Minimal one-line banner
79    Minimal,
80    /// No banner at all
81    None,
82}
83
84/// Verbosity levels for traffic logging
85#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
86pub enum TrafficVerbosity {
87    /// No traffic logging
88    #[default]
89    None,
90    /// Summary only (method name, timing)
91    Summary,
92    /// Include headers/metadata
93    Headers,
94    /// Full request/response bodies
95    Full,
96}
97
98/// Custom color overrides
99#[derive(Debug, Clone, Default)]
100pub struct CustomColors {
101    /// Primary brand color override
102    pub primary: Option<String>,
103    /// Secondary accent color override
104    pub secondary: Option<String>,
105    /// Success color override
106    pub success: Option<String>,
107    /// Warning color override
108    pub warning: Option<String>,
109    /// Error color override
110    pub error: Option<String>,
111}
112
113impl Default for ConsoleConfig {
114    fn default() -> Self {
115        Self {
116            context: None,
117            force_color: None,
118            force_plain: false,
119            custom_colors: None,
120            show_banner: true,
121            show_capabilities: true,
122            banner_style: BannerStyle::Full,
123            log_level: None,
124            log_timestamps: true,
125            log_targets: true,
126            log_file_line: false,
127            show_stats_periodic: false,
128            stats_interval_secs: 60,
129            show_request_traffic: false,
130            traffic_verbosity: TrafficVerbosity::None,
131            show_suggestions: true,
132            show_error_codes: true,
133            show_backtrace: false,
134            max_table_rows: 100,
135            max_json_depth: 5,
136            truncate_at: 200,
137        }
138    }
139}
140
141impl ConsoleConfig {
142    /// Create config with defaults
143    #[must_use]
144    pub fn new() -> Self {
145        Self::default()
146    }
147
148    /// Create config from environment variables
149    ///
150    /// # Environment Variables
151    ///
152    /// | Variable | Values | Description |
153    /// |----------|--------|-------------|
154    /// | `FASTMCP_FORCE_COLOR` | (set) | Force rich output |
155    /// | `FASTMCP_PLAIN` | (set) | Force plain output |
156    /// | `NO_COLOR` | (set) | Disable colors (standard) |
157    /// | `FASTMCP_BANNER` | full/compact/minimal/none | Banner style |
158    /// | `FASTMCP_LOG` | trace/debug/info/warn/error | Log level |
159    /// | `FASTMCP_LOG_TIMESTAMPS` | 0/1 | Show timestamps |
160    /// | `FASTMCP_TRAFFIC` | none/summary/headers/full | Traffic logging |
161    /// | `RUST_BACKTRACE` | 1/full | Show backtraces |
162    #[must_use]
163    pub fn from_env() -> Self {
164        let mut config = Self::default();
165
166        // Display mode
167        if env::var("FASTMCP_FORCE_COLOR").is_ok() {
168            config.force_color = Some(true);
169        }
170        if env::var("FASTMCP_PLAIN").is_ok() || env::var("NO_COLOR").is_ok() {
171            config.force_plain = true;
172        }
173
174        // Banner
175        if let Ok(val) = env::var("FASTMCP_BANNER") {
176            config.banner_style = match val.to_lowercase().as_str() {
177                "compact" => BannerStyle::Compact,
178                "minimal" => BannerStyle::Minimal,
179                "none" | "0" | "false" => BannerStyle::None,
180                // "full" and any other value default to Full
181                _ => BannerStyle::Full,
182            };
183            config.show_banner = !matches!(config.banner_style, BannerStyle::None);
184        }
185
186        // Logging
187        if let Ok(level) = env::var("FASTMCP_LOG") {
188            config.log_level = match level.to_lowercase().as_str() {
189                "trace" => Some(log::Level::Trace),
190                "debug" => Some(log::Level::Debug),
191                "info" => Some(log::Level::Info),
192                "warn" | "warning" => Some(log::Level::Warn),
193                "error" => Some(log::Level::Error),
194                _ => None,
195            };
196        }
197        if env::var("FASTMCP_LOG_TIMESTAMPS")
198            .map(|v| v == "0" || v.to_lowercase() == "false")
199            .unwrap_or(false)
200        {
201            config.log_timestamps = false;
202        }
203
204        // Traffic
205        if let Ok(val) = env::var("FASTMCP_TRAFFIC") {
206            config.traffic_verbosity = match val.to_lowercase().as_str() {
207                "summary" | "1" => TrafficVerbosity::Summary,
208                "headers" | "2" => TrafficVerbosity::Headers,
209                "full" | "3" => TrafficVerbosity::Full,
210                // "none", "0", and any other value default to None
211                _ => TrafficVerbosity::None,
212            };
213            config.show_request_traffic =
214                !matches!(config.traffic_verbosity, TrafficVerbosity::None);
215        }
216
217        // Errors
218        if env::var("RUST_BACKTRACE").is_ok() {
219            config.show_backtrace = true;
220        }
221
222        config
223    }
224
225    // ─────────────────────────────────────────────────
226    // Builder Methods
227    // ─────────────────────────────────────────────────
228
229    /// Force color output
230    #[must_use]
231    pub fn force_color(mut self, force: bool) -> Self {
232        self.force_color = Some(force);
233        self
234    }
235
236    /// Enable plain text mode (no styling)
237    #[must_use]
238    pub fn plain_mode(mut self) -> Self {
239        self.force_plain = true;
240        self
241    }
242
243    /// Set the banner style
244    #[must_use]
245    pub fn with_banner(mut self, style: BannerStyle) -> Self {
246        self.banner_style = style;
247        self.show_banner = !matches!(style, BannerStyle::None);
248        self
249    }
250
251    /// Disable the banner entirely
252    #[must_use]
253    pub fn without_banner(mut self) -> Self {
254        self.show_banner = false;
255        self.banner_style = BannerStyle::None;
256        self
257    }
258
259    /// Set the log level
260    #[must_use]
261    pub fn with_log_level(mut self, level: log::Level) -> Self {
262        self.log_level = Some(level);
263        self
264    }
265
266    /// Set traffic logging verbosity
267    #[must_use]
268    pub fn with_traffic(mut self, verbosity: TrafficVerbosity) -> Self {
269        self.traffic_verbosity = verbosity;
270        self.show_request_traffic = !matches!(verbosity, TrafficVerbosity::None);
271        self
272    }
273
274    /// Enable periodic stats display
275    #[must_use]
276    pub fn with_periodic_stats(mut self, interval_secs: u64) -> Self {
277        self.show_stats_periodic = true;
278        self.stats_interval_secs = interval_secs;
279        self
280    }
281
282    /// Disable fix suggestions for errors
283    #[must_use]
284    pub fn without_suggestions(mut self) -> Self {
285        self.show_suggestions = false;
286        self
287    }
288
289    /// Set custom colors
290    #[must_use]
291    pub fn with_custom_colors(mut self, colors: CustomColors) -> Self {
292        self.custom_colors = Some(colors);
293        self
294    }
295
296    /// Set display context explicitly
297    #[must_use]
298    pub fn with_context(mut self, context: DisplayContext) -> Self {
299        self.context = Some(context);
300        self
301    }
302
303    /// Set maximum table rows
304    #[must_use]
305    pub fn with_max_table_rows(mut self, max: usize) -> Self {
306        self.max_table_rows = max;
307        self
308    }
309
310    /// Set maximum JSON depth
311    #[must_use]
312    pub fn with_max_json_depth(mut self, max: usize) -> Self {
313        self.max_json_depth = max;
314        self
315    }
316
317    /// Set truncation length
318    #[must_use]
319    pub fn with_truncate_at(mut self, len: usize) -> Self {
320        self.truncate_at = len;
321        self
322    }
323
324    // ─────────────────────────────────────────────────
325    // Accessor Methods
326    // ─────────────────────────────────────────────────
327
328    /// Get the theme (uses global theme singleton)
329    #[must_use]
330    pub fn theme(&self) -> &'static crate::theme::FastMcpTheme {
331        crate::theme::theme()
332    }
333
334    // ─────────────────────────────────────────────────
335    // Resolution Methods
336    // ─────────────────────────────────────────────────
337
338    /// Resolve the display context based on config and environment
339    #[must_use]
340    pub fn resolve_context(&self) -> DisplayContext {
341        if self.force_plain {
342            return DisplayContext::new_agent();
343        }
344        if let Some(true) = self.force_color {
345            return DisplayContext::new_human();
346        }
347        self.context.unwrap_or_else(DisplayContext::detect)
348    }
349
350    /// Check if rich output should be used based on resolved context
351    #[must_use]
352    pub fn should_use_rich(&self) -> bool {
353        self.resolve_context().is_human()
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn test_default_config() {
363        let config = ConsoleConfig::new();
364        assert!(config.show_banner);
365        assert!(config.show_capabilities);
366        assert_eq!(config.banner_style, BannerStyle::Full);
367        assert!(config.log_timestamps);
368        assert!(!config.force_plain);
369        assert_eq!(config.max_table_rows, 100);
370    }
371
372    #[test]
373    fn test_builder_pattern() {
374        let config = ConsoleConfig::new()
375            .with_banner(BannerStyle::Compact)
376            .with_log_level(log::Level::Debug)
377            .with_traffic(TrafficVerbosity::Summary)
378            .with_periodic_stats(30);
379
380        assert_eq!(config.banner_style, BannerStyle::Compact);
381        assert_eq!(config.log_level, Some(log::Level::Debug));
382        assert_eq!(config.traffic_verbosity, TrafficVerbosity::Summary);
383        assert!(config.show_stats_periodic);
384        assert_eq!(config.stats_interval_secs, 30);
385    }
386
387    #[test]
388    fn test_plain_mode() {
389        let config = ConsoleConfig::new().plain_mode();
390        assert!(config.force_plain);
391        assert_eq!(config.resolve_context(), DisplayContext::Agent);
392    }
393
394    #[test]
395    fn test_force_color() {
396        let config = ConsoleConfig::new().force_color(true);
397        assert_eq!(config.force_color, Some(true));
398        assert_eq!(config.resolve_context(), DisplayContext::Human);
399    }
400
401    #[test]
402    fn test_without_banner() {
403        let config = ConsoleConfig::new().without_banner();
404        assert!(!config.show_banner);
405        assert_eq!(config.banner_style, BannerStyle::None);
406    }
407}