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    #[must_use]
395    pub fn is_tool_allowed(&self, provider: &str, tool_name: &str) -> bool {
396        if !self.enforce {
397            return true;
398        }
399
400        self.resolve_match(provider, tool_name, |rules| &rules.tools)
401    }
402
403    /// Determine whether a resource is permitted for the given provider
404    #[must_use]
405    pub fn is_resource_allowed(&self, provider: &str, resource: &str) -> bool {
406        if !self.enforce {
407            return true;
408        }
409
410        self.resolve_match(provider, resource, |rules| &rules.resources)
411    }
412
413    /// Determine whether a prompt is permitted for the given provider
414    #[must_use]
415    pub fn is_prompt_allowed(&self, provider: &str, prompt: &str) -> bool {
416        if !self.enforce {
417            return true;
418        }
419
420        self.resolve_match(provider, prompt, |rules| &rules.prompts)
421    }
422
423    /// Determine whether a logging channel is permitted
424    pub fn is_logging_channel_allowed(&self, provider: Option<&str>, channel: &str) -> bool {
425        if !self.enforce {
426            return true;
427        }
428
429        if let Some(name) = provider
430            && let Some(rules) = self.providers.get(name)
431            && let Some(patterns) = &rules.logging
432        {
433            return pattern_matches(patterns, channel);
434        }
435
436        if let Some(patterns) = &self.default.logging
437            && pattern_matches(patterns, channel)
438        {
439            return true;
440        }
441
442        false
443    }
444
445    /// Determine whether a configuration key can be modified
446    pub fn is_configuration_allowed(
447        &self,
448        provider: Option<&str>,
449        category: &str,
450        key: &str,
451    ) -> bool {
452        if !self.enforce {
453            return true;
454        }
455
456        if let Some(name) = provider
457            && let Some(rules) = self.providers.get(name)
458            && let Some(result) = configuration_allowed(rules, category, key)
459        {
460            return result;
461        }
462
463        if let Some(result) = configuration_allowed(&self.default, category, key) {
464            return result;
465        }
466
467        false
468    }
469
470    fn resolve_match<'a, F>(&'a self, provider: &str, candidate: &str, accessor: F) -> bool
471    where
472        F: Fn(&'a McpAllowListRules) -> &'a Option<Vec<String>>,
473    {
474        if let Some(rules) = self.providers.get(provider)
475            && let Some(patterns) = accessor(rules)
476        {
477            return pattern_matches(patterns, candidate);
478        }
479
480        if let Some(patterns) = accessor(&self.default)
481            && pattern_matches(patterns, candidate)
482        {
483            return true;
484        }
485
486        false
487    }
488}
489
490fn configuration_allowed(rules: &McpAllowListRules, category: &str, key: &str) -> Option<bool> {
491    rules.configuration.as_ref().and_then(|entries| {
492        entries
493            .get(category)
494            .map(|patterns| pattern_matches(patterns, key))
495    })
496}
497
498fn pattern_matches(patterns: &[String], candidate: &str) -> bool {
499    patterns
500        .iter()
501        .any(|pattern| wildcard_match(pattern, candidate))
502}
503
504fn wildcard_match(pattern: &str, candidate: &str) -> bool {
505    if pattern == "*" {
506        return true;
507    }
508
509    let mut regex_pattern = String::from("^");
510    let mut literal_buffer = String::new();
511
512    for ch in pattern.chars() {
513        match ch {
514            '*' => {
515                if !literal_buffer.is_empty() {
516                    regex_pattern.push_str(&regex::escape(&literal_buffer));
517                    literal_buffer.clear();
518                }
519                regex_pattern.push_str(".*");
520            }
521            '?' => {
522                if !literal_buffer.is_empty() {
523                    regex_pattern.push_str(&regex::escape(&literal_buffer));
524                    literal_buffer.clear();
525                }
526                regex_pattern.push('.');
527            }
528            _ => literal_buffer.push(ch),
529        }
530    }
531
532    if !literal_buffer.is_empty() {
533        regex_pattern.push_str(&regex::escape(&literal_buffer));
534    }
535
536    regex_pattern.push('$');
537
538    Regex::new(&regex_pattern)
539        .map(|regex| regex.is_match(candidate))
540        .unwrap_or(false)
541}
542
543/// Allow list rules for a provider or default configuration
544#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
545#[derive(Debug, Clone, Deserialize, Serialize, Default)]
546pub struct McpAllowListRules {
547    /// Tool name patterns permitted for the provider
548    #[serde(default)]
549    pub tools: Option<Vec<String>>,
550
551    /// Resource name patterns permitted for the provider
552    #[serde(default)]
553    pub resources: Option<Vec<String>>,
554
555    /// Prompt name patterns permitted for the provider
556    #[serde(default)]
557    pub prompts: Option<Vec<String>>,
558
559    /// Logging channels permitted for the provider
560    #[serde(default)]
561    pub logging: Option<Vec<String>>,
562
563    /// Configuration keys permitted for the provider grouped by category
564    #[serde(default)]
565    pub configuration: Option<BTreeMap<String, Vec<String>>>,
566}
567
568/// Configuration for the MCP server (vtcode acting as an MCP server)
569#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
570#[derive(Debug, Clone, Deserialize, Serialize)]
571pub struct McpServerConfig {
572    /// Enable vtcode's MCP server capability
573    #[serde(default = "default_mcp_server_enabled")]
574    pub enabled: bool,
575
576    /// Bind address for the MCP server
577    #[serde(default = "default_mcp_server_bind")]
578    pub bind_address: String,
579
580    /// Port for the MCP server
581    #[serde(default = "default_mcp_server_port")]
582    pub port: u16,
583
584    /// Server transport type
585    #[serde(default = "default_mcp_server_transport")]
586    pub transport: McpServerTransport,
587
588    /// Server identifier
589    #[serde(default = "default_mcp_server_name")]
590    pub name: String,
591
592    /// Server version
593    #[serde(default = "default_mcp_server_version")]
594    pub version: String,
595
596    /// Tools exposed by the vtcode MCP server
597    #[serde(default)]
598    pub exposed_tools: Vec<String>,
599}
600
601impl Default for McpServerConfig {
602    fn default() -> Self {
603        Self {
604            enabled: default_mcp_server_enabled(),
605            bind_address: default_mcp_server_bind(),
606            port: default_mcp_server_port(),
607            transport: default_mcp_server_transport(),
608            name: default_mcp_server_name(),
609            version: default_mcp_server_version(),
610            exposed_tools: Vec::new(),
611        }
612    }
613}
614
615/// MCP server transport types
616#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
617#[derive(Debug, Clone, Deserialize, Serialize)]
618#[serde(rename_all = "snake_case")]
619#[derive(Default)]
620pub enum McpServerTransport {
621    /// Server Sent Events transport
622    #[default]
623    Sse,
624    /// HTTP transport
625    Http,
626}
627
628/// Transport configuration for MCP providers
629#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
630#[allow(clippy::large_enum_variant)]
631#[derive(Debug, Clone, Deserialize, Serialize)]
632#[serde(untagged)]
633pub enum McpTransportConfig {
634    /// Standard I/O transport (stdio)
635    Stdio(McpStdioServerConfig),
636    /// HTTP transport
637    Http(McpHttpServerConfig),
638}
639
640/// Configuration for stdio-based MCP servers
641#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
642#[derive(Debug, Clone, Deserialize, Serialize, Default)]
643pub struct McpStdioServerConfig {
644    /// Command to execute
645    pub command: String,
646
647    /// Command arguments
648    pub args: Vec<String>,
649
650    /// Working directory for the command
651    #[serde(default)]
652    pub working_directory: Option<String>,
653}
654
655/// Configuration for HTTP-based MCP servers
656///
657/// Note: HTTP transport is partially implemented. Basic connectivity testing is supported,
658/// but full streamable HTTP MCP server support requires additional implementation
659/// using Server-Sent Events (SSE) or WebSocket connections.
660#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
661#[derive(Debug, Clone, Deserialize, Serialize)]
662pub struct McpHttpServerConfig {
663    /// Server endpoint URL
664    pub endpoint: String,
665
666    /// API key environment variable name
667    #[serde(default)]
668    pub api_key_env: Option<String>,
669
670    /// Optional OAuth configuration for providers that issue bearer tokens dynamically.
671    #[serde(default)]
672    pub oauth: Option<McpOAuthConfig>,
673
674    /// Protocol version
675    #[serde(default = "default_mcp_protocol_version")]
676    pub protocol_version: String,
677
678    /// Headers to include in requests
679    #[serde(default, alias = "headers")]
680    #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, String>"))]
681    pub http_headers: HashMap<String, String>,
682
683    /// Headers whose values are sourced from environment variables
684    /// (`{ header-name = "ENV_VAR" }`). Empty values are ignored.
685    #[serde(default)]
686    #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, String>"))]
687    pub env_http_headers: HashMap<String, String>,
688}
689
690impl Default for McpHttpServerConfig {
691    fn default() -> Self {
692        Self {
693            endpoint: String::new(),
694            api_key_env: None,
695            oauth: None,
696            protocol_version: default_mcp_protocol_version(),
697            http_headers: HashMap::new(),
698            env_http_headers: HashMap::new(),
699        }
700    }
701}
702
703/// Default value functions
704fn default_mcp_enabled() -> bool {
705    false
706}
707
708fn default_mcp_ui_mode() -> McpUiMode {
709    McpUiMode::Compact
710}
711
712fn default_max_mcp_events() -> usize {
713    50
714}
715
716fn default_show_provider_names() -> bool {
717    true
718}
719
720fn default_max_concurrent_connections() -> usize {
721    5
722}
723
724fn default_request_timeout_seconds() -> u64 {
725    30
726}
727
728fn default_retry_attempts() -> u32 {
729    3
730}
731
732fn default_experimental_use_rmcp_client() -> bool {
733    true
734}
735
736fn default_provider_enabled() -> bool {
737    true
738}
739
740fn default_provider_max_concurrent() -> usize {
741    3
742}
743
744fn default_allowlist_enforced() -> bool {
745    false
746}
747
748fn default_mcp_protocol_version() -> String {
749    "2024-11-05".into()
750}
751
752fn default_mcp_server_enabled() -> bool {
753    false
754}
755
756fn default_connection_pooling_enabled() -> bool {
757    true
758}
759
760fn default_tool_cache_capacity() -> usize {
761    100
762}
763
764fn default_connection_timeout_seconds() -> u64 {
765    30
766}
767
768fn default_allow_model_lifecycle_control() -> bool {
769    false
770}
771
772fn default_mcp_server_bind() -> String {
773    "127.0.0.1".into()
774}
775
776fn default_mcp_server_port() -> u16 {
777    3000
778}
779
780fn default_mcp_server_transport() -> McpServerTransport {
781    McpServerTransport::Sse
782}
783
784fn default_mcp_server_name() -> String {
785    "vtcode-mcp-server".into()
786}
787
788fn default_mcp_server_version() -> String {
789    env!("CARGO_PKG_VERSION").into()
790}
791
792fn normalize_mcp_identifier(value: &str) -> String {
793    value
794        .chars()
795        .filter(|ch| ch.is_ascii_alphanumeric())
796        .map(|ch| ch.to_ascii_lowercase())
797        .collect()
798}
799
800fn default_mcp_auth_enabled() -> bool {
801    false
802}
803
804fn default_requests_per_minute() -> u32 {
805    100
806}
807
808fn default_concurrent_requests() -> u32 {
809    10
810}
811
812fn default_schema_validation_enabled() -> bool {
813    true
814}
815
816fn default_path_traversal_protection_enabled() -> bool {
817    true
818}
819
820fn default_max_argument_size() -> u32 {
821    1024 * 1024 // 1MB
822}
823
824fn default_mcp_requirements_enforce() -> bool {
825    false
826}
827
828#[cfg(test)]
829mod tests {
830    use super::*;
831    use crate::constants::mcp as mcp_constants;
832    use std::collections::BTreeMap;
833
834    #[test]
835    fn test_mcp_config_defaults() {
836        let config = McpClientConfig::default();
837        assert!(!config.enabled);
838        assert_eq!(config.ui.mode, McpUiMode::Compact);
839        assert_eq!(config.ui.max_events, 50);
840        assert!(config.ui.show_provider_names);
841        assert!(config.ui.renderers.is_empty());
842        assert_eq!(config.max_concurrent_connections, 5);
843        assert_eq!(config.request_timeout_seconds, 30);
844        assert_eq!(config.retry_attempts, 3);
845        assert!(!config.lifecycle.allow_model_control);
846        assert!(config.providers.is_empty());
847        assert!(!config.requirements.enforce);
848        assert!(config.requirements.allowed_stdio_commands.is_empty());
849        assert!(config.requirements.allowed_http_endpoints.is_empty());
850        assert!(!config.server.enabled);
851        assert!(!config.allowlist.enforce);
852        assert!(config.allowlist.default.tools.is_none());
853    }
854
855    #[test]
856    fn test_allowlist_pattern_matching() {
857        let patterns = vec!["get_*".to_string(), "convert_timezone".to_string()];
858        assert!(pattern_matches(&patterns, "get_current_time"));
859        assert!(pattern_matches(&patterns, "convert_timezone"));
860        assert!(!pattern_matches(&patterns, "delete_timezone"));
861    }
862
863    #[test]
864    fn test_allowlist_provider_override() {
865        let mut config = McpAllowListConfig {
866            enforce: true,
867            default: McpAllowListRules {
868                tools: Some(vec!["get_*".to_string()]),
869                ..Default::default()
870            },
871            ..Default::default()
872        };
873
874        let provider_rules = McpAllowListRules {
875            tools: Some(vec!["list_*".to_string()]),
876            ..Default::default()
877        };
878        config
879            .providers
880            .insert("context7".to_string(), provider_rules);
881
882        assert!(config.is_tool_allowed("context7", "list_documents"));
883        assert!(!config.is_tool_allowed("context7", "get_current_time"));
884        assert!(config.is_tool_allowed("other", "get_timezone"));
885        assert!(!config.is_tool_allowed("other", "list_documents"));
886    }
887
888    #[test]
889    fn test_allowlist_configuration_rules() {
890        let mut config = McpAllowListConfig {
891            enforce: true,
892            default: McpAllowListRules {
893                configuration: Some(BTreeMap::from([(
894                    "ui".to_string(),
895                    vec!["mode".to_string(), "max_events".to_string()],
896                )])),
897                ..Default::default()
898            },
899            ..Default::default()
900        };
901
902        let provider_rules = McpAllowListRules {
903            configuration: Some(BTreeMap::from([(
904                "provider".to_string(),
905                vec!["max_concurrent_requests".to_string()],
906            )])),
907            ..Default::default()
908        };
909        config.providers.insert("time".to_string(), provider_rules);
910
911        assert!(config.is_configuration_allowed(None, "ui", "mode"));
912        assert!(!config.is_configuration_allowed(None, "ui", "show_provider_names"));
913        assert!(config.is_configuration_allowed(
914            Some("time"),
915            "provider",
916            "max_concurrent_requests"
917        ));
918        assert!(!config.is_configuration_allowed(Some("time"), "provider", "retry_attempts"));
919    }
920
921    #[test]
922    fn test_allowlist_resource_override() {
923        let mut config = McpAllowListConfig {
924            enforce: true,
925            default: McpAllowListRules {
926                resources: Some(vec!["docs/**/*".to_string()]),
927                ..Default::default()
928            },
929            ..Default::default()
930        };
931
932        let provider_rules = McpAllowListRules {
933            resources: Some(vec!["journals/*".to_string()]),
934            ..Default::default()
935        };
936        config
937            .providers
938            .insert("context7".to_string(), provider_rules);
939
940        assert!(config.is_resource_allowed("context7", "journals/2024"));
941        assert!(config.is_resource_allowed("other", "docs/config/config.md"));
942        assert!(config.is_resource_allowed("other", "docs/guides/zed-acp.md"));
943        assert!(!config.is_resource_allowed("other", "journals/2023"));
944    }
945
946    #[test]
947    fn test_allowlist_logging_override() {
948        let mut config = McpAllowListConfig {
949            enforce: true,
950            default: McpAllowListRules {
951                logging: Some(vec!["info".to_string(), "debug".to_string()]),
952                ..Default::default()
953            },
954            ..Default::default()
955        };
956
957        let provider_rules = McpAllowListRules {
958            logging: Some(vec!["audit".to_string()]),
959            ..Default::default()
960        };
961        config
962            .providers
963            .insert("sequential".to_string(), provider_rules);
964
965        assert!(config.is_logging_channel_allowed(Some("sequential"), "audit"));
966        assert!(!config.is_logging_channel_allowed(Some("sequential"), "info"));
967        assert!(config.is_logging_channel_allowed(Some("other"), "info"));
968        assert!(!config.is_logging_channel_allowed(Some("other"), "trace"));
969    }
970
971    #[test]
972    fn test_mcp_ui_renderer_resolution() {
973        let mut config = McpUiConfig::default();
974        config.renderers.insert(
975            mcp_constants::RENDERER_CONTEXT7.to_string(),
976            McpRendererProfile::Context7,
977        );
978        config.renderers.insert(
979            mcp_constants::RENDERER_SEQUENTIAL_THINKING.to_string(),
980            McpRendererProfile::SequentialThinking,
981        );
982
983        assert_eq!(
984            config.renderer_for_tool("mcp_context7_lookup"),
985            Some(McpRendererProfile::Context7)
986        );
987        assert_eq!(
988            config.renderer_for_tool("mcp_context7lookup"),
989            Some(McpRendererProfile::Context7)
990        );
991        assert_eq!(
992            config.renderer_for_tool("mcp_sequentialthinking_run"),
993            Some(McpRendererProfile::SequentialThinking)
994        );
995        assert_eq!(
996            config.renderer_for_identifier("sequential-thinking-analyze"),
997            Some(McpRendererProfile::SequentialThinking)
998        );
999        assert_eq!(config.renderer_for_tool("mcp_unknown"), None);
1000    }
1001}