Skip to main content

vtcode_config/
mcp.rs

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