vtcode_config/
mcp.rs

1use regex::Regex;
2use serde::{Deserialize, Serialize};
3use std::collections::{BTreeMap, HashMap};
4
5/// Top-level MCP configuration
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
7#[derive(Debug, Clone, Deserialize, Serialize)]
8pub struct McpClientConfig {
9    /// Enable MCP functionality
10    #[serde(default = "default_mcp_enabled")]
11    pub enabled: bool,
12
13    /// MCP UI display configuration
14    #[serde(default)]
15    pub ui: McpUiConfig,
16
17    /// Configured MCP providers
18    #[serde(default)]
19    pub providers: Vec<McpProviderConfig>,
20
21    /// MCP server configuration (for vtcode to expose tools)
22    #[serde(default)]
23    pub server: McpServerConfig,
24
25    /// Allow list configuration for MCP access control
26    #[serde(default)]
27    pub allowlist: McpAllowListConfig,
28
29    /// Maximum number of concurrent MCP connections
30    #[serde(default = "default_max_concurrent_connections")]
31    pub max_concurrent_connections: usize,
32
33    /// Request timeout in seconds
34    #[serde(default = "default_request_timeout_seconds")]
35    pub request_timeout_seconds: u64,
36
37    /// Connection retry attempts
38    #[serde(default = "default_retry_attempts")]
39    pub retry_attempts: u32,
40
41    /// Optional timeout (seconds) when starting providers
42    #[serde(default)]
43    pub startup_timeout_seconds: Option<u64>,
44
45    /// Optional timeout (seconds) for tool execution
46    #[serde(default)]
47    pub tool_timeout_seconds: Option<u64>,
48
49    /// Toggle experimental RMCP client features
50    #[serde(default = "default_experimental_use_rmcp_client")]
51    pub experimental_use_rmcp_client: bool,
52
53    /// Enable connection pooling for better performance
54    #[serde(default = "default_connection_pooling_enabled")]
55    pub connection_pooling_enabled: bool,
56
57    /// Cache capacity for tool discovery results
58    #[serde(default = "default_tool_cache_capacity")]
59    pub tool_cache_capacity: usize,
60
61    /// Connection timeout in seconds
62    #[serde(default = "default_connection_timeout_seconds")]
63    pub connection_timeout_seconds: u64,
64
65    /// Security configuration for MCP
66    #[serde(default)]
67    pub security: McpSecurityConfig,
68}
69
70/// Security configuration for MCP
71#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
72#[derive(Debug, Clone, Deserialize, Serialize)]
73pub struct McpSecurityConfig {
74    /// Enable authentication for MCP server
75    #[serde(default = "default_mcp_auth_enabled")]
76    pub auth_enabled: bool,
77
78    /// API key for MCP server authentication (environment variable name)
79    #[serde(default)]
80    pub api_key_env: Option<String>,
81
82    /// Rate limiting configuration
83    #[serde(default)]
84    pub rate_limit: McpRateLimitConfig,
85
86    /// Tool call validation configuration
87    #[serde(default)]
88    pub validation: McpValidationConfig,
89}
90
91impl Default for McpSecurityConfig {
92    fn default() -> Self {
93        Self {
94            auth_enabled: default_mcp_auth_enabled(),
95            api_key_env: None,
96            rate_limit: McpRateLimitConfig::default(),
97            validation: McpValidationConfig::default(),
98        }
99    }
100}
101
102/// Rate limiting configuration for MCP
103#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
104#[derive(Debug, Clone, Deserialize, Serialize)]
105pub struct McpRateLimitConfig {
106    /// Maximum requests per minute per client
107    #[serde(default = "default_requests_per_minute")]
108    pub requests_per_minute: u32,
109
110    /// Maximum concurrent requests per client
111    #[serde(default = "default_concurrent_requests")]
112    pub concurrent_requests: u32,
113}
114
115impl Default for McpRateLimitConfig {
116    fn default() -> Self {
117        Self {
118            requests_per_minute: default_requests_per_minute(),
119            concurrent_requests: default_concurrent_requests(),
120        }
121    }
122}
123
124/// Validation configuration for MCP
125#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
126#[derive(Debug, Clone, Deserialize, Serialize)]
127pub struct McpValidationConfig {
128    /// Enable JSON schema validation for tool arguments
129    #[serde(default = "default_schema_validation_enabled")]
130    pub schema_validation_enabled: bool,
131
132    /// Enable path traversal protection
133    #[serde(default = "default_path_traversal_protection_enabled")]
134    pub path_traversal_protection: bool,
135
136    /// Maximum argument size in bytes
137    #[serde(default = "default_max_argument_size")]
138    pub max_argument_size: u32,
139}
140
141impl Default for McpValidationConfig {
142    fn default() -> Self {
143        Self {
144            schema_validation_enabled: default_schema_validation_enabled(),
145            path_traversal_protection: default_path_traversal_protection_enabled(),
146            max_argument_size: default_max_argument_size(),
147        }
148    }
149}
150
151impl Default for McpClientConfig {
152    fn default() -> Self {
153        Self {
154            enabled: default_mcp_enabled(),
155            ui: McpUiConfig::default(),
156            providers: Vec::new(),
157            server: McpServerConfig::default(),
158            allowlist: McpAllowListConfig::default(),
159            max_concurrent_connections: default_max_concurrent_connections(),
160            request_timeout_seconds: default_request_timeout_seconds(),
161            retry_attempts: default_retry_attempts(),
162            startup_timeout_seconds: None,
163            tool_timeout_seconds: None,
164            experimental_use_rmcp_client: default_experimental_use_rmcp_client(),
165            security: McpSecurityConfig::default(),
166            connection_pooling_enabled: default_connection_pooling_enabled(),
167            connection_timeout_seconds: default_connection_timeout_seconds(),
168            tool_cache_capacity: default_tool_cache_capacity(),
169        }
170    }
171}
172
173/// UI configuration for MCP display
174#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
175#[derive(Debug, Clone, Deserialize, Serialize)]
176pub struct McpUiConfig {
177    /// UI mode for MCP events: "compact" or "full"
178    #[serde(default = "default_mcp_ui_mode")]
179    pub mode: McpUiMode,
180
181    /// Maximum number of MCP events to display
182    #[serde(default = "default_max_mcp_events")]
183    pub max_events: usize,
184
185    /// Show MCP provider names in UI
186    #[serde(default = "default_show_provider_names")]
187    pub show_provider_names: bool,
188
189    /// Custom renderer profiles for provider-specific output formatting
190    #[serde(default)]
191    pub renderers: HashMap<String, McpRendererProfile>,
192}
193
194impl Default for McpUiConfig {
195    fn default() -> Self {
196        Self {
197            mode: default_mcp_ui_mode(),
198            max_events: default_max_mcp_events(),
199            show_provider_names: default_show_provider_names(),
200            renderers: HashMap::new(),
201        }
202    }
203}
204
205impl McpUiConfig {
206    /// Resolve renderer profile for a provider or tool identifier
207    pub fn renderer_for_identifier(&self, identifier: &str) -> Option<McpRendererProfile> {
208        let normalized_identifier = normalize_mcp_identifier(identifier);
209        if normalized_identifier.is_empty() {
210            return None;
211        }
212
213        self.renderers.iter().find_map(|(key, profile)| {
214            let normalized_key = normalize_mcp_identifier(key);
215            if normalized_identifier.starts_with(&normalized_key) {
216                Some(*profile)
217            } else {
218                None
219            }
220        })
221    }
222
223    /// Resolve renderer profile for a fully qualified tool name
224    pub fn renderer_for_tool(&self, tool_name: &str) -> Option<McpRendererProfile> {
225        let identifier = tool_name.strip_prefix("mcp_").unwrap_or(tool_name);
226        self.renderer_for_identifier(identifier)
227    }
228}
229
230/// UI mode for MCP event display
231#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
233#[serde(rename_all = "snake_case")]
234#[derive(Default)]
235pub enum McpUiMode {
236    /// Compact mode - shows only event titles
237    #[default]
238    Compact,
239    /// Full mode - shows detailed event logs
240    Full,
241}
242
243impl std::fmt::Display for McpUiMode {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        match self {
246            McpUiMode::Compact => write!(f, "compact"),
247            McpUiMode::Full => write!(f, "full"),
248        }
249    }
250}
251
252/// Named renderer profiles for MCP tool output formatting
253#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
255#[serde(rename_all = "kebab-case")]
256pub enum McpRendererProfile {
257    /// Context7 knowledge base renderer
258    Context7,
259    /// Sequential thinking trace renderer
260    SequentialThinking,
261}
262
263/// Configuration for a single MCP provider
264#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
265#[derive(Debug, Clone, Deserialize, Serialize)]
266pub struct McpProviderConfig {
267    /// Provider name (used for identification)
268    pub name: String,
269
270    /// Transport configuration
271    #[serde(flatten)]
272    pub transport: McpTransportConfig,
273
274    /// Provider-specific environment variables
275    #[serde(default)]
276    pub env: HashMap<String, String>,
277
278    /// Whether this provider is enabled
279    #[serde(default = "default_provider_enabled")]
280    pub enabled: bool,
281
282    /// Maximum number of concurrent requests to this provider
283    #[serde(default = "default_provider_max_concurrent")]
284    pub max_concurrent_requests: usize,
285
286    /// Startup timeout in milliseconds for this provider
287    #[serde(default)]
288    pub startup_timeout_ms: Option<u64>,
289}
290
291impl Default for McpProviderConfig {
292    fn default() -> Self {
293        Self {
294            name: String::new(),
295            transport: McpTransportConfig::Stdio(McpStdioServerConfig::default()),
296            env: HashMap::new(),
297            enabled: default_provider_enabled(),
298            max_concurrent_requests: default_provider_max_concurrent(),
299            startup_timeout_ms: None,
300        }
301    }
302}
303
304/// Allow list configuration for MCP providers
305#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
306#[derive(Debug, Clone, Deserialize, Serialize)]
307pub struct McpAllowListConfig {
308    /// Whether to enforce allow list checks
309    #[serde(default = "default_allowlist_enforced")]
310    pub enforce: bool,
311
312    /// Default rules applied when provider-specific rules are absent
313    #[serde(default)]
314    pub default: McpAllowListRules,
315
316    /// Provider-specific allow list rules
317    #[serde(default)]
318    pub providers: BTreeMap<String, McpAllowListRules>,
319}
320
321impl Default for McpAllowListConfig {
322    fn default() -> Self {
323        Self {
324            enforce: default_allowlist_enforced(),
325            default: McpAllowListRules::default(),
326            providers: BTreeMap::new(),
327        }
328    }
329}
330
331impl McpAllowListConfig {
332    /// Determine whether a tool is permitted for the given provider
333    pub fn is_tool_allowed(&self, provider: &str, tool_name: &str) -> bool {
334        if !self.enforce {
335            return true;
336        }
337
338        self.resolve_match(provider, tool_name, |rules| &rules.tools)
339    }
340
341    /// Determine whether a resource is permitted for the given provider
342    pub fn is_resource_allowed(&self, provider: &str, resource: &str) -> bool {
343        if !self.enforce {
344            return true;
345        }
346
347        self.resolve_match(provider, resource, |rules| &rules.resources)
348    }
349
350    /// Determine whether a prompt is permitted for the given provider
351    pub fn is_prompt_allowed(&self, provider: &str, prompt: &str) -> bool {
352        if !self.enforce {
353            return true;
354        }
355
356        self.resolve_match(provider, prompt, |rules| &rules.prompts)
357    }
358
359    /// Determine whether a logging channel is permitted
360    pub fn is_logging_channel_allowed(&self, provider: Option<&str>, channel: &str) -> bool {
361        if !self.enforce {
362            return true;
363        }
364
365        if let Some(name) = provider
366            && let Some(rules) = self.providers.get(name)
367            && let Some(patterns) = &rules.logging
368        {
369            return pattern_matches(patterns, channel);
370        }
371
372        if let Some(patterns) = &self.default.logging
373            && pattern_matches(patterns, channel)
374        {
375            return true;
376        }
377
378        false
379    }
380
381    /// Determine whether a configuration key can be modified
382    pub fn is_configuration_allowed(
383        &self,
384        provider: Option<&str>,
385        category: &str,
386        key: &str,
387    ) -> bool {
388        if !self.enforce {
389            return true;
390        }
391
392        if let Some(name) = provider
393            && let Some(rules) = self.providers.get(name)
394            && let Some(result) = configuration_allowed(rules, category, key)
395        {
396            return result;
397        }
398
399        if let Some(result) = configuration_allowed(&self.default, category, key) {
400            return result;
401        }
402
403        false
404    }
405
406    fn resolve_match<'a, F>(&'a self, provider: &str, candidate: &str, accessor: F) -> bool
407    where
408        F: Fn(&'a McpAllowListRules) -> &'a Option<Vec<String>>,
409    {
410        if let Some(rules) = self.providers.get(provider)
411            && let Some(patterns) = accessor(rules)
412        {
413            return pattern_matches(patterns, candidate);
414        }
415
416        if let Some(patterns) = accessor(&self.default)
417            && pattern_matches(patterns, candidate)
418        {
419            return true;
420        }
421
422        false
423    }
424}
425
426fn configuration_allowed(rules: &McpAllowListRules, category: &str, key: &str) -> Option<bool> {
427    rules.configuration.as_ref().and_then(|entries| {
428        entries
429            .get(category)
430            .map(|patterns| pattern_matches(patterns, key))
431    })
432}
433
434fn pattern_matches(patterns: &[String], candidate: &str) -> bool {
435    patterns
436        .iter()
437        .any(|pattern| wildcard_match(pattern, candidate))
438}
439
440fn wildcard_match(pattern: &str, candidate: &str) -> bool {
441    if pattern == "*" {
442        return true;
443    }
444
445    let mut regex_pattern = String::from("^");
446    let mut literal_buffer = String::new();
447
448    for ch in pattern.chars() {
449        match ch {
450            '*' => {
451                if !literal_buffer.is_empty() {
452                    regex_pattern.push_str(&regex::escape(&literal_buffer));
453                    literal_buffer.clear();
454                }
455                regex_pattern.push_str(".*");
456            }
457            '?' => {
458                if !literal_buffer.is_empty() {
459                    regex_pattern.push_str(&regex::escape(&literal_buffer));
460                    literal_buffer.clear();
461                }
462                regex_pattern.push('.');
463            }
464            _ => literal_buffer.push(ch),
465        }
466    }
467
468    if !literal_buffer.is_empty() {
469        regex_pattern.push_str(&regex::escape(&literal_buffer));
470    }
471
472    regex_pattern.push('$');
473
474    Regex::new(&regex_pattern)
475        .map(|regex| regex.is_match(candidate))
476        .unwrap_or(false)
477}
478
479/// Allow list rules for a provider or default configuration
480#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
481#[derive(Debug, Clone, Deserialize, Serialize, Default)]
482pub struct McpAllowListRules {
483    /// Tool name patterns permitted for the provider
484    #[serde(default)]
485    pub tools: Option<Vec<String>>,
486
487    /// Resource name patterns permitted for the provider
488    #[serde(default)]
489    pub resources: Option<Vec<String>>,
490
491    /// Prompt name patterns permitted for the provider
492    #[serde(default)]
493    pub prompts: Option<Vec<String>>,
494
495    /// Logging channels permitted for the provider
496    #[serde(default)]
497    pub logging: Option<Vec<String>>,
498
499    /// Configuration keys permitted for the provider grouped by category
500    #[serde(default)]
501    pub configuration: Option<BTreeMap<String, Vec<String>>>,
502}
503
504/// Configuration for the MCP server (vtcode acting as an MCP server)
505#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
506#[derive(Debug, Clone, Deserialize, Serialize)]
507pub struct McpServerConfig {
508    /// Enable vtcode's MCP server capability
509    #[serde(default = "default_mcp_server_enabled")]
510    pub enabled: bool,
511
512    /// Bind address for the MCP server
513    #[serde(default = "default_mcp_server_bind")]
514    pub bind_address: String,
515
516    /// Port for the MCP server
517    #[serde(default = "default_mcp_server_port")]
518    pub port: u16,
519
520    /// Server transport type
521    #[serde(default = "default_mcp_server_transport")]
522    pub transport: McpServerTransport,
523
524    /// Server identifier
525    #[serde(default = "default_mcp_server_name")]
526    pub name: String,
527
528    /// Server version
529    #[serde(default = "default_mcp_server_version")]
530    pub version: String,
531
532    /// Tools exposed by the vtcode MCP server
533    #[serde(default)]
534    pub exposed_tools: Vec<String>,
535}
536
537impl Default for McpServerConfig {
538    fn default() -> Self {
539        Self {
540            enabled: default_mcp_server_enabled(),
541            bind_address: default_mcp_server_bind(),
542            port: default_mcp_server_port(),
543            transport: default_mcp_server_transport(),
544            name: default_mcp_server_name(),
545            version: default_mcp_server_version(),
546            exposed_tools: Vec::new(),
547        }
548    }
549}
550
551/// MCP server transport types
552#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
553#[derive(Debug, Clone, Deserialize, Serialize)]
554#[serde(rename_all = "snake_case")]
555#[derive(Default)]
556pub enum McpServerTransport {
557    /// Server Sent Events transport
558    #[default]
559    Sse,
560    /// HTTP transport
561    Http,
562}
563
564/// Transport configuration for MCP providers
565#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
566#[derive(Debug, Clone, Deserialize, Serialize)]
567#[serde(untagged)]
568pub enum McpTransportConfig {
569    /// Standard I/O transport (stdio)
570    Stdio(McpStdioServerConfig),
571    /// HTTP transport
572    Http(McpHttpServerConfig),
573}
574
575/// Configuration for stdio-based MCP servers
576#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
577#[derive(Debug, Clone, Deserialize, Serialize, Default)]
578pub struct McpStdioServerConfig {
579    /// Command to execute
580    pub command: String,
581
582    /// Command arguments
583    pub args: Vec<String>,
584
585    /// Working directory for the command
586    #[serde(default)]
587    pub working_directory: Option<String>,
588}
589
590/// Configuration for HTTP-based MCP servers
591///
592/// Note: HTTP transport is partially implemented. Basic connectivity testing is supported,
593/// but full streamable HTTP MCP server support requires additional implementation
594/// using Server-Sent Events (SSE) or WebSocket connections.
595#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
596#[derive(Debug, Clone, Deserialize, Serialize)]
597pub struct McpHttpServerConfig {
598    /// Server endpoint URL
599    pub endpoint: String,
600
601    /// API key environment variable name
602    #[serde(default)]
603    pub api_key_env: Option<String>,
604
605    /// Protocol version
606    #[serde(default = "default_mcp_protocol_version")]
607    pub protocol_version: String,
608
609    /// Headers to include in requests
610    #[serde(default, alias = "headers")]
611    pub http_headers: HashMap<String, String>,
612
613    /// Headers whose values are sourced from environment variables
614    /// (`{ header-name = "ENV_VAR" }`). Empty values are ignored.
615    #[serde(default)]
616    pub env_http_headers: HashMap<String, String>,
617}
618
619impl Default for McpHttpServerConfig {
620    fn default() -> Self {
621        Self {
622            endpoint: String::new(),
623            api_key_env: None,
624            protocol_version: default_mcp_protocol_version(),
625            http_headers: HashMap::new(),
626            env_http_headers: HashMap::new(),
627        }
628    }
629}
630
631/// Default value functions
632fn default_mcp_enabled() -> bool {
633    false
634}
635
636fn default_mcp_ui_mode() -> McpUiMode {
637    McpUiMode::Compact
638}
639
640fn default_max_mcp_events() -> usize {
641    50
642}
643
644fn default_show_provider_names() -> bool {
645    true
646}
647
648fn default_max_concurrent_connections() -> usize {
649    5
650}
651
652fn default_request_timeout_seconds() -> u64 {
653    30
654}
655
656fn default_retry_attempts() -> u32 {
657    3
658}
659
660fn default_experimental_use_rmcp_client() -> bool {
661    true
662}
663
664fn default_provider_enabled() -> bool {
665    true
666}
667
668fn default_provider_max_concurrent() -> usize {
669    3
670}
671
672fn default_allowlist_enforced() -> bool {
673    false
674}
675
676fn default_mcp_protocol_version() -> String {
677    "2024-11-05".into()
678}
679
680fn default_mcp_server_enabled() -> bool {
681    false
682}
683
684fn default_connection_pooling_enabled() -> bool {
685    true
686}
687
688fn default_tool_cache_capacity() -> usize {
689    100
690}
691
692fn default_connection_timeout_seconds() -> u64 {
693    30
694}
695
696fn default_mcp_server_bind() -> String {
697    "127.0.0.1".into()
698}
699
700fn default_mcp_server_port() -> u16 {
701    3000
702}
703
704fn default_mcp_server_transport() -> McpServerTransport {
705    McpServerTransport::Sse
706}
707
708fn default_mcp_server_name() -> String {
709    "vtcode-mcp-server".into()
710}
711
712fn default_mcp_server_version() -> String {
713    env!("CARGO_PKG_VERSION").into()
714}
715
716fn normalize_mcp_identifier(value: &str) -> String {
717    value
718        .chars()
719        .filter(|ch| ch.is_ascii_alphanumeric())
720        .map(|ch| ch.to_ascii_lowercase())
721        .collect()
722}
723
724fn default_mcp_auth_enabled() -> bool {
725    false
726}
727
728fn default_requests_per_minute() -> u32 {
729    100
730}
731
732fn default_concurrent_requests() -> u32 {
733    10
734}
735
736fn default_schema_validation_enabled() -> bool {
737    true
738}
739
740fn default_path_traversal_protection_enabled() -> bool {
741    true
742}
743
744fn default_max_argument_size() -> u32 {
745    1024 * 1024 // 1MB
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751    use crate::constants::mcp as mcp_constants;
752    use std::collections::BTreeMap;
753
754    #[test]
755    fn test_mcp_config_defaults() {
756        let config = McpClientConfig::default();
757        assert!(!config.enabled);
758        assert_eq!(config.ui.mode, McpUiMode::Compact);
759        assert_eq!(config.ui.max_events, 50);
760        assert!(config.ui.show_provider_names);
761        assert!(config.ui.renderers.is_empty());
762        assert_eq!(config.max_concurrent_connections, 5);
763        assert_eq!(config.request_timeout_seconds, 30);
764        assert_eq!(config.retry_attempts, 3);
765        assert!(config.providers.is_empty());
766        assert!(!config.server.enabled);
767        assert!(!config.allowlist.enforce);
768        assert!(config.allowlist.default.tools.is_none());
769    }
770
771    #[test]
772    fn test_allowlist_pattern_matching() {
773        let patterns = vec!["get_*".to_string(), "convert_timezone".to_string()];
774        assert!(pattern_matches(&patterns, "get_current_time"));
775        assert!(pattern_matches(&patterns, "convert_timezone"));
776        assert!(!pattern_matches(&patterns, "delete_timezone"));
777    }
778
779    #[test]
780    fn test_allowlist_provider_override() {
781        let mut config = McpAllowListConfig::default();
782        config.enforce = true;
783        config.default.tools = Some(vec!["get_*".to_string()]);
784
785        let mut provider_rules = McpAllowListRules::default();
786        provider_rules.tools = Some(vec!["list_*".to_string()]);
787        config
788            .providers
789            .insert("context7".to_string(), provider_rules);
790
791        assert!(config.is_tool_allowed("context7", "list_documents"));
792        assert!(!config.is_tool_allowed("context7", "get_current_time"));
793        assert!(config.is_tool_allowed("other", "get_timezone"));
794        assert!(!config.is_tool_allowed("other", "list_documents"));
795    }
796
797    #[test]
798    fn test_allowlist_configuration_rules() {
799        let mut config = McpAllowListConfig::default();
800        config.enforce = true;
801
802        let mut default_rules = McpAllowListRules::default();
803        default_rules.configuration = Some(BTreeMap::from([(
804            "ui".to_string(),
805            vec!["mode".to_string(), "max_events".to_string()],
806        )]));
807        config.default = default_rules;
808
809        let mut provider_rules = McpAllowListRules::default();
810        provider_rules.configuration = Some(BTreeMap::from([(
811            "provider".to_string(),
812            vec!["max_concurrent_requests".to_string()],
813        )]));
814        config.providers.insert("time".to_string(), provider_rules);
815
816        assert!(config.is_configuration_allowed(None, "ui", "mode"));
817        assert!(!config.is_configuration_allowed(None, "ui", "show_provider_names"));
818        assert!(config.is_configuration_allowed(
819            Some("time"),
820            "provider",
821            "max_concurrent_requests"
822        ));
823        assert!(!config.is_configuration_allowed(Some("time"), "provider", "retry_attempts"));
824    }
825
826    #[test]
827    fn test_allowlist_resource_override() {
828        let mut config = McpAllowListConfig::default();
829        config.enforce = true;
830        config.default.resources = Some(vec!["docs/*".to_string()]);
831
832        let mut provider_rules = McpAllowListRules::default();
833        provider_rules.resources = Some(vec!["journals/*".to_string()]);
834        config
835            .providers
836            .insert("context7".to_string(), provider_rules);
837
838        assert!(config.is_resource_allowed("context7", "journals/2024"));
839        assert!(!config.is_resource_allowed("context7", "docs/manual"));
840        assert!(config.is_resource_allowed("other", "docs/reference"));
841        assert!(!config.is_resource_allowed("other", "journals/2023"));
842    }
843
844    #[test]
845    fn test_allowlist_logging_override() {
846        let mut config = McpAllowListConfig::default();
847        config.enforce = true;
848        config.default.logging = Some(vec!["info".to_string(), "debug".to_string()]);
849
850        let mut provider_rules = McpAllowListRules::default();
851        provider_rules.logging = Some(vec!["audit".to_string()]);
852        config
853            .providers
854            .insert("sequential".to_string(), provider_rules);
855
856        assert!(config.is_logging_channel_allowed(Some("sequential"), "audit"));
857        assert!(!config.is_logging_channel_allowed(Some("sequential"), "info"));
858        assert!(config.is_logging_channel_allowed(Some("other"), "info"));
859        assert!(!config.is_logging_channel_allowed(Some("other"), "trace"));
860    }
861
862    #[test]
863    fn test_mcp_ui_renderer_resolution() {
864        let mut config = McpUiConfig::default();
865        config.renderers.insert(
866            mcp_constants::RENDERER_CONTEXT7.to_string(),
867            McpRendererProfile::Context7,
868        );
869        config.renderers.insert(
870            mcp_constants::RENDERER_SEQUENTIAL_THINKING.to_string(),
871            McpRendererProfile::SequentialThinking,
872        );
873
874        assert_eq!(
875            config.renderer_for_tool("mcp_context7_lookup"),
876            Some(McpRendererProfile::Context7)
877        );
878        assert_eq!(
879            config.renderer_for_tool("mcp_context7lookup"),
880            Some(McpRendererProfile::Context7)
881        );
882        assert_eq!(
883            config.renderer_for_tool("mcp_sequentialthinking_run"),
884            Some(McpRendererProfile::SequentialThinking)
885        );
886        assert_eq!(
887            config.renderer_for_identifier("sequential-thinking-analyze"),
888            Some(McpRendererProfile::SequentialThinking)
889        );
890        assert_eq!(config.renderer_for_tool("mcp_unknown"), None);
891    }
892}