Skip to main content

vtcode_config/
mcp.rs

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