Skip to main content

vtcode_config/
mcp.rs

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