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