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