Skip to main content

vtcode_config/
mcp.rs

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