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