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
76#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
78#[derive(Debug, Clone, Deserialize, Serialize)]
79pub struct McpSecurityConfig {
80 #[serde(default = "default_mcp_auth_enabled")]
82 pub auth_enabled: bool,
83
84 #[serde(default)]
86 pub api_key_env: Option<String>,
87
88 #[serde(default)]
90 pub rate_limit: McpRateLimitConfig,
91
92 #[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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
110#[derive(Debug, Clone, Deserialize, Serialize)]
111pub struct McpRateLimitConfig {
112 #[serde(default = "default_requests_per_minute")]
114 pub requests_per_minute: u32,
115
116 #[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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
132#[derive(Debug, Clone, Deserialize, Serialize)]
133pub struct McpValidationConfig {
134 #[serde(default = "default_schema_validation_enabled")]
136 pub schema_validation_enabled: bool,
137
138 #[serde(default = "default_path_traversal_protection_enabled")]
140 pub path_traversal_protection: bool,
141
142 #[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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
182#[derive(Debug, Clone, Deserialize, Serialize)]
183pub struct McpRequirementsConfig {
184 #[serde(default = "default_mcp_requirements_enforce")]
186 pub enforce: bool,
187
188 #[serde(default)]
190 pub allowed_stdio_commands: Vec<String>,
191
192 #[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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
209#[derive(Debug, Clone, Deserialize, Serialize)]
210pub struct McpUiConfig {
211 #[serde(default = "default_mcp_ui_mode")]
213 pub mode: McpUiMode,
214
215 #[serde(default = "default_max_mcp_events")]
217 pub max_events: usize,
218
219 #[serde(default = "default_show_provider_names")]
221 pub show_provider_names: bool,
222
223 #[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 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 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#[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 #[default]
276 Compact,
277 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#[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,
297 SequentialThinking,
299}
300
301#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
303#[derive(Debug, Clone, Deserialize, Serialize)]
304pub struct McpProviderConfig {
305 pub name: String,
307
308 #[serde(flatten)]
310 pub transport: McpTransportConfig,
311
312 #[serde(default)]
314 #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, String>"))]
315 pub env: HashMap<String, String>,
316
317 #[serde(default = "default_provider_enabled")]
319 pub enabled: bool,
320
321 #[serde(default = "default_provider_max_concurrent")]
323 pub max_concurrent_requests: usize,
324
325 #[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#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
345#[derive(Debug, Clone, Deserialize, Serialize)]
346pub struct McpAllowListConfig {
347 #[serde(default = "default_allowlist_enforced")]
349 pub enforce: bool,
350
351 #[serde(default)]
353 pub default: McpAllowListRules,
354
355 #[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 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 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 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 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 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(®ex::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(®ex::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(®ex::escape(&literal_buffer));
509 }
510
511 regex_pattern.push('$');
512
513 Regex::new(®ex_pattern)
514 .map(|regex| regex.is_match(candidate))
515 .unwrap_or(false)
516}
517
518#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
520#[derive(Debug, Clone, Deserialize, Serialize, Default)]
521pub struct McpAllowListRules {
522 #[serde(default)]
524 pub tools: Option<Vec<String>>,
525
526 #[serde(default)]
528 pub resources: Option<Vec<String>>,
529
530 #[serde(default)]
532 pub prompts: Option<Vec<String>>,
533
534 #[serde(default)]
536 pub logging: Option<Vec<String>>,
537
538 #[serde(default)]
540 pub configuration: Option<BTreeMap<String, Vec<String>>>,
541}
542
543#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
545#[derive(Debug, Clone, Deserialize, Serialize)]
546pub struct McpServerConfig {
547 #[serde(default = "default_mcp_server_enabled")]
549 pub enabled: bool,
550
551 #[serde(default = "default_mcp_server_bind")]
553 pub bind_address: String,
554
555 #[serde(default = "default_mcp_server_port")]
557 pub port: u16,
558
559 #[serde(default = "default_mcp_server_transport")]
561 pub transport: McpServerTransport,
562
563 #[serde(default = "default_mcp_server_name")]
565 pub name: String,
566
567 #[serde(default = "default_mcp_server_version")]
569 pub version: String,
570
571 #[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#[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 #[default]
598 Sse,
599 Http,
601}
602
603#[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 Stdio(McpStdioServerConfig),
611 Http(McpHttpServerConfig),
613}
614
615#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
617#[derive(Debug, Clone, Deserialize, Serialize, Default)]
618pub struct McpStdioServerConfig {
619 pub command: String,
621
622 pub args: Vec<String>,
624
625 #[serde(default)]
627 pub working_directory: Option<String>,
628}
629
630#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
636#[derive(Debug, Clone, Deserialize, Serialize)]
637pub struct McpHttpServerConfig {
638 pub endpoint: String,
640
641 #[serde(default)]
643 pub api_key_env: Option<String>,
644
645 #[serde(default)]
647 pub oauth: Option<McpOAuthConfig>,
648
649 #[serde(default = "default_mcp_protocol_version")]
651 pub protocol_version: String,
652
653 #[serde(default, alias = "headers")]
655 #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, String>"))]
656 pub http_headers: HashMap<String, String>,
657
658 #[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
678fn 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 }
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}