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