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