1use hashbrown::HashMap;
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6mod transport;
7pub use transport::{
8 MCP_LEGACY_PROTOCOL_VERSION, MCP_STABLE_PROTOCOL_VERSION, McpHttpHandshakeMode, McpHttpServerConfig,
9 McpProviderConfig, McpStdioServerConfig, McpTransportConfig,
10};
11
12#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
14#[derive(Debug, Clone, Deserialize, Serialize)]
15pub struct McpClientConfig {
16 #[serde(default = "default_mcp_enabled")]
18 pub enabled: bool,
19
20 #[serde(default)]
22 pub ui: McpUiConfig,
23
24 #[serde(default)]
26 pub providers: Vec<McpProviderConfig>,
27
28 #[serde(default)]
30 pub requirements: McpRequirementsConfig,
31
32 #[serde(default)]
34 pub server: McpServerConfig,
35
36 #[serde(default)]
38 pub allowlist: McpAllowListConfig,
39
40 #[serde(default = "default_max_concurrent_connections")]
42 pub max_concurrent_connections: usize,
43
44 #[serde(default = "default_request_timeout_seconds")]
46 pub request_timeout_seconds: u64,
47
48 #[serde(default = "default_retry_attempts")]
50 pub retry_attempts: u32,
51
52 #[serde(default)]
54 pub startup_timeout_seconds: Option<u64>,
55
56 #[serde(default)]
58 pub tool_timeout_seconds: Option<u64>,
59
60 #[serde(default = "default_experimental_use_rmcp_client")]
62 pub experimental_use_rmcp_client: bool,
63
64 #[serde(default = "default_connection_pooling_enabled")]
66 pub connection_pooling_enabled: bool,
67
68 #[serde(default = "default_tool_cache_capacity")]
70 pub tool_cache_capacity: usize,
71
72 #[serde(default = "default_connection_timeout_seconds")]
74 pub connection_timeout_seconds: u64,
75
76 #[serde(default)]
78 pub security: McpSecurityConfig,
79
80 #[serde(default)]
82 pub lifecycle: McpLifecycleConfig,
83}
84
85#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
87#[derive(Debug, Clone, Deserialize, Serialize)]
88pub struct McpSecurityConfig {
89 #[serde(default = "default_mcp_auth_enabled")]
91 pub auth_enabled: bool,
92
93 #[serde(default)]
95 pub api_key_env: Option<String>,
96
97 #[serde(default)]
99 rate_limit: McpRateLimitConfig,
100
101 #[serde(default)]
103 pub validation: McpValidationConfig,
104}
105
106impl Default for McpSecurityConfig {
107 fn default() -> Self {
108 Self {
109 auth_enabled: default_mcp_auth_enabled(),
110 api_key_env: None,
111 rate_limit: McpRateLimitConfig::default(),
112 validation: McpValidationConfig::default(),
113 }
114 }
115}
116
117#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
119#[derive(Debug, Clone, Deserialize, Serialize)]
120pub struct McpRateLimitConfig {
121 #[serde(default = "default_requests_per_minute")]
123 requests_per_minute: u32,
124
125 #[serde(default = "default_concurrent_requests")]
127 concurrent_requests: u32,
128}
129
130impl Default for McpRateLimitConfig {
131 fn default() -> Self {
132 Self {
133 requests_per_minute: default_requests_per_minute(),
134 concurrent_requests: default_concurrent_requests(),
135 }
136 }
137}
138
139#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
141#[derive(Debug, Clone, Deserialize, Serialize)]
142pub struct McpValidationConfig {
143 #[serde(default = "default_schema_validation_enabled")]
145 schema_validation_enabled: bool,
146
147 #[serde(default = "default_path_traversal_protection_enabled")]
149 pub path_traversal_protection: bool,
150
151 #[serde(default = "default_max_argument_size")]
153 pub max_argument_size: u32,
154}
155
156impl Default for McpValidationConfig {
157 fn default() -> Self {
158 Self {
159 schema_validation_enabled: default_schema_validation_enabled(),
160 path_traversal_protection: default_path_traversal_protection_enabled(),
161 max_argument_size: default_max_argument_size(),
162 }
163 }
164}
165
166impl Default for McpClientConfig {
167 fn default() -> Self {
168 Self {
169 enabled: default_mcp_enabled(),
170 ui: McpUiConfig::default(),
171 providers: Vec::new(),
172 requirements: McpRequirementsConfig::default(),
173 server: McpServerConfig::default(),
174 allowlist: McpAllowListConfig::default(),
175 max_concurrent_connections: default_max_concurrent_connections(),
176 request_timeout_seconds: default_request_timeout_seconds(),
177 retry_attempts: default_retry_attempts(),
178 startup_timeout_seconds: None,
179 tool_timeout_seconds: None,
180 experimental_use_rmcp_client: default_experimental_use_rmcp_client(),
181 security: McpSecurityConfig::default(),
182 lifecycle: McpLifecycleConfig::default(),
183 connection_pooling_enabled: default_connection_pooling_enabled(),
184 connection_timeout_seconds: default_connection_timeout_seconds(),
185 tool_cache_capacity: default_tool_cache_capacity(),
186 }
187 }
188}
189
190#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
192#[derive(Debug, Clone, Deserialize, Serialize)]
193pub struct McpLifecycleConfig {
194 #[serde(default = "default_allow_model_lifecycle_control")]
196 pub allow_model_control: bool,
197}
198
199impl Default for McpLifecycleConfig {
200 fn default() -> Self {
201 Self {
202 allow_model_control: default_allow_model_lifecycle_control(),
203 }
204 }
205}
206
207#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
209#[derive(Debug, Clone, Deserialize, Serialize)]
210pub struct McpRequirementsConfig {
211 #[serde(default = "default_mcp_requirements_enforce")]
213 pub enforce: bool,
214
215 #[serde(default)]
217 pub allowed_stdio_commands: Vec<String>,
218
219 #[serde(default)]
221 pub allowed_http_endpoints: Vec<String>,
222}
223
224impl Default for McpRequirementsConfig {
225 fn default() -> Self {
226 Self {
227 enforce: default_mcp_requirements_enforce(),
228 allowed_stdio_commands: Vec::new(),
229 allowed_http_endpoints: Vec::new(),
230 }
231 }
232}
233
234#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
236#[derive(Debug, Clone, Deserialize, Serialize)]
237pub struct McpUiConfig {
238 #[serde(default = "default_mcp_ui_mode")]
240 pub mode: McpUiMode,
241
242 #[serde(default = "default_max_mcp_events")]
244 pub max_events: usize,
245
246 #[serde(default = "default_show_provider_names")]
248 pub show_provider_names: bool,
249
250 #[serde(default)]
252 #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, McpRendererProfile>"))]
253 pub renderers: HashMap<String, McpRendererProfile>,
254}
255
256impl Default for McpUiConfig {
257 fn default() -> Self {
258 Self {
259 mode: default_mcp_ui_mode(),
260 max_events: default_max_mcp_events(),
261 show_provider_names: default_show_provider_names(),
262 renderers: HashMap::new(),
263 }
264 }
265}
266
267impl McpUiConfig {
268 fn renderer_for_identifier(&self, identifier: &str) -> Option<McpRendererProfile> {
270 let normalized_identifier = normalize_mcp_identifier(identifier);
271 if normalized_identifier.is_empty() {
272 return None;
273 }
274
275 self.renderers.iter().find_map(|(key, profile)| {
276 let normalized_key = normalize_mcp_identifier(key);
277 if normalized_identifier.starts_with(&normalized_key) {
278 Some(*profile)
279 } else {
280 None
281 }
282 })
283 }
284
285 pub fn renderer_for_tool(&self, tool_name: &str) -> Option<McpRendererProfile> {
287 let identifier = tool_name.strip_prefix("mcp_").unwrap_or(tool_name);
288 self.renderer_for_identifier(identifier)
289 }
290}
291
292#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
295#[serde(rename_all = "snake_case")]
296#[derive(Default)]
297pub enum McpUiMode {
298 #[default]
300 Compact,
301 Full,
303}
304
305impl std::fmt::Display for McpUiMode {
306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307 match self {
308 McpUiMode::Compact => write!(f, "compact"),
309 McpUiMode::Full => write!(f, "full"),
310 }
311 }
312}
313
314#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
316#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
317#[serde(rename_all = "kebab-case")]
318pub enum McpRendererProfile {
319 Context7,
321 SequentialThinking,
323}
324
325#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
327#[derive(Debug, Clone, Deserialize, Serialize)]
328pub struct McpAllowListConfig {
329 #[serde(default = "default_allowlist_enforced")]
331 pub enforce: bool,
332
333 #[serde(default)]
335 pub default: McpAllowListRules,
336
337 #[serde(default)]
339 pub providers: BTreeMap<String, McpAllowListRules>,
340}
341
342impl Default for McpAllowListConfig {
343 fn default() -> Self {
344 Self {
345 enforce: default_allowlist_enforced(),
346 default: McpAllowListRules::default(),
347 providers: BTreeMap::new(),
348 }
349 }
350}
351
352impl McpAllowListConfig {
353 #[must_use]
355 pub fn is_tool_allowed(&self, provider: &str, tool_name: &str) -> bool {
356 if !self.enforce {
357 return true;
358 }
359
360 self.resolve_match(provider, tool_name, |rules| &rules.tools)
361 }
362
363 #[must_use]
365 pub fn is_resource_allowed(&self, provider: &str, resource: &str) -> bool {
366 if !self.enforce {
367 return true;
368 }
369
370 self.resolve_match(provider, resource, |rules| &rules.resources)
371 }
372
373 #[must_use]
375 pub fn is_prompt_allowed(&self, provider: &str, prompt: &str) -> bool {
376 if !self.enforce {
377 return true;
378 }
379
380 self.resolve_match(provider, prompt, |rules| &rules.prompts)
381 }
382
383 fn is_logging_channel_allowed(&self, provider: Option<&str>, channel: &str) -> bool {
385 if !self.enforce {
386 return true;
387 }
388
389 if let Some(name) = provider
390 && let Some(rules) = self.providers.get(name)
391 && let Some(patterns) = &rules.logging
392 {
393 return pattern_matches(patterns, channel);
394 }
395
396 if let Some(patterns) = &self.default.logging
397 && pattern_matches(patterns, channel)
398 {
399 return true;
400 }
401
402 false
403 }
404
405 fn is_configuration_allowed(&self, provider: Option<&str>, category: &str, key: &str) -> bool {
407 if !self.enforce {
408 return true;
409 }
410
411 if let Some(name) = provider
412 && let Some(rules) = self.providers.get(name)
413 && let Some(result) = configuration_allowed(rules, category, key)
414 {
415 return result;
416 }
417
418 if let Some(result) = configuration_allowed(&self.default, category, key) {
419 return result;
420 }
421
422 false
423 }
424
425 fn resolve_match<'a, F>(&'a self, provider: &str, candidate: &str, accessor: F) -> bool
426 where
427 F: Fn(&'a McpAllowListRules) -> &'a Option<Vec<String>>,
428 {
429 if let Some(rules) = self.providers.get(provider)
430 && let Some(patterns) = accessor(rules)
431 {
432 return pattern_matches(patterns, candidate);
433 }
434
435 if let Some(patterns) = accessor(&self.default)
436 && pattern_matches(patterns, candidate)
437 {
438 return true;
439 }
440
441 false
442 }
443}
444
445fn configuration_allowed(rules: &McpAllowListRules, category: &str, key: &str) -> Option<bool> {
446 rules
447 .configuration
448 .as_ref()
449 .and_then(|entries| entries.get(category).map(|patterns| pattern_matches(patterns, key)))
450}
451
452fn pattern_matches(patterns: &[String], candidate: &str) -> bool {
453 patterns.iter().any(|pattern| wildcard_match(pattern, candidate))
454}
455
456fn wildcard_match(pattern: &str, candidate: &str) -> bool {
457 if pattern == "*" {
458 return true;
459 }
460
461 let mut regex_pattern = String::from("^");
462 let mut literal_buffer = String::new();
463
464 for ch in pattern.chars() {
465 match ch {
466 '*' => {
467 if !literal_buffer.is_empty() {
468 regex_pattern.push_str(®ex::escape(&literal_buffer));
469 literal_buffer.clear();
470 }
471 regex_pattern.push_str(".*");
472 }
473 '?' => {
474 if !literal_buffer.is_empty() {
475 regex_pattern.push_str(®ex::escape(&literal_buffer));
476 literal_buffer.clear();
477 }
478 regex_pattern.push('.');
479 }
480 _ => literal_buffer.push(ch),
481 }
482 }
483
484 if !literal_buffer.is_empty() {
485 regex_pattern.push_str(®ex::escape(&literal_buffer));
486 }
487
488 regex_pattern.push('$');
489
490 Regex::new(®ex_pattern)
491 .map(|regex| regex.is_match(candidate))
492 .unwrap_or(false)
493}
494
495#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
497#[derive(Debug, Clone, Deserialize, Serialize, Default)]
498pub struct McpAllowListRules {
499 #[serde(default)]
501 pub tools: Option<Vec<String>>,
502
503 #[serde(default)]
505 pub resources: Option<Vec<String>>,
506
507 #[serde(default)]
509 pub prompts: Option<Vec<String>>,
510
511 #[serde(default)]
513 pub logging: Option<Vec<String>>,
514
515 #[serde(default)]
517 pub configuration: Option<BTreeMap<String, Vec<String>>>,
518}
519
520#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
522#[derive(Debug, Clone, Deserialize, Serialize)]
523pub struct McpServerConfig {
524 #[serde(default = "default_mcp_server_enabled")]
526 pub enabled: bool,
527
528 #[serde(default = "default_mcp_server_bind")]
530 pub bind_address: String,
531
532 #[serde(default = "default_mcp_server_port")]
534 pub port: u16,
535
536 #[serde(default = "default_mcp_server_transport")]
538 pub transport: McpServerTransport,
539
540 #[serde(default = "default_mcp_server_name")]
542 pub name: String,
543
544 #[serde(default = "default_mcp_server_version")]
546 pub version: String,
547
548 #[serde(default)]
550 pub exposed_tools: Vec<String>,
551}
552
553impl Default for McpServerConfig {
554 fn default() -> Self {
555 Self {
556 enabled: default_mcp_server_enabled(),
557 bind_address: default_mcp_server_bind(),
558 port: default_mcp_server_port(),
559 transport: default_mcp_server_transport(),
560 name: default_mcp_server_name(),
561 version: default_mcp_server_version(),
562 exposed_tools: Vec::new(),
563 }
564 }
565}
566
567#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
569#[derive(Debug, Clone, Deserialize, Serialize)]
570#[serde(rename_all = "snake_case")]
571#[derive(Default)]
572pub enum McpServerTransport {
573 #[default]
575 Sse,
576 Http,
578}
579
580fn default_mcp_enabled() -> bool {
582 false
583}
584
585fn default_mcp_ui_mode() -> McpUiMode {
586 McpUiMode::Compact
587}
588
589fn default_max_mcp_events() -> usize {
590 50
591}
592
593fn default_show_provider_names() -> bool {
594 true
595}
596
597fn default_max_concurrent_connections() -> usize {
598 5
599}
600
601fn default_request_timeout_seconds() -> u64 {
602 30
603}
604
605fn default_retry_attempts() -> u32 {
606 3
607}
608
609fn default_experimental_use_rmcp_client() -> bool {
610 true
611}
612
613fn default_allowlist_enforced() -> bool {
614 false
615}
616
617fn default_mcp_server_enabled() -> bool {
618 false
619}
620
621fn default_connection_pooling_enabled() -> bool {
622 true
623}
624
625fn default_tool_cache_capacity() -> usize {
626 100
627}
628
629fn default_connection_timeout_seconds() -> u64 {
630 30
631}
632
633fn default_allow_model_lifecycle_control() -> bool {
634 false
635}
636
637fn default_mcp_server_bind() -> String {
638 "127.0.0.1".into()
639}
640
641fn default_mcp_server_port() -> u16 {
642 3000
643}
644
645fn default_mcp_server_transport() -> McpServerTransport {
646 McpServerTransport::Sse
647}
648
649fn default_mcp_server_name() -> String {
650 "vtcode-mcp-server".into()
651}
652
653fn default_mcp_server_version() -> String {
654 env!("CARGO_PKG_VERSION").into()
655}
656
657fn normalize_mcp_identifier(value: &str) -> String {
658 value
659 .chars()
660 .filter(|ch| ch.is_ascii_alphanumeric())
661 .map(|ch| ch.to_ascii_lowercase())
662 .collect()
663}
664
665fn default_mcp_auth_enabled() -> bool {
666 false
667}
668
669fn default_requests_per_minute() -> u32 {
670 100
671}
672
673fn default_concurrent_requests() -> u32 {
674 10
675}
676
677fn default_schema_validation_enabled() -> bool {
678 true
679}
680
681fn default_path_traversal_protection_enabled() -> bool {
682 true
683}
684
685fn default_max_argument_size() -> u32 {
686 1024 * 1024 }
688
689fn default_mcp_requirements_enforce() -> bool {
690 false
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696 use crate::constants::mcp as mcp_constants;
697 use std::collections::BTreeMap;
698
699 #[test]
700 fn test_mcp_config_defaults() {
701 let config = McpClientConfig::default();
702 assert!(!config.enabled);
703 assert_eq!(config.ui.mode, McpUiMode::Compact);
704 assert_eq!(config.ui.max_events, 50);
705 assert!(config.ui.show_provider_names);
706 assert!(config.ui.renderers.is_empty());
707 assert_eq!(config.max_concurrent_connections, 5);
708 assert_eq!(config.request_timeout_seconds, 30);
709 assert_eq!(config.retry_attempts, 3);
710 assert!(!config.lifecycle.allow_model_control);
711 assert!(config.providers.is_empty());
712 assert!(!config.requirements.enforce);
713 assert!(config.requirements.allowed_stdio_commands.is_empty());
714 assert!(config.requirements.allowed_http_endpoints.is_empty());
715 assert!(!config.server.enabled);
716 assert!(!config.allowlist.enforce);
717 assert!(config.allowlist.default.tools.is_none());
718 }
719
720 #[test]
721 fn test_allowlist_pattern_matching() {
722 let patterns = vec!["get_*".to_string(), "convert_timezone".to_string()];
723 assert!(pattern_matches(&patterns, "get_current_time"));
724 assert!(pattern_matches(&patterns, "convert_timezone"));
725 assert!(!pattern_matches(&patterns, "delete_timezone"));
726 }
727
728 #[test]
729 fn test_allowlist_provider_override() {
730 let mut config = McpAllowListConfig {
731 enforce: true,
732 default: McpAllowListRules {
733 tools: Some(vec!["get_*".to_string()]),
734 ..Default::default()
735 },
736 ..Default::default()
737 };
738
739 let provider_rules = McpAllowListRules {
740 tools: Some(vec!["list_*".to_string()]),
741 ..Default::default()
742 };
743 config.providers.insert("context7".to_string(), provider_rules);
744
745 assert!(config.is_tool_allowed("context7", "list_documents"));
746 assert!(!config.is_tool_allowed("context7", "get_current_time"));
747 assert!(config.is_tool_allowed("other", "get_timezone"));
748 assert!(!config.is_tool_allowed("other", "list_documents"));
749 }
750
751 #[test]
752 fn test_allowlist_configuration_rules() {
753 let mut config = McpAllowListConfig {
754 enforce: true,
755 default: McpAllowListRules {
756 configuration: Some(BTreeMap::from([(
757 "ui".to_string(),
758 vec!["mode".to_string(), "max_events".to_string()],
759 )])),
760 ..Default::default()
761 },
762 ..Default::default()
763 };
764
765 let provider_rules = McpAllowListRules {
766 configuration: Some(BTreeMap::from([(
767 "provider".to_string(),
768 vec!["max_concurrent_requests".to_string()],
769 )])),
770 ..Default::default()
771 };
772 config.providers.insert("time".to_string(), provider_rules);
773
774 assert!(config.is_configuration_allowed(None, "ui", "mode"));
775 assert!(!config.is_configuration_allowed(None, "ui", "show_provider_names"));
776 assert!(config.is_configuration_allowed(Some("time"), "provider", "max_concurrent_requests"));
777 assert!(!config.is_configuration_allowed(Some("time"), "provider", "retry_attempts"));
778 }
779
780 #[test]
781 fn test_allowlist_resource_override() {
782 let mut config = McpAllowListConfig {
783 enforce: true,
784 default: McpAllowListRules {
785 resources: Some(vec!["docs/**/*".to_string()]),
786 ..Default::default()
787 },
788 ..Default::default()
789 };
790
791 let provider_rules = McpAllowListRules {
792 resources: Some(vec!["journals/*".to_string()]),
793 ..Default::default()
794 };
795 config.providers.insert("context7".to_string(), provider_rules);
796
797 assert!(config.is_resource_allowed("context7", "journals/2024"));
798 assert!(config.is_resource_allowed("other", "docs/config/config.md"));
799 assert!(config.is_resource_allowed("other", "docs/guides/zed-acp.md"));
800 assert!(!config.is_resource_allowed("other", "journals/2023"));
801 }
802
803 #[test]
804 fn test_allowlist_logging_override() {
805 let mut config = McpAllowListConfig {
806 enforce: true,
807 default: McpAllowListRules {
808 logging: Some(vec!["info".to_string(), "debug".to_string()]),
809 ..Default::default()
810 },
811 ..Default::default()
812 };
813
814 let provider_rules = McpAllowListRules {
815 logging: Some(vec!["audit".to_string()]),
816 ..Default::default()
817 };
818 config.providers.insert("sequential".to_string(), provider_rules);
819
820 assert!(config.is_logging_channel_allowed(Some("sequential"), "audit"));
821 assert!(!config.is_logging_channel_allowed(Some("sequential"), "info"));
822 assert!(config.is_logging_channel_allowed(Some("other"), "info"));
823 assert!(!config.is_logging_channel_allowed(Some("other"), "trace"));
824 }
825
826 #[test]
827 fn test_mcp_ui_renderer_resolution() {
828 let mut config = McpUiConfig::default();
829 config
830 .renderers
831 .insert(mcp_constants::RENDERER_CONTEXT7.to_string(), McpRendererProfile::Context7);
832 config
833 .renderers
834 .insert(mcp_constants::RENDERER_SEQUENTIAL_THINKING.to_string(), McpRendererProfile::SequentialThinking);
835
836 assert_eq!(config.renderer_for_tool("mcp_context7_lookup"), Some(McpRendererProfile::Context7));
837 assert_eq!(config.renderer_for_tool("mcp_context7lookup"), Some(McpRendererProfile::Context7));
838 assert_eq!(
839 config.renderer_for_tool("mcp_sequentialthinking_run"),
840 Some(McpRendererProfile::SequentialThinking)
841 );
842 assert_eq!(
843 config.renderer_for_identifier("sequential-thinking-analyze"),
844 Some(McpRendererProfile::SequentialThinking)
845 );
846 assert_eq!(config.renderer_for_tool("mcp_unknown"), None);
847 }
848}