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