1use hashbrown::HashMap;
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
8#[derive(Debug, Clone, Deserialize, Serialize)]
9pub struct McpClientConfig {
10 #[serde(default = "default_mcp_enabled")]
12 pub enabled: bool,
13
14 #[serde(default)]
16 pub ui: McpUiConfig,
17
18 #[serde(default)]
20 pub providers: Vec<McpProviderConfig>,
21
22 #[serde(default)]
24 pub requirements: McpRequirementsConfig,
25
26 #[serde(default)]
28 pub server: McpServerConfig,
29
30 #[serde(default)]
32 pub allowlist: McpAllowListConfig,
33
34 #[serde(default = "default_max_concurrent_connections")]
36 pub max_concurrent_connections: usize,
37
38 #[serde(default = "default_request_timeout_seconds")]
40 pub request_timeout_seconds: u64,
41
42 #[serde(default = "default_retry_attempts")]
44 pub retry_attempts: u32,
45
46 #[serde(default)]
48 pub startup_timeout_seconds: Option<u64>,
49
50 #[serde(default)]
52 pub tool_timeout_seconds: Option<u64>,
53
54 #[serde(default = "default_experimental_use_rmcp_client")]
56 pub experimental_use_rmcp_client: bool,
57
58 #[serde(default = "default_connection_pooling_enabled")]
60 pub connection_pooling_enabled: bool,
61
62 #[serde(default = "default_tool_cache_capacity")]
64 pub tool_cache_capacity: usize,
65
66 #[serde(default = "default_connection_timeout_seconds")]
68 pub connection_timeout_seconds: u64,
69
70 #[serde(default)]
72 pub security: McpSecurityConfig,
73}
74
75#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
77#[derive(Debug, Clone, Deserialize, Serialize)]
78pub struct McpSecurityConfig {
79 #[serde(default = "default_mcp_auth_enabled")]
81 pub auth_enabled: bool,
82
83 #[serde(default)]
85 pub api_key_env: Option<String>,
86
87 #[serde(default)]
89 pub rate_limit: McpRateLimitConfig,
90
91 #[serde(default)]
93 pub validation: McpValidationConfig,
94}
95
96impl Default for McpSecurityConfig {
97 fn default() -> Self {
98 Self {
99 auth_enabled: default_mcp_auth_enabled(),
100 api_key_env: None,
101 rate_limit: McpRateLimitConfig::default(),
102 validation: McpValidationConfig::default(),
103 }
104 }
105}
106
107#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
109#[derive(Debug, Clone, Deserialize, Serialize)]
110pub struct McpRateLimitConfig {
111 #[serde(default = "default_requests_per_minute")]
113 pub requests_per_minute: u32,
114
115 #[serde(default = "default_concurrent_requests")]
117 pub concurrent_requests: u32,
118}
119
120impl Default for McpRateLimitConfig {
121 fn default() -> Self {
122 Self {
123 requests_per_minute: default_requests_per_minute(),
124 concurrent_requests: default_concurrent_requests(),
125 }
126 }
127}
128
129#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
131#[derive(Debug, Clone, Deserialize, Serialize)]
132pub struct McpValidationConfig {
133 #[serde(default = "default_schema_validation_enabled")]
135 pub schema_validation_enabled: bool,
136
137 #[serde(default = "default_path_traversal_protection_enabled")]
139 pub path_traversal_protection: bool,
140
141 #[serde(default = "default_max_argument_size")]
143 pub max_argument_size: u32,
144}
145
146impl Default for McpValidationConfig {
147 fn default() -> Self {
148 Self {
149 schema_validation_enabled: default_schema_validation_enabled(),
150 path_traversal_protection: default_path_traversal_protection_enabled(),
151 max_argument_size: default_max_argument_size(),
152 }
153 }
154}
155
156impl Default for McpClientConfig {
157 fn default() -> Self {
158 Self {
159 enabled: default_mcp_enabled(),
160 ui: McpUiConfig::default(),
161 providers: Vec::new(),
162 requirements: McpRequirementsConfig::default(),
163 server: McpServerConfig::default(),
164 allowlist: McpAllowListConfig::default(),
165 max_concurrent_connections: default_max_concurrent_connections(),
166 request_timeout_seconds: default_request_timeout_seconds(),
167 retry_attempts: default_retry_attempts(),
168 startup_timeout_seconds: None,
169 tool_timeout_seconds: None,
170 experimental_use_rmcp_client: default_experimental_use_rmcp_client(),
171 security: McpSecurityConfig::default(),
172 connection_pooling_enabled: default_connection_pooling_enabled(),
173 connection_timeout_seconds: default_connection_timeout_seconds(),
174 tool_cache_capacity: default_tool_cache_capacity(),
175 }
176 }
177}
178
179#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
181#[derive(Debug, Clone, Deserialize, Serialize)]
182pub struct McpRequirementsConfig {
183 #[serde(default = "default_mcp_requirements_enforce")]
185 pub enforce: bool,
186
187 #[serde(default)]
189 pub allowed_stdio_commands: Vec<String>,
190
191 #[serde(default)]
193 pub allowed_http_endpoints: Vec<String>,
194}
195
196impl Default for McpRequirementsConfig {
197 fn default() -> Self {
198 Self {
199 enforce: default_mcp_requirements_enforce(),
200 allowed_stdio_commands: Vec::new(),
201 allowed_http_endpoints: Vec::new(),
202 }
203 }
204}
205
206#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
208#[derive(Debug, Clone, Deserialize, Serialize)]
209pub struct McpUiConfig {
210 #[serde(default = "default_mcp_ui_mode")]
212 pub mode: McpUiMode,
213
214 #[serde(default = "default_max_mcp_events")]
216 pub max_events: usize,
217
218 #[serde(default = "default_show_provider_names")]
220 pub show_provider_names: bool,
221
222 #[serde(default)]
224 #[cfg_attr(
225 feature = "schema",
226 schemars(with = "BTreeMap<String, McpRendererProfile>")
227 )]
228 pub renderers: HashMap<String, McpRendererProfile>,
229}
230
231impl Default for McpUiConfig {
232 fn default() -> Self {
233 Self {
234 mode: default_mcp_ui_mode(),
235 max_events: default_max_mcp_events(),
236 show_provider_names: default_show_provider_names(),
237 renderers: HashMap::new(),
238 }
239 }
240}
241
242impl McpUiConfig {
243 pub fn renderer_for_identifier(&self, identifier: &str) -> Option<McpRendererProfile> {
245 let normalized_identifier = normalize_mcp_identifier(identifier);
246 if normalized_identifier.is_empty() {
247 return None;
248 }
249
250 self.renderers.iter().find_map(|(key, profile)| {
251 let normalized_key = normalize_mcp_identifier(key);
252 if normalized_identifier.starts_with(&normalized_key) {
253 Some(*profile)
254 } else {
255 None
256 }
257 })
258 }
259
260 pub fn renderer_for_tool(&self, tool_name: &str) -> Option<McpRendererProfile> {
262 let identifier = tool_name.strip_prefix("mcp_").unwrap_or(tool_name);
263 self.renderer_for_identifier(identifier)
264 }
265}
266
267#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
270#[serde(rename_all = "snake_case")]
271#[derive(Default)]
272pub enum McpUiMode {
273 #[default]
275 Compact,
276 Full,
278}
279
280impl std::fmt::Display for McpUiMode {
281 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282 match self {
283 McpUiMode::Compact => write!(f, "compact"),
284 McpUiMode::Full => write!(f, "full"),
285 }
286 }
287}
288
289#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
292#[serde(rename_all = "kebab-case")]
293pub enum McpRendererProfile {
294 Context7,
296 SequentialThinking,
298}
299
300#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
302#[derive(Debug, Clone, Deserialize, Serialize)]
303pub struct McpProviderConfig {
304 pub name: String,
306
307 #[serde(flatten)]
309 pub transport: McpTransportConfig,
310
311 #[serde(default)]
313 #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, String>"))]
314 pub env: HashMap<String, String>,
315
316 #[serde(default = "default_provider_enabled")]
318 pub enabled: bool,
319
320 #[serde(default = "default_provider_max_concurrent")]
322 pub max_concurrent_requests: usize,
323
324 #[serde(default)]
326 pub startup_timeout_ms: Option<u64>,
327}
328
329impl Default for McpProviderConfig {
330 fn default() -> Self {
331 Self {
332 name: String::new(),
333 transport: McpTransportConfig::Stdio(McpStdioServerConfig::default()),
334 env: HashMap::new(),
335 enabled: default_provider_enabled(),
336 max_concurrent_requests: default_provider_max_concurrent(),
337 startup_timeout_ms: None,
338 }
339 }
340}
341
342#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
344#[derive(Debug, Clone, Deserialize, Serialize)]
345pub struct McpAllowListConfig {
346 #[serde(default = "default_allowlist_enforced")]
348 pub enforce: bool,
349
350 #[serde(default)]
352 pub default: McpAllowListRules,
353
354 #[serde(default)]
356 pub providers: BTreeMap<String, McpAllowListRules>,
357}
358
359impl Default for McpAllowListConfig {
360 fn default() -> Self {
361 Self {
362 enforce: default_allowlist_enforced(),
363 default: McpAllowListRules::default(),
364 providers: BTreeMap::new(),
365 }
366 }
367}
368
369impl McpAllowListConfig {
370 pub fn is_tool_allowed(&self, provider: &str, tool_name: &str) -> bool {
372 if !self.enforce {
373 return true;
374 }
375
376 self.resolve_match(provider, tool_name, |rules| &rules.tools)
377 }
378
379 pub fn is_resource_allowed(&self, provider: &str, resource: &str) -> bool {
381 if !self.enforce {
382 return true;
383 }
384
385 self.resolve_match(provider, resource, |rules| &rules.resources)
386 }
387
388 pub fn is_prompt_allowed(&self, provider: &str, prompt: &str) -> bool {
390 if !self.enforce {
391 return true;
392 }
393
394 self.resolve_match(provider, prompt, |rules| &rules.prompts)
395 }
396
397 pub fn is_logging_channel_allowed(&self, provider: Option<&str>, channel: &str) -> bool {
399 if !self.enforce {
400 return true;
401 }
402
403 if let Some(name) = provider
404 && let Some(rules) = self.providers.get(name)
405 && let Some(patterns) = &rules.logging
406 {
407 return pattern_matches(patterns, channel);
408 }
409
410 if let Some(patterns) = &self.default.logging
411 && pattern_matches(patterns, channel)
412 {
413 return true;
414 }
415
416 false
417 }
418
419 pub fn is_configuration_allowed(
421 &self,
422 provider: Option<&str>,
423 category: &str,
424 key: &str,
425 ) -> bool {
426 if !self.enforce {
427 return true;
428 }
429
430 if let Some(name) = provider
431 && let Some(rules) = self.providers.get(name)
432 && let Some(result) = configuration_allowed(rules, category, key)
433 {
434 return result;
435 }
436
437 if let Some(result) = configuration_allowed(&self.default, category, key) {
438 return result;
439 }
440
441 false
442 }
443
444 fn resolve_match<'a, F>(&'a self, provider: &str, candidate: &str, accessor: F) -> bool
445 where
446 F: Fn(&'a McpAllowListRules) -> &'a Option<Vec<String>>,
447 {
448 if let Some(rules) = self.providers.get(provider)
449 && let Some(patterns) = accessor(rules)
450 {
451 return pattern_matches(patterns, candidate);
452 }
453
454 if let Some(patterns) = accessor(&self.default)
455 && pattern_matches(patterns, candidate)
456 {
457 return true;
458 }
459
460 false
461 }
462}
463
464fn configuration_allowed(rules: &McpAllowListRules, category: &str, key: &str) -> Option<bool> {
465 rules.configuration.as_ref().and_then(|entries| {
466 entries
467 .get(category)
468 .map(|patterns| pattern_matches(patterns, key))
469 })
470}
471
472fn pattern_matches(patterns: &[String], candidate: &str) -> bool {
473 patterns
474 .iter()
475 .any(|pattern| wildcard_match(pattern, candidate))
476}
477
478fn wildcard_match(pattern: &str, candidate: &str) -> bool {
479 if pattern == "*" {
480 return true;
481 }
482
483 let mut regex_pattern = String::from("^");
484 let mut literal_buffer = String::new();
485
486 for ch in pattern.chars() {
487 match ch {
488 '*' => {
489 if !literal_buffer.is_empty() {
490 regex_pattern.push_str(®ex::escape(&literal_buffer));
491 literal_buffer.clear();
492 }
493 regex_pattern.push_str(".*");
494 }
495 '?' => {
496 if !literal_buffer.is_empty() {
497 regex_pattern.push_str(®ex::escape(&literal_buffer));
498 literal_buffer.clear();
499 }
500 regex_pattern.push('.');
501 }
502 _ => literal_buffer.push(ch),
503 }
504 }
505
506 if !literal_buffer.is_empty() {
507 regex_pattern.push_str(®ex::escape(&literal_buffer));
508 }
509
510 regex_pattern.push('$');
511
512 Regex::new(®ex_pattern)
513 .map(|regex| regex.is_match(candidate))
514 .unwrap_or(false)
515}
516
517#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
519#[derive(Debug, Clone, Deserialize, Serialize, Default)]
520pub struct McpAllowListRules {
521 #[serde(default)]
523 pub tools: Option<Vec<String>>,
524
525 #[serde(default)]
527 pub resources: Option<Vec<String>>,
528
529 #[serde(default)]
531 pub prompts: Option<Vec<String>>,
532
533 #[serde(default)]
535 pub logging: Option<Vec<String>>,
536
537 #[serde(default)]
539 pub configuration: Option<BTreeMap<String, Vec<String>>>,
540}
541
542#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
544#[derive(Debug, Clone, Deserialize, Serialize)]
545pub struct McpServerConfig {
546 #[serde(default = "default_mcp_server_enabled")]
548 pub enabled: bool,
549
550 #[serde(default = "default_mcp_server_bind")]
552 pub bind_address: String,
553
554 #[serde(default = "default_mcp_server_port")]
556 pub port: u16,
557
558 #[serde(default = "default_mcp_server_transport")]
560 pub transport: McpServerTransport,
561
562 #[serde(default = "default_mcp_server_name")]
564 pub name: String,
565
566 #[serde(default = "default_mcp_server_version")]
568 pub version: String,
569
570 #[serde(default)]
572 pub exposed_tools: Vec<String>,
573}
574
575impl Default for McpServerConfig {
576 fn default() -> Self {
577 Self {
578 enabled: default_mcp_server_enabled(),
579 bind_address: default_mcp_server_bind(),
580 port: default_mcp_server_port(),
581 transport: default_mcp_server_transport(),
582 name: default_mcp_server_name(),
583 version: default_mcp_server_version(),
584 exposed_tools: Vec::new(),
585 }
586 }
587}
588
589#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
591#[derive(Debug, Clone, Deserialize, Serialize)]
592#[serde(rename_all = "snake_case")]
593#[derive(Default)]
594pub enum McpServerTransport {
595 #[default]
597 Sse,
598 Http,
600}
601
602#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
604#[derive(Debug, Clone, Deserialize, Serialize)]
605#[serde(untagged)]
606pub enum McpTransportConfig {
607 Stdio(McpStdioServerConfig),
609 Http(McpHttpServerConfig),
611}
612
613#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
615#[derive(Debug, Clone, Deserialize, Serialize, Default)]
616pub struct McpStdioServerConfig {
617 pub command: String,
619
620 pub args: Vec<String>,
622
623 #[serde(default)]
625 pub working_directory: Option<String>,
626}
627
628#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
634#[derive(Debug, Clone, Deserialize, Serialize)]
635pub struct McpHttpServerConfig {
636 pub endpoint: String,
638
639 #[serde(default)]
641 pub api_key_env: Option<String>,
642
643 #[serde(default = "default_mcp_protocol_version")]
645 pub protocol_version: String,
646
647 #[serde(default, alias = "headers")]
649 #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, String>"))]
650 pub http_headers: HashMap<String, String>,
651
652 #[serde(default)]
655 #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, String>"))]
656 pub env_http_headers: HashMap<String, String>,
657}
658
659impl Default for McpHttpServerConfig {
660 fn default() -> Self {
661 Self {
662 endpoint: String::new(),
663 api_key_env: None,
664 protocol_version: default_mcp_protocol_version(),
665 http_headers: HashMap::new(),
666 env_http_headers: HashMap::new(),
667 }
668 }
669}
670
671fn default_mcp_enabled() -> bool {
673 false
674}
675
676fn default_mcp_ui_mode() -> McpUiMode {
677 McpUiMode::Compact
678}
679
680fn default_max_mcp_events() -> usize {
681 50
682}
683
684fn default_show_provider_names() -> bool {
685 true
686}
687
688fn default_max_concurrent_connections() -> usize {
689 5
690}
691
692fn default_request_timeout_seconds() -> u64 {
693 30
694}
695
696fn default_retry_attempts() -> u32 {
697 3
698}
699
700fn default_experimental_use_rmcp_client() -> bool {
701 true
702}
703
704fn default_provider_enabled() -> bool {
705 true
706}
707
708fn default_provider_max_concurrent() -> usize {
709 3
710}
711
712fn default_allowlist_enforced() -> bool {
713 false
714}
715
716fn default_mcp_protocol_version() -> String {
717 "2024-11-05".into()
718}
719
720fn default_mcp_server_enabled() -> bool {
721 false
722}
723
724fn default_connection_pooling_enabled() -> bool {
725 true
726}
727
728fn default_tool_cache_capacity() -> usize {
729 100
730}
731
732fn default_connection_timeout_seconds() -> u64 {
733 30
734}
735
736fn default_mcp_server_bind() -> String {
737 "127.0.0.1".into()
738}
739
740fn default_mcp_server_port() -> u16 {
741 3000
742}
743
744fn default_mcp_server_transport() -> McpServerTransport {
745 McpServerTransport::Sse
746}
747
748fn default_mcp_server_name() -> String {
749 "vtcode-mcp-server".into()
750}
751
752fn default_mcp_server_version() -> String {
753 env!("CARGO_PKG_VERSION").into()
754}
755
756fn normalize_mcp_identifier(value: &str) -> String {
757 value
758 .chars()
759 .filter(|ch| ch.is_ascii_alphanumeric())
760 .map(|ch| ch.to_ascii_lowercase())
761 .collect()
762}
763
764fn default_mcp_auth_enabled() -> bool {
765 false
766}
767
768fn default_requests_per_minute() -> u32 {
769 100
770}
771
772fn default_concurrent_requests() -> u32 {
773 10
774}
775
776fn default_schema_validation_enabled() -> bool {
777 true
778}
779
780fn default_path_traversal_protection_enabled() -> bool {
781 true
782}
783
784fn default_max_argument_size() -> u32 {
785 1024 * 1024 }
787
788fn default_mcp_requirements_enforce() -> bool {
789 false
790}
791
792#[cfg(test)]
793mod tests {
794 use super::*;
795 use crate::constants::mcp as mcp_constants;
796 use std::collections::BTreeMap;
797
798 #[test]
799 fn test_mcp_config_defaults() {
800 let config = McpClientConfig::default();
801 assert!(!config.enabled);
802 assert_eq!(config.ui.mode, McpUiMode::Compact);
803 assert_eq!(config.ui.max_events, 50);
804 assert!(config.ui.show_provider_names);
805 assert!(config.ui.renderers.is_empty());
806 assert_eq!(config.max_concurrent_connections, 5);
807 assert_eq!(config.request_timeout_seconds, 30);
808 assert_eq!(config.retry_attempts, 3);
809 assert!(config.providers.is_empty());
810 assert!(!config.requirements.enforce);
811 assert!(config.requirements.allowed_stdio_commands.is_empty());
812 assert!(config.requirements.allowed_http_endpoints.is_empty());
813 assert!(!config.server.enabled);
814 assert!(!config.allowlist.enforce);
815 assert!(config.allowlist.default.tools.is_none());
816 }
817
818 #[test]
819 fn test_allowlist_pattern_matching() {
820 let patterns = vec!["get_*".to_string(), "convert_timezone".to_string()];
821 assert!(pattern_matches(&patterns, "get_current_time"));
822 assert!(pattern_matches(&patterns, "convert_timezone"));
823 assert!(!pattern_matches(&patterns, "delete_timezone"));
824 }
825
826 #[test]
827 fn test_allowlist_provider_override() {
828 let mut config = McpAllowListConfig {
829 enforce: true,
830 default: McpAllowListRules {
831 tools: Some(vec!["get_*".to_string()]),
832 ..Default::default()
833 },
834 ..Default::default()
835 };
836
837 let provider_rules = McpAllowListRules {
838 tools: Some(vec!["list_*".to_string()]),
839 ..Default::default()
840 };
841 config
842 .providers
843 .insert("context7".to_string(), provider_rules);
844
845 assert!(config.is_tool_allowed("context7", "list_documents"));
846 assert!(!config.is_tool_allowed("context7", "get_current_time"));
847 assert!(config.is_tool_allowed("other", "get_timezone"));
848 assert!(!config.is_tool_allowed("other", "list_documents"));
849 }
850
851 #[test]
852 fn test_allowlist_configuration_rules() {
853 let mut config = McpAllowListConfig {
854 enforce: true,
855 default: McpAllowListRules {
856 configuration: Some(BTreeMap::from([(
857 "ui".to_string(),
858 vec!["mode".to_string(), "max_events".to_string()],
859 )])),
860 ..Default::default()
861 },
862 ..Default::default()
863 };
864
865 let provider_rules = McpAllowListRules {
866 configuration: Some(BTreeMap::from([(
867 "provider".to_string(),
868 vec!["max_concurrent_requests".to_string()],
869 )])),
870 ..Default::default()
871 };
872 config.providers.insert("time".to_string(), provider_rules);
873
874 assert!(config.is_configuration_allowed(None, "ui", "mode"));
875 assert!(!config.is_configuration_allowed(None, "ui", "show_provider_names"));
876 assert!(config.is_configuration_allowed(
877 Some("time"),
878 "provider",
879 "max_concurrent_requests"
880 ));
881 assert!(!config.is_configuration_allowed(Some("time"), "provider", "retry_attempts"));
882 }
883
884 #[test]
885 fn test_allowlist_resource_override() {
886 let mut config = McpAllowListConfig {
887 enforce: true,
888 default: McpAllowListRules {
889 resources: Some(vec!["docs/**/*".to_string()]),
890 ..Default::default()
891 },
892 ..Default::default()
893 };
894
895 let provider_rules = McpAllowListRules {
896 resources: Some(vec!["journals/*".to_string()]),
897 ..Default::default()
898 };
899 config
900 .providers
901 .insert("context7".to_string(), provider_rules);
902
903 assert!(config.is_resource_allowed("context7", "journals/2024"));
904 assert!(config.is_resource_allowed("other", "docs/config/config.md"));
905 assert!(config.is_resource_allowed("other", "docs/guides/zed-acp.md"));
906 assert!(!config.is_resource_allowed("other", "journals/2023"));
907 }
908
909 #[test]
910 fn test_allowlist_logging_override() {
911 let mut config = McpAllowListConfig {
912 enforce: true,
913 default: McpAllowListRules {
914 logging: Some(vec!["info".to_string(), "debug".to_string()]),
915 ..Default::default()
916 },
917 ..Default::default()
918 };
919
920 let provider_rules = McpAllowListRules {
921 logging: Some(vec!["audit".to_string()]),
922 ..Default::default()
923 };
924 config
925 .providers
926 .insert("sequential".to_string(), provider_rules);
927
928 assert!(config.is_logging_channel_allowed(Some("sequential"), "audit"));
929 assert!(!config.is_logging_channel_allowed(Some("sequential"), "info"));
930 assert!(config.is_logging_channel_allowed(Some("other"), "info"));
931 assert!(!config.is_logging_channel_allowed(Some("other"), "trace"));
932 }
933
934 #[test]
935 fn test_mcp_ui_renderer_resolution() {
936 let mut config = McpUiConfig::default();
937 config.renderers.insert(
938 mcp_constants::RENDERER_CONTEXT7.to_string(),
939 McpRendererProfile::Context7,
940 );
941 config.renderers.insert(
942 mcp_constants::RENDERER_SEQUENTIAL_THINKING.to_string(),
943 McpRendererProfile::SequentialThinking,
944 );
945
946 assert_eq!(
947 config.renderer_for_tool("mcp_context7_lookup"),
948 Some(McpRendererProfile::Context7)
949 );
950 assert_eq!(
951 config.renderer_for_tool("mcp_context7lookup"),
952 Some(McpRendererProfile::Context7)
953 );
954 assert_eq!(
955 config.renderer_for_tool("mcp_sequentialthinking_run"),
956 Some(McpRendererProfile::SequentialThinking)
957 );
958 assert_eq!(
959 config.renderer_for_identifier("sequential-thinking-analyze"),
960 Some(McpRendererProfile::SequentialThinking)
961 );
962 assert_eq!(config.renderer_for_tool("mcp_unknown"), None);
963 }
964}