1use hashbrown::HashMap;
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use vtcode_auth::McpOAuthConfig;
6
7#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
9#[derive(Debug, Clone, Deserialize, Serialize)]
10pub struct McpClientConfig {
11 #[serde(default = "default_mcp_enabled")]
13 pub enabled: bool,
14
15 #[serde(default)]
17 pub ui: McpUiConfig,
18
19 #[serde(default)]
21 pub providers: Vec<McpProviderConfig>,
22
23 #[serde(default)]
25 pub requirements: McpRequirementsConfig,
26
27 #[serde(default)]
29 pub server: McpServerConfig,
30
31 #[serde(default)]
33 pub allowlist: McpAllowListConfig,
34
35 #[serde(default = "default_max_concurrent_connections")]
37 pub max_concurrent_connections: usize,
38
39 #[serde(default = "default_request_timeout_seconds")]
41 pub request_timeout_seconds: u64,
42
43 #[serde(default = "default_retry_attempts")]
45 pub retry_attempts: u32,
46
47 #[serde(default)]
49 pub startup_timeout_seconds: Option<u64>,
50
51 #[serde(default)]
53 pub tool_timeout_seconds: Option<u64>,
54
55 #[serde(default = "default_experimental_use_rmcp_client")]
57 pub experimental_use_rmcp_client: bool,
58
59 #[serde(default = "default_connection_pooling_enabled")]
61 pub connection_pooling_enabled: bool,
62
63 #[serde(default = "default_tool_cache_capacity")]
65 pub tool_cache_capacity: usize,
66
67 #[serde(default = "default_connection_timeout_seconds")]
69 pub connection_timeout_seconds: u64,
70
71 #[serde(default)]
73 pub security: McpSecurityConfig,
74
75 #[serde(default)]
77 pub lifecycle: McpLifecycleConfig,
78}
79
80#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
82#[derive(Debug, Clone, Deserialize, Serialize)]
83pub struct McpSecurityConfig {
84 #[serde(default = "default_mcp_auth_enabled")]
86 pub auth_enabled: bool,
87
88 #[serde(default)]
90 pub api_key_env: Option<String>,
91
92 #[serde(default)]
94 pub rate_limit: McpRateLimitConfig,
95
96 #[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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
114#[derive(Debug, Clone, Deserialize, Serialize)]
115pub struct McpRateLimitConfig {
116 #[serde(default = "default_requests_per_minute")]
118 pub requests_per_minute: u32,
119
120 #[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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
136#[derive(Debug, Clone, Deserialize, Serialize)]
137pub struct McpValidationConfig {
138 #[serde(default = "default_schema_validation_enabled")]
140 pub schema_validation_enabled: bool,
141
142 #[serde(default = "default_path_traversal_protection_enabled")]
144 pub path_traversal_protection: bool,
145
146 #[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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
187#[derive(Debug, Clone, Deserialize, Serialize)]
188pub struct McpLifecycleConfig {
189 #[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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
204#[derive(Debug, Clone, Deserialize, Serialize)]
205pub struct McpRequirementsConfig {
206 #[serde(default = "default_mcp_requirements_enforce")]
208 pub enforce: bool,
209
210 #[serde(default)]
212 pub allowed_stdio_commands: Vec<String>,
213
214 #[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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
231#[derive(Debug, Clone, Deserialize, Serialize)]
232pub struct McpUiConfig {
233 #[serde(default = "default_mcp_ui_mode")]
235 pub mode: McpUiMode,
236
237 #[serde(default = "default_max_mcp_events")]
239 pub max_events: usize,
240
241 #[serde(default = "default_show_provider_names")]
243 pub show_provider_names: bool,
244
245 #[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 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 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#[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 #[default]
298 Compact,
299 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#[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,
319 SequentialThinking,
321}
322
323#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
325#[derive(Debug, Clone, Deserialize, Serialize)]
326pub struct McpProviderConfig {
327 pub name: String,
329
330 #[serde(flatten)]
332 pub transport: McpTransportConfig,
333
334 #[serde(default)]
336 #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, String>"))]
337 pub env: HashMap<String, String>,
338
339 #[serde(default = "default_provider_enabled")]
341 pub enabled: bool,
342
343 #[serde(default = "default_provider_max_concurrent")]
345 pub max_concurrent_requests: usize,
346
347 #[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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
367#[derive(Debug, Clone, Deserialize, Serialize)]
368pub struct McpAllowListConfig {
369 #[serde(default = "default_allowlist_enforced")]
371 pub enforce: bool,
372
373 #[serde(default)]
375 pub default: McpAllowListRules,
376
377 #[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 pub fn is_tool_allowed(&self, provider: &str, tool_name: &str) -> bool {
395 if !self.enforce {
396 return true;
397 }
398
399 self.resolve_match(provider, tool_name, |rules| &rules.tools)
400 }
401
402 pub fn is_resource_allowed(&self, provider: &str, resource: &str) -> bool {
404 if !self.enforce {
405 return true;
406 }
407
408 self.resolve_match(provider, resource, |rules| &rules.resources)
409 }
410
411 pub fn is_prompt_allowed(&self, provider: &str, prompt: &str) -> bool {
413 if !self.enforce {
414 return true;
415 }
416
417 self.resolve_match(provider, prompt, |rules| &rules.prompts)
418 }
419
420 pub fn is_logging_channel_allowed(&self, provider: Option<&str>, channel: &str) -> bool {
422 if !self.enforce {
423 return true;
424 }
425
426 if let Some(name) = provider
427 && let Some(rules) = self.providers.get(name)
428 && let Some(patterns) = &rules.logging
429 {
430 return pattern_matches(patterns, channel);
431 }
432
433 if let Some(patterns) = &self.default.logging
434 && pattern_matches(patterns, channel)
435 {
436 return true;
437 }
438
439 false
440 }
441
442 pub fn is_configuration_allowed(
444 &self,
445 provider: Option<&str>,
446 category: &str,
447 key: &str,
448 ) -> bool {
449 if !self.enforce {
450 return true;
451 }
452
453 if let Some(name) = provider
454 && let Some(rules) = self.providers.get(name)
455 && let Some(result) = configuration_allowed(rules, category, key)
456 {
457 return result;
458 }
459
460 if let Some(result) = configuration_allowed(&self.default, category, key) {
461 return result;
462 }
463
464 false
465 }
466
467 fn resolve_match<'a, F>(&'a self, provider: &str, candidate: &str, accessor: F) -> bool
468 where
469 F: Fn(&'a McpAllowListRules) -> &'a Option<Vec<String>>,
470 {
471 if let Some(rules) = self.providers.get(provider)
472 && let Some(patterns) = accessor(rules)
473 {
474 return pattern_matches(patterns, candidate);
475 }
476
477 if let Some(patterns) = accessor(&self.default)
478 && pattern_matches(patterns, candidate)
479 {
480 return true;
481 }
482
483 false
484 }
485}
486
487fn configuration_allowed(rules: &McpAllowListRules, category: &str, key: &str) -> Option<bool> {
488 rules.configuration.as_ref().and_then(|entries| {
489 entries
490 .get(category)
491 .map(|patterns| pattern_matches(patterns, key))
492 })
493}
494
495fn pattern_matches(patterns: &[String], candidate: &str) -> bool {
496 patterns
497 .iter()
498 .any(|pattern| wildcard_match(pattern, candidate))
499}
500
501fn wildcard_match(pattern: &str, candidate: &str) -> bool {
502 if pattern == "*" {
503 return true;
504 }
505
506 let mut regex_pattern = String::from("^");
507 let mut literal_buffer = String::new();
508
509 for ch in pattern.chars() {
510 match ch {
511 '*' => {
512 if !literal_buffer.is_empty() {
513 regex_pattern.push_str(®ex::escape(&literal_buffer));
514 literal_buffer.clear();
515 }
516 regex_pattern.push_str(".*");
517 }
518 '?' => {
519 if !literal_buffer.is_empty() {
520 regex_pattern.push_str(®ex::escape(&literal_buffer));
521 literal_buffer.clear();
522 }
523 regex_pattern.push('.');
524 }
525 _ => literal_buffer.push(ch),
526 }
527 }
528
529 if !literal_buffer.is_empty() {
530 regex_pattern.push_str(®ex::escape(&literal_buffer));
531 }
532
533 regex_pattern.push('$');
534
535 Regex::new(®ex_pattern)
536 .map(|regex| regex.is_match(candidate))
537 .unwrap_or(false)
538}
539
540#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
542#[derive(Debug, Clone, Deserialize, Serialize, Default)]
543pub struct McpAllowListRules {
544 #[serde(default)]
546 pub tools: Option<Vec<String>>,
547
548 #[serde(default)]
550 pub resources: Option<Vec<String>>,
551
552 #[serde(default)]
554 pub prompts: Option<Vec<String>>,
555
556 #[serde(default)]
558 pub logging: Option<Vec<String>>,
559
560 #[serde(default)]
562 pub configuration: Option<BTreeMap<String, Vec<String>>>,
563}
564
565#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
567#[derive(Debug, Clone, Deserialize, Serialize)]
568pub struct McpServerConfig {
569 #[serde(default = "default_mcp_server_enabled")]
571 pub enabled: bool,
572
573 #[serde(default = "default_mcp_server_bind")]
575 pub bind_address: String,
576
577 #[serde(default = "default_mcp_server_port")]
579 pub port: u16,
580
581 #[serde(default = "default_mcp_server_transport")]
583 pub transport: McpServerTransport,
584
585 #[serde(default = "default_mcp_server_name")]
587 pub name: String,
588
589 #[serde(default = "default_mcp_server_version")]
591 pub version: String,
592
593 #[serde(default)]
595 pub exposed_tools: Vec<String>,
596}
597
598impl Default for McpServerConfig {
599 fn default() -> Self {
600 Self {
601 enabled: default_mcp_server_enabled(),
602 bind_address: default_mcp_server_bind(),
603 port: default_mcp_server_port(),
604 transport: default_mcp_server_transport(),
605 name: default_mcp_server_name(),
606 version: default_mcp_server_version(),
607 exposed_tools: Vec::new(),
608 }
609 }
610}
611
612#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
614#[derive(Debug, Clone, Deserialize, Serialize)]
615#[serde(rename_all = "snake_case")]
616#[derive(Default)]
617pub enum McpServerTransport {
618 #[default]
620 Sse,
621 Http,
623}
624
625#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
627#[allow(clippy::large_enum_variant)]
628#[derive(Debug, Clone, Deserialize, Serialize)]
629#[serde(untagged)]
630pub enum McpTransportConfig {
631 Stdio(McpStdioServerConfig),
633 Http(McpHttpServerConfig),
635}
636
637#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
639#[derive(Debug, Clone, Deserialize, Serialize, Default)]
640pub struct McpStdioServerConfig {
641 pub command: String,
643
644 pub args: Vec<String>,
646
647 #[serde(default)]
649 pub working_directory: Option<String>,
650}
651
652#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
658#[derive(Debug, Clone, Deserialize, Serialize)]
659pub struct McpHttpServerConfig {
660 pub endpoint: String,
662
663 #[serde(default)]
665 pub api_key_env: Option<String>,
666
667 #[serde(default)]
669 pub oauth: Option<McpOAuthConfig>,
670
671 #[serde(default = "default_mcp_protocol_version")]
673 pub protocol_version: String,
674
675 #[serde(default, alias = "headers")]
677 #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, String>"))]
678 pub http_headers: HashMap<String, String>,
679
680 #[serde(default)]
683 #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, String>"))]
684 pub env_http_headers: HashMap<String, String>,
685}
686
687impl Default for McpHttpServerConfig {
688 fn default() -> Self {
689 Self {
690 endpoint: String::new(),
691 api_key_env: None,
692 oauth: None,
693 protocol_version: default_mcp_protocol_version(),
694 http_headers: HashMap::new(),
695 env_http_headers: HashMap::new(),
696 }
697 }
698}
699
700fn default_mcp_enabled() -> bool {
702 false
703}
704
705fn default_mcp_ui_mode() -> McpUiMode {
706 McpUiMode::Compact
707}
708
709fn default_max_mcp_events() -> usize {
710 50
711}
712
713fn default_show_provider_names() -> bool {
714 true
715}
716
717fn default_max_concurrent_connections() -> usize {
718 5
719}
720
721fn default_request_timeout_seconds() -> u64 {
722 30
723}
724
725fn default_retry_attempts() -> u32 {
726 3
727}
728
729fn default_experimental_use_rmcp_client() -> bool {
730 true
731}
732
733fn default_provider_enabled() -> bool {
734 true
735}
736
737fn default_provider_max_concurrent() -> usize {
738 3
739}
740
741fn default_allowlist_enforced() -> bool {
742 false
743}
744
745fn default_mcp_protocol_version() -> String {
746 "2024-11-05".into()
747}
748
749fn default_mcp_server_enabled() -> bool {
750 false
751}
752
753fn default_connection_pooling_enabled() -> bool {
754 true
755}
756
757fn default_tool_cache_capacity() -> usize {
758 100
759}
760
761fn default_connection_timeout_seconds() -> u64 {
762 30
763}
764
765fn default_allow_model_lifecycle_control() -> bool {
766 false
767}
768
769fn default_mcp_server_bind() -> String {
770 "127.0.0.1".into()
771}
772
773fn default_mcp_server_port() -> u16 {
774 3000
775}
776
777fn default_mcp_server_transport() -> McpServerTransport {
778 McpServerTransport::Sse
779}
780
781fn default_mcp_server_name() -> String {
782 "vtcode-mcp-server".into()
783}
784
785fn default_mcp_server_version() -> String {
786 env!("CARGO_PKG_VERSION").into()
787}
788
789fn normalize_mcp_identifier(value: &str) -> String {
790 value
791 .chars()
792 .filter(|ch| ch.is_ascii_alphanumeric())
793 .map(|ch| ch.to_ascii_lowercase())
794 .collect()
795}
796
797fn default_mcp_auth_enabled() -> bool {
798 false
799}
800
801fn default_requests_per_minute() -> u32 {
802 100
803}
804
805fn default_concurrent_requests() -> u32 {
806 10
807}
808
809fn default_schema_validation_enabled() -> bool {
810 true
811}
812
813fn default_path_traversal_protection_enabled() -> bool {
814 true
815}
816
817fn default_max_argument_size() -> u32 {
818 1024 * 1024 }
820
821fn default_mcp_requirements_enforce() -> bool {
822 false
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828 use crate::constants::mcp as mcp_constants;
829 use std::collections::BTreeMap;
830
831 #[test]
832 fn test_mcp_config_defaults() {
833 let config = McpClientConfig::default();
834 assert!(!config.enabled);
835 assert_eq!(config.ui.mode, McpUiMode::Compact);
836 assert_eq!(config.ui.max_events, 50);
837 assert!(config.ui.show_provider_names);
838 assert!(config.ui.renderers.is_empty());
839 assert_eq!(config.max_concurrent_connections, 5);
840 assert_eq!(config.request_timeout_seconds, 30);
841 assert_eq!(config.retry_attempts, 3);
842 assert!(!config.lifecycle.allow_model_control);
843 assert!(config.providers.is_empty());
844 assert!(!config.requirements.enforce);
845 assert!(config.requirements.allowed_stdio_commands.is_empty());
846 assert!(config.requirements.allowed_http_endpoints.is_empty());
847 assert!(!config.server.enabled);
848 assert!(!config.allowlist.enforce);
849 assert!(config.allowlist.default.tools.is_none());
850 }
851
852 #[test]
853 fn test_allowlist_pattern_matching() {
854 let patterns = vec!["get_*".to_string(), "convert_timezone".to_string()];
855 assert!(pattern_matches(&patterns, "get_current_time"));
856 assert!(pattern_matches(&patterns, "convert_timezone"));
857 assert!(!pattern_matches(&patterns, "delete_timezone"));
858 }
859
860 #[test]
861 fn test_allowlist_provider_override() {
862 let mut config = McpAllowListConfig {
863 enforce: true,
864 default: McpAllowListRules {
865 tools: Some(vec!["get_*".to_string()]),
866 ..Default::default()
867 },
868 ..Default::default()
869 };
870
871 let provider_rules = McpAllowListRules {
872 tools: Some(vec!["list_*".to_string()]),
873 ..Default::default()
874 };
875 config
876 .providers
877 .insert("context7".to_string(), provider_rules);
878
879 assert!(config.is_tool_allowed("context7", "list_documents"));
880 assert!(!config.is_tool_allowed("context7", "get_current_time"));
881 assert!(config.is_tool_allowed("other", "get_timezone"));
882 assert!(!config.is_tool_allowed("other", "list_documents"));
883 }
884
885 #[test]
886 fn test_allowlist_configuration_rules() {
887 let mut config = McpAllowListConfig {
888 enforce: true,
889 default: McpAllowListRules {
890 configuration: Some(BTreeMap::from([(
891 "ui".to_string(),
892 vec!["mode".to_string(), "max_events".to_string()],
893 )])),
894 ..Default::default()
895 },
896 ..Default::default()
897 };
898
899 let provider_rules = McpAllowListRules {
900 configuration: Some(BTreeMap::from([(
901 "provider".to_string(),
902 vec!["max_concurrent_requests".to_string()],
903 )])),
904 ..Default::default()
905 };
906 config.providers.insert("time".to_string(), provider_rules);
907
908 assert!(config.is_configuration_allowed(None, "ui", "mode"));
909 assert!(!config.is_configuration_allowed(None, "ui", "show_provider_names"));
910 assert!(config.is_configuration_allowed(
911 Some("time"),
912 "provider",
913 "max_concurrent_requests"
914 ));
915 assert!(!config.is_configuration_allowed(Some("time"), "provider", "retry_attempts"));
916 }
917
918 #[test]
919 fn test_allowlist_resource_override() {
920 let mut config = McpAllowListConfig {
921 enforce: true,
922 default: McpAllowListRules {
923 resources: Some(vec!["docs/**/*".to_string()]),
924 ..Default::default()
925 },
926 ..Default::default()
927 };
928
929 let provider_rules = McpAllowListRules {
930 resources: Some(vec!["journals/*".to_string()]),
931 ..Default::default()
932 };
933 config
934 .providers
935 .insert("context7".to_string(), provider_rules);
936
937 assert!(config.is_resource_allowed("context7", "journals/2024"));
938 assert!(config.is_resource_allowed("other", "docs/config/config.md"));
939 assert!(config.is_resource_allowed("other", "docs/guides/zed-acp.md"));
940 assert!(!config.is_resource_allowed("other", "journals/2023"));
941 }
942
943 #[test]
944 fn test_allowlist_logging_override() {
945 let mut config = McpAllowListConfig {
946 enforce: true,
947 default: McpAllowListRules {
948 logging: Some(vec!["info".to_string(), "debug".to_string()]),
949 ..Default::default()
950 },
951 ..Default::default()
952 };
953
954 let provider_rules = McpAllowListRules {
955 logging: Some(vec!["audit".to_string()]),
956 ..Default::default()
957 };
958 config
959 .providers
960 .insert("sequential".to_string(), provider_rules);
961
962 assert!(config.is_logging_channel_allowed(Some("sequential"), "audit"));
963 assert!(!config.is_logging_channel_allowed(Some("sequential"), "info"));
964 assert!(config.is_logging_channel_allowed(Some("other"), "info"));
965 assert!(!config.is_logging_channel_allowed(Some("other"), "trace"));
966 }
967
968 #[test]
969 fn test_mcp_ui_renderer_resolution() {
970 let mut config = McpUiConfig::default();
971 config.renderers.insert(
972 mcp_constants::RENDERER_CONTEXT7.to_string(),
973 McpRendererProfile::Context7,
974 );
975 config.renderers.insert(
976 mcp_constants::RENDERER_SEQUENTIAL_THINKING.to_string(),
977 McpRendererProfile::SequentialThinking,
978 );
979
980 assert_eq!(
981 config.renderer_for_tool("mcp_context7_lookup"),
982 Some(McpRendererProfile::Context7)
983 );
984 assert_eq!(
985 config.renderer_for_tool("mcp_context7lookup"),
986 Some(McpRendererProfile::Context7)
987 );
988 assert_eq!(
989 config.renderer_for_tool("mcp_sequentialthinking_run"),
990 Some(McpRendererProfile::SequentialThinking)
991 );
992 assert_eq!(
993 config.renderer_for_identifier("sequential-thinking-analyze"),
994 Some(McpRendererProfile::SequentialThinking)
995 );
996 assert_eq!(config.renderer_for_tool("mcp_unknown"), None);
997 }
998}