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