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 pub renderers: HashMap<String, McpRendererProfile>,
225}
226
227impl Default for McpUiConfig {
228 fn default() -> Self {
229 Self {
230 mode: default_mcp_ui_mode(),
231 max_events: default_max_mcp_events(),
232 show_provider_names: default_show_provider_names(),
233 renderers: HashMap::new(),
234 }
235 }
236}
237
238impl McpUiConfig {
239 pub fn renderer_for_identifier(&self, identifier: &str) -> Option<McpRendererProfile> {
241 let normalized_identifier = normalize_mcp_identifier(identifier);
242 if normalized_identifier.is_empty() {
243 return None;
244 }
245
246 self.renderers.iter().find_map(|(key, profile)| {
247 let normalized_key = normalize_mcp_identifier(key);
248 if normalized_identifier.starts_with(&normalized_key) {
249 Some(*profile)
250 } else {
251 None
252 }
253 })
254 }
255
256 pub fn renderer_for_tool(&self, tool_name: &str) -> Option<McpRendererProfile> {
258 let identifier = tool_name.strip_prefix("mcp_").unwrap_or(tool_name);
259 self.renderer_for_identifier(identifier)
260 }
261}
262
263#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
266#[serde(rename_all = "snake_case")]
267#[derive(Default)]
268pub enum McpUiMode {
269 #[default]
271 Compact,
272 Full,
274}
275
276impl std::fmt::Display for McpUiMode {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 match self {
279 McpUiMode::Compact => write!(f, "compact"),
280 McpUiMode::Full => write!(f, "full"),
281 }
282 }
283}
284
285#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
288#[serde(rename_all = "kebab-case")]
289pub enum McpRendererProfile {
290 Context7,
292 SequentialThinking,
294}
295
296#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
298#[derive(Debug, Clone, Deserialize, Serialize)]
299pub struct McpProviderConfig {
300 pub name: String,
302
303 #[serde(flatten)]
305 pub transport: McpTransportConfig,
306
307 #[serde(default)]
309 pub env: HashMap<String, String>,
310
311 #[serde(default = "default_provider_enabled")]
313 pub enabled: bool,
314
315 #[serde(default = "default_provider_max_concurrent")]
317 pub max_concurrent_requests: usize,
318
319 #[serde(default)]
321 pub startup_timeout_ms: Option<u64>,
322}
323
324impl Default for McpProviderConfig {
325 fn default() -> Self {
326 Self {
327 name: String::new(),
328 transport: McpTransportConfig::Stdio(McpStdioServerConfig::default()),
329 env: HashMap::new(),
330 enabled: default_provider_enabled(),
331 max_concurrent_requests: default_provider_max_concurrent(),
332 startup_timeout_ms: None,
333 }
334 }
335}
336
337#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
339#[derive(Debug, Clone, Deserialize, Serialize)]
340pub struct McpAllowListConfig {
341 #[serde(default = "default_allowlist_enforced")]
343 pub enforce: bool,
344
345 #[serde(default)]
347 pub default: McpAllowListRules,
348
349 #[serde(default)]
351 pub providers: BTreeMap<String, McpAllowListRules>,
352}
353
354impl Default for McpAllowListConfig {
355 fn default() -> Self {
356 Self {
357 enforce: default_allowlist_enforced(),
358 default: McpAllowListRules::default(),
359 providers: BTreeMap::new(),
360 }
361 }
362}
363
364impl McpAllowListConfig {
365 pub fn is_tool_allowed(&self, provider: &str, tool_name: &str) -> bool {
367 if !self.enforce {
368 return true;
369 }
370
371 self.resolve_match(provider, tool_name, |rules| &rules.tools)
372 }
373
374 pub fn is_resource_allowed(&self, provider: &str, resource: &str) -> bool {
376 if !self.enforce {
377 return true;
378 }
379
380 self.resolve_match(provider, resource, |rules| &rules.resources)
381 }
382
383 pub fn is_prompt_allowed(&self, provider: &str, prompt: &str) -> bool {
385 if !self.enforce {
386 return true;
387 }
388
389 self.resolve_match(provider, prompt, |rules| &rules.prompts)
390 }
391
392 pub fn is_logging_channel_allowed(&self, provider: Option<&str>, channel: &str) -> bool {
394 if !self.enforce {
395 return true;
396 }
397
398 if let Some(name) = provider
399 && let Some(rules) = self.providers.get(name)
400 && let Some(patterns) = &rules.logging
401 {
402 return pattern_matches(patterns, channel);
403 }
404
405 if let Some(patterns) = &self.default.logging
406 && pattern_matches(patterns, channel)
407 {
408 return true;
409 }
410
411 false
412 }
413
414 pub fn is_configuration_allowed(
416 &self,
417 provider: Option<&str>,
418 category: &str,
419 key: &str,
420 ) -> bool {
421 if !self.enforce {
422 return true;
423 }
424
425 if let Some(name) = provider
426 && let Some(rules) = self.providers.get(name)
427 && let Some(result) = configuration_allowed(rules, category, key)
428 {
429 return result;
430 }
431
432 if let Some(result) = configuration_allowed(&self.default, category, key) {
433 return result;
434 }
435
436 false
437 }
438
439 fn resolve_match<'a, F>(&'a self, provider: &str, candidate: &str, accessor: F) -> bool
440 where
441 F: Fn(&'a McpAllowListRules) -> &'a Option<Vec<String>>,
442 {
443 if let Some(rules) = self.providers.get(provider)
444 && let Some(patterns) = accessor(rules)
445 {
446 return pattern_matches(patterns, candidate);
447 }
448
449 if let Some(patterns) = accessor(&self.default)
450 && pattern_matches(patterns, candidate)
451 {
452 return true;
453 }
454
455 false
456 }
457}
458
459fn configuration_allowed(rules: &McpAllowListRules, category: &str, key: &str) -> Option<bool> {
460 rules.configuration.as_ref().and_then(|entries| {
461 entries
462 .get(category)
463 .map(|patterns| pattern_matches(patterns, key))
464 })
465}
466
467fn pattern_matches(patterns: &[String], candidate: &str) -> bool {
468 patterns
469 .iter()
470 .any(|pattern| wildcard_match(pattern, candidate))
471}
472
473fn wildcard_match(pattern: &str, candidate: &str) -> bool {
474 if pattern == "*" {
475 return true;
476 }
477
478 let mut regex_pattern = String::from("^");
479 let mut literal_buffer = String::new();
480
481 for ch in pattern.chars() {
482 match ch {
483 '*' => {
484 if !literal_buffer.is_empty() {
485 regex_pattern.push_str(®ex::escape(&literal_buffer));
486 literal_buffer.clear();
487 }
488 regex_pattern.push_str(".*");
489 }
490 '?' => {
491 if !literal_buffer.is_empty() {
492 regex_pattern.push_str(®ex::escape(&literal_buffer));
493 literal_buffer.clear();
494 }
495 regex_pattern.push('.');
496 }
497 _ => literal_buffer.push(ch),
498 }
499 }
500
501 if !literal_buffer.is_empty() {
502 regex_pattern.push_str(®ex::escape(&literal_buffer));
503 }
504
505 regex_pattern.push('$');
506
507 Regex::new(®ex_pattern)
508 .map(|regex| regex.is_match(candidate))
509 .unwrap_or(false)
510}
511
512#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
514#[derive(Debug, Clone, Deserialize, Serialize, Default)]
515pub struct McpAllowListRules {
516 #[serde(default)]
518 pub tools: Option<Vec<String>>,
519
520 #[serde(default)]
522 pub resources: Option<Vec<String>>,
523
524 #[serde(default)]
526 pub prompts: Option<Vec<String>>,
527
528 #[serde(default)]
530 pub logging: Option<Vec<String>>,
531
532 #[serde(default)]
534 pub configuration: Option<BTreeMap<String, Vec<String>>>,
535}
536
537#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
539#[derive(Debug, Clone, Deserialize, Serialize)]
540pub struct McpServerConfig {
541 #[serde(default = "default_mcp_server_enabled")]
543 pub enabled: bool,
544
545 #[serde(default = "default_mcp_server_bind")]
547 pub bind_address: String,
548
549 #[serde(default = "default_mcp_server_port")]
551 pub port: u16,
552
553 #[serde(default = "default_mcp_server_transport")]
555 pub transport: McpServerTransport,
556
557 #[serde(default = "default_mcp_server_name")]
559 pub name: String,
560
561 #[serde(default = "default_mcp_server_version")]
563 pub version: String,
564
565 #[serde(default)]
567 pub exposed_tools: Vec<String>,
568}
569
570impl Default for McpServerConfig {
571 fn default() -> Self {
572 Self {
573 enabled: default_mcp_server_enabled(),
574 bind_address: default_mcp_server_bind(),
575 port: default_mcp_server_port(),
576 transport: default_mcp_server_transport(),
577 name: default_mcp_server_name(),
578 version: default_mcp_server_version(),
579 exposed_tools: Vec::new(),
580 }
581 }
582}
583
584#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
586#[derive(Debug, Clone, Deserialize, Serialize)]
587#[serde(rename_all = "snake_case")]
588#[derive(Default)]
589pub enum McpServerTransport {
590 #[default]
592 Sse,
593 Http,
595}
596
597#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
599#[derive(Debug, Clone, Deserialize, Serialize)]
600#[serde(untagged)]
601pub enum McpTransportConfig {
602 Stdio(McpStdioServerConfig),
604 Http(McpHttpServerConfig),
606}
607
608#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
610#[derive(Debug, Clone, Deserialize, Serialize, Default)]
611pub struct McpStdioServerConfig {
612 pub command: String,
614
615 pub args: Vec<String>,
617
618 #[serde(default)]
620 pub working_directory: Option<String>,
621}
622
623#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
629#[derive(Debug, Clone, Deserialize, Serialize)]
630pub struct McpHttpServerConfig {
631 pub endpoint: String,
633
634 #[serde(default)]
636 pub api_key_env: Option<String>,
637
638 #[serde(default = "default_mcp_protocol_version")]
640 pub protocol_version: String,
641
642 #[serde(default, alias = "headers")]
644 pub http_headers: HashMap<String, String>,
645
646 #[serde(default)]
649 pub env_http_headers: HashMap<String, String>,
650}
651
652impl Default for McpHttpServerConfig {
653 fn default() -> Self {
654 Self {
655 endpoint: String::new(),
656 api_key_env: None,
657 protocol_version: default_mcp_protocol_version(),
658 http_headers: HashMap::new(),
659 env_http_headers: HashMap::new(),
660 }
661 }
662}
663
664fn default_mcp_enabled() -> bool {
666 false
667}
668
669fn default_mcp_ui_mode() -> McpUiMode {
670 McpUiMode::Compact
671}
672
673fn default_max_mcp_events() -> usize {
674 50
675}
676
677fn default_show_provider_names() -> bool {
678 true
679}
680
681fn default_max_concurrent_connections() -> usize {
682 5
683}
684
685fn default_request_timeout_seconds() -> u64 {
686 30
687}
688
689fn default_retry_attempts() -> u32 {
690 3
691}
692
693fn default_experimental_use_rmcp_client() -> bool {
694 true
695}
696
697fn default_provider_enabled() -> bool {
698 true
699}
700
701fn default_provider_max_concurrent() -> usize {
702 3
703}
704
705fn default_allowlist_enforced() -> bool {
706 false
707}
708
709fn default_mcp_protocol_version() -> String {
710 "2024-11-05".into()
711}
712
713fn default_mcp_server_enabled() -> bool {
714 false
715}
716
717fn default_connection_pooling_enabled() -> bool {
718 true
719}
720
721fn default_tool_cache_capacity() -> usize {
722 100
723}
724
725fn default_connection_timeout_seconds() -> u64 {
726 30
727}
728
729fn default_mcp_server_bind() -> String {
730 "127.0.0.1".into()
731}
732
733fn default_mcp_server_port() -> u16 {
734 3000
735}
736
737fn default_mcp_server_transport() -> McpServerTransport {
738 McpServerTransport::Sse
739}
740
741fn default_mcp_server_name() -> String {
742 "vtcode-mcp-server".into()
743}
744
745fn default_mcp_server_version() -> String {
746 env!("CARGO_PKG_VERSION").into()
747}
748
749fn normalize_mcp_identifier(value: &str) -> String {
750 value
751 .chars()
752 .filter(|ch| ch.is_ascii_alphanumeric())
753 .map(|ch| ch.to_ascii_lowercase())
754 .collect()
755}
756
757fn default_mcp_auth_enabled() -> bool {
758 false
759}
760
761fn default_requests_per_minute() -> u32 {
762 100
763}
764
765fn default_concurrent_requests() -> u32 {
766 10
767}
768
769fn default_schema_validation_enabled() -> bool {
770 true
771}
772
773fn default_path_traversal_protection_enabled() -> bool {
774 true
775}
776
777fn default_max_argument_size() -> u32 {
778 1024 * 1024 }
780
781fn default_mcp_requirements_enforce() -> bool {
782 false
783}
784
785#[cfg(test)]
786mod tests {
787 use super::*;
788 use crate::constants::mcp as mcp_constants;
789 use std::collections::BTreeMap;
790
791 #[test]
792 fn test_mcp_config_defaults() {
793 let config = McpClientConfig::default();
794 assert!(!config.enabled);
795 assert_eq!(config.ui.mode, McpUiMode::Compact);
796 assert_eq!(config.ui.max_events, 50);
797 assert!(config.ui.show_provider_names);
798 assert!(config.ui.renderers.is_empty());
799 assert_eq!(config.max_concurrent_connections, 5);
800 assert_eq!(config.request_timeout_seconds, 30);
801 assert_eq!(config.retry_attempts, 3);
802 assert!(config.providers.is_empty());
803 assert!(!config.requirements.enforce);
804 assert!(config.requirements.allowed_stdio_commands.is_empty());
805 assert!(config.requirements.allowed_http_endpoints.is_empty());
806 assert!(!config.server.enabled);
807 assert!(!config.allowlist.enforce);
808 assert!(config.allowlist.default.tools.is_none());
809 }
810
811 #[test]
812 fn test_allowlist_pattern_matching() {
813 let patterns = vec!["get_*".to_string(), "convert_timezone".to_string()];
814 assert!(pattern_matches(&patterns, "get_current_time"));
815 assert!(pattern_matches(&patterns, "convert_timezone"));
816 assert!(!pattern_matches(&patterns, "delete_timezone"));
817 }
818
819 #[test]
820 fn test_allowlist_provider_override() {
821 let mut config = McpAllowListConfig {
822 enforce: true,
823 default: McpAllowListRules {
824 tools: Some(vec!["get_*".to_string()]),
825 ..Default::default()
826 },
827 ..Default::default()
828 };
829
830 let provider_rules = McpAllowListRules {
831 tools: Some(vec!["list_*".to_string()]),
832 ..Default::default()
833 };
834 config
835 .providers
836 .insert("context7".to_string(), provider_rules);
837
838 assert!(config.is_tool_allowed("context7", "list_documents"));
839 assert!(!config.is_tool_allowed("context7", "get_current_time"));
840 assert!(config.is_tool_allowed("other", "get_timezone"));
841 assert!(!config.is_tool_allowed("other", "list_documents"));
842 }
843
844 #[test]
845 fn test_allowlist_configuration_rules() {
846 let mut config = McpAllowListConfig {
847 enforce: true,
848 default: McpAllowListRules {
849 configuration: Some(BTreeMap::from([(
850 "ui".to_string(),
851 vec!["mode".to_string(), "max_events".to_string()],
852 )])),
853 ..Default::default()
854 },
855 ..Default::default()
856 };
857
858 let provider_rules = McpAllowListRules {
859 configuration: Some(BTreeMap::from([(
860 "provider".to_string(),
861 vec!["max_concurrent_requests".to_string()],
862 )])),
863 ..Default::default()
864 };
865 config.providers.insert("time".to_string(), provider_rules);
866
867 assert!(config.is_configuration_allowed(None, "ui", "mode"));
868 assert!(!config.is_configuration_allowed(None, "ui", "show_provider_names"));
869 assert!(config.is_configuration_allowed(
870 Some("time"),
871 "provider",
872 "max_concurrent_requests"
873 ));
874 assert!(!config.is_configuration_allowed(Some("time"), "provider", "retry_attempts"));
875 }
876
877 #[test]
878 fn test_allowlist_resource_override() {
879 let mut config = McpAllowListConfig {
880 enforce: true,
881 default: McpAllowListRules {
882 resources: Some(vec!["docs/**/*".to_string()]),
883 ..Default::default()
884 },
885 ..Default::default()
886 };
887
888 let provider_rules = McpAllowListRules {
889 resources: Some(vec!["journals/*".to_string()]),
890 ..Default::default()
891 };
892 config
893 .providers
894 .insert("context7".to_string(), provider_rules);
895
896 assert!(config.is_resource_allowed("context7", "journals/2024"));
897 assert!(config.is_resource_allowed("other", "docs/config/config.md"));
898 assert!(config.is_resource_allowed("other", "docs/guides/zed-acp.md"));
899 assert!(!config.is_resource_allowed("other", "journals/2023"));
900 }
901
902 #[test]
903 fn test_allowlist_logging_override() {
904 let mut config = McpAllowListConfig {
905 enforce: true,
906 default: McpAllowListRules {
907 logging: Some(vec!["info".to_string(), "debug".to_string()]),
908 ..Default::default()
909 },
910 ..Default::default()
911 };
912
913 let provider_rules = McpAllowListRules {
914 logging: Some(vec!["audit".to_string()]),
915 ..Default::default()
916 };
917 config
918 .providers
919 .insert("sequential".to_string(), provider_rules);
920
921 assert!(config.is_logging_channel_allowed(Some("sequential"), "audit"));
922 assert!(!config.is_logging_channel_allowed(Some("sequential"), "info"));
923 assert!(config.is_logging_channel_allowed(Some("other"), "info"));
924 assert!(!config.is_logging_channel_allowed(Some("other"), "trace"));
925 }
926
927 #[test]
928 fn test_mcp_ui_renderer_resolution() {
929 let mut config = McpUiConfig::default();
930 config.renderers.insert(
931 mcp_constants::RENDERER_CONTEXT7.to_string(),
932 McpRendererProfile::Context7,
933 );
934 config.renderers.insert(
935 mcp_constants::RENDERER_SEQUENTIAL_THINKING.to_string(),
936 McpRendererProfile::SequentialThinking,
937 );
938
939 assert_eq!(
940 config.renderer_for_tool("mcp_context7_lookup"),
941 Some(McpRendererProfile::Context7)
942 );
943 assert_eq!(
944 config.renderer_for_tool("mcp_context7lookup"),
945 Some(McpRendererProfile::Context7)
946 );
947 assert_eq!(
948 config.renderer_for_tool("mcp_sequentialthinking_run"),
949 Some(McpRendererProfile::SequentialThinking)
950 );
951 assert_eq!(
952 config.renderer_for_identifier("sequential-thinking-analyze"),
953 Some(McpRendererProfile::SequentialThinking)
954 );
955 assert_eq!(config.renderer_for_tool("mcp_unknown"), None);
956 }
957}