Skip to main content

fastmcp_console/
detection.rs

1//! Agent/human context detection
2//!
3//! Determines whether rich output should be enabled based on the execution context.
4
5/// Display context representing the environment
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum DisplayContext {
8    /// Agent context - plain output for machine parsing
9    Agent,
10    /// Human context - rich styled output
11    #[default]
12    Human,
13}
14
15impl DisplayContext {
16    /// Create an agent (plain output) context
17    #[must_use]
18    pub fn new_agent() -> Self {
19        Self::Agent
20    }
21
22    /// Create a human (rich output) context
23    #[must_use]
24    pub fn new_human() -> Self {
25        Self::Human
26    }
27
28    /// Auto-detect the display context from environment
29    #[must_use]
30    pub fn detect() -> Self {
31        if should_enable_rich() {
32            Self::Human
33        } else {
34            Self::Agent
35        }
36    }
37
38    /// Check if this is a human context (rich output enabled)
39    #[must_use]
40    pub fn is_human(&self) -> bool {
41        matches!(self, Self::Human)
42    }
43
44    /// Check if this is an agent context (plain output)
45    #[must_use]
46    pub fn is_agent(&self) -> bool {
47        matches!(self, Self::Agent)
48    }
49}
50
51/// Determine if we're running in an agent context
52#[must_use]
53pub fn is_agent_context() -> bool {
54    // MCP clients set these when spawning servers
55    std::env::var("MCP_CLIENT").is_ok()
56        || std::env::var("CLAUDE_CODE").is_ok()
57        || std::env::var("CODEX_CLI").is_ok()
58        || std::env::var("CURSOR_SESSION").is_ok()
59        // Generic agent indicators
60        || std::env::var("CI").is_ok()
61        || std::env::var("AGENT_MODE").is_ok()
62        // Explicit rich disable
63        || std::env::var("FASTMCP_PLAIN").is_ok()
64        || std::env::var("NO_COLOR").is_ok()
65}
66
67/// Determine if rich output should be enabled
68#[must_use]
69pub fn should_enable_rich() -> bool {
70    use std::io::IsTerminal;
71
72    // Explicit plain mode and the standard NO_COLOR contract take precedence
73    // over color-enabling knobs. This matches ConsoleConfig::resolve_context.
74    if std::env::var("FASTMCP_PLAIN").is_ok() || std::env::var("NO_COLOR").is_ok() {
75        return false;
76    }
77
78    // Support both the legacy rich flag and ConsoleConfig's documented flag.
79    if std::env::var("FASTMCP_RICH").is_ok() || std::env::var("FASTMCP_FORCE_COLOR").is_ok() {
80        return true;
81    }
82
83    // In agent context, disable rich by default
84    if is_agent_context() {
85        return false;
86    }
87
88    // Human context only when stderr is an interactive terminal.
89    std::io::stderr().is_terminal()
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_display_context_new_agent() {
98        let ctx = DisplayContext::new_agent();
99        assert!(ctx.is_agent());
100        assert!(!ctx.is_human());
101    }
102
103    #[test]
104    fn test_display_context_new_human() {
105        let ctx = DisplayContext::new_human();
106        assert!(ctx.is_human());
107        assert!(!ctx.is_agent());
108    }
109
110    #[test]
111    fn test_display_context_default_is_human() {
112        let ctx = DisplayContext::default();
113        assert!(ctx.is_human());
114    }
115
116    #[test]
117    fn test_display_context_equality() {
118        assert_eq!(DisplayContext::Agent, DisplayContext::Agent);
119        assert_eq!(DisplayContext::Human, DisplayContext::Human);
120        assert_ne!(DisplayContext::Agent, DisplayContext::Human);
121    }
122
123    #[test]
124    fn test_display_context_clone() {
125        let ctx = DisplayContext::Agent;
126        let cloned = ctx;
127        assert_eq!(ctx, cloned);
128    }
129
130    #[test]
131    fn test_display_context_debug() {
132        let ctx = DisplayContext::Agent;
133        let debug_str = format!("{:?}", ctx);
134        assert!(debug_str.contains("Agent"));
135    }
136
137    // =========================================================================
138    // Additional coverage tests (bd-1p24)
139    // =========================================================================
140
141    #[test]
142    fn display_context_copy_semantics() {
143        let ctx = DisplayContext::Agent;
144        let copied = ctx;
145        // Both should be usable (Copy trait)
146        assert!(ctx.is_agent());
147        assert!(copied.is_agent());
148    }
149
150    #[test]
151    fn display_context_debug_human() {
152        let ctx = DisplayContext::Human;
153        let debug_str = format!("{ctx:?}");
154        assert!(debug_str.contains("Human"));
155    }
156
157    #[test]
158    fn detect_returns_valid_context() {
159        // In CI, detect() should return Agent (CI env var is set)
160        let ctx = DisplayContext::detect();
161        assert!(ctx.is_agent() || ctx.is_human());
162    }
163
164    #[test]
165    fn is_agent_context_and_should_enable_rich_are_consistent() {
166        // If is_agent_context() returns true, should_enable_rich() should return
167        // false unless an explicit color flag is present.
168        if is_agent_context() {
169            let force_plain =
170                std::env::var("FASTMCP_PLAIN").is_ok() || std::env::var("NO_COLOR").is_ok();
171            let force_rich = std::env::var("FASTMCP_RICH").is_ok()
172                || std::env::var("FASTMCP_FORCE_COLOR").is_ok();
173            if force_plain || !force_rich {
174                assert!(!should_enable_rich());
175            }
176        }
177    }
178}