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 enable always wins
73    if std::env::var("FASTMCP_RICH").is_ok() {
74        return true;
75    }
76
77    // In agent context, disable rich by default
78    if is_agent_context() {
79        return false;
80    }
81
82    // Human context only when stderr is an interactive terminal.
83    std::io::stderr().is_terminal()
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn test_display_context_new_agent() {
92        let ctx = DisplayContext::new_agent();
93        assert!(ctx.is_agent());
94        assert!(!ctx.is_human());
95    }
96
97    #[test]
98    fn test_display_context_new_human() {
99        let ctx = DisplayContext::new_human();
100        assert!(ctx.is_human());
101        assert!(!ctx.is_agent());
102    }
103
104    #[test]
105    fn test_display_context_default_is_human() {
106        let ctx = DisplayContext::default();
107        assert!(ctx.is_human());
108    }
109
110    #[test]
111    fn test_display_context_equality() {
112        assert_eq!(DisplayContext::Agent, DisplayContext::Agent);
113        assert_eq!(DisplayContext::Human, DisplayContext::Human);
114        assert_ne!(DisplayContext::Agent, DisplayContext::Human);
115    }
116
117    #[test]
118    fn test_display_context_clone() {
119        let ctx = DisplayContext::Agent;
120        let cloned = ctx;
121        assert_eq!(ctx, cloned);
122    }
123
124    #[test]
125    fn test_display_context_debug() {
126        let ctx = DisplayContext::Agent;
127        let debug_str = format!("{:?}", ctx);
128        assert!(debug_str.contains("Agent"));
129    }
130
131    // =========================================================================
132    // Additional coverage tests (bd-1p24)
133    // =========================================================================
134
135    #[test]
136    fn display_context_copy_semantics() {
137        let ctx = DisplayContext::Agent;
138        let copied = ctx;
139        // Both should be usable (Copy trait)
140        assert!(ctx.is_agent());
141        assert!(copied.is_agent());
142    }
143
144    #[test]
145    fn display_context_debug_human() {
146        let ctx = DisplayContext::Human;
147        let debug_str = format!("{ctx:?}");
148        assert!(debug_str.contains("Human"));
149    }
150
151    #[test]
152    fn detect_returns_valid_context() {
153        // In CI, detect() should return Agent (CI env var is set)
154        let ctx = DisplayContext::detect();
155        assert!(ctx.is_agent() || ctx.is_human());
156    }
157
158    #[test]
159    fn is_agent_context_and_should_enable_rich_are_consistent() {
160        // If is_agent_context() returns true, should_enable_rich() should return
161        // false (unless FASTMCP_RICH is set, which it shouldn't be in tests)
162        if is_agent_context() {
163            // agent context -> rich should be disabled (unless FASTMCP_RICH override)
164            if std::env::var("FASTMCP_RICH").is_err() {
165                assert!(!should_enable_rich());
166            }
167        }
168    }
169}