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