1use std::collections::HashMap;
5
6use serde::{Deserialize, Serialize};
7
8use crate::defaults::default_true;
9use crate::providers::ProviderName;
10
11pub use crate::mcp_security::ToolSecurityMeta;
12
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum McpTrustLevel {
21 Trusted,
23 #[default]
25 Untrusted,
26 Sandboxed,
28}
29
30impl McpTrustLevel {
31 #[must_use]
35 pub fn restriction_level(self) -> u8 {
36 match self {
37 Self::Trusted => 0,
38 Self::Untrusted => 1,
39 Self::Sandboxed => 2,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Deserialize, Serialize)]
46pub struct RateLimit {
47 pub max_calls_per_minute: u32,
49}
50
51#[derive(Debug, Clone, Default, Deserialize, Serialize)]
55#[serde(default)]
56pub struct McpPolicy {
57 pub allowed_tools: Option<Vec<String>>,
59 pub denied_tools: Vec<String>,
61 pub rate_limit: Option<RateLimit>,
63}
64
65fn default_skill_allowlist() -> Vec<String> {
66 vec!["*".into()]
67}
68
69#[derive(Debug, Clone, Deserialize, Serialize)]
75pub struct ChannelSkillsConfig {
76 #[serde(default = "default_skill_allowlist")]
79 pub allowed: Vec<String>,
80}
81
82impl Default for ChannelSkillsConfig {
83 fn default() -> Self {
84 Self {
85 allowed: default_skill_allowlist(),
86 }
87 }
88}
89
90#[must_use]
95pub fn is_skill_allowed(name: &str, config: &ChannelSkillsConfig) -> bool {
96 config.allowed.iter().any(|p| glob_match(p, name))
97}
98
99fn glob_match(pattern: &str, name: &str) -> bool {
100 if let Some(prefix) = pattern.strip_suffix('*') {
101 if prefix.is_empty() {
102 return true;
103 }
104 name.starts_with(prefix)
105 } else {
106 pattern == name
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 fn allow(patterns: &[&str]) -> ChannelSkillsConfig {
115 ChannelSkillsConfig {
116 allowed: patterns.iter().map(ToString::to_string).collect(),
117 }
118 }
119
120 #[test]
121 fn telegram_config_defaults() {
122 let src = r#"token = "test_token""#;
124 let cfg: TelegramConfig = toml::from_str(src).unwrap();
125 assert!(!cfg.guest_mode);
126 assert!(!cfg.bot_to_bot);
127 assert!(cfg.allowed_bots.is_empty());
128 assert_eq!(cfg.max_bot_chain_depth, 1);
129 }
130
131 #[test]
132 fn telegram_config_explicit_values() {
133 let src = r#"
134token = "test_token"
135guest_mode = true
136bot_to_bot = true
137allowed_bots = ["@bot_a", "@bot_b"]
138max_bot_chain_depth = 5
139"#;
140 let cfg: TelegramConfig = toml::from_str(src).unwrap();
141 assert!(cfg.guest_mode);
142 assert!(cfg.bot_to_bot);
143 assert_eq!(cfg.allowed_bots, vec!["@bot_a", "@bot_b"]);
144 assert_eq!(cfg.max_bot_chain_depth, 5);
145 }
146
147 #[test]
148 fn test_default_output_schema_hint_bytes_is_1024() {
149 assert_eq!(default_output_schema_hint_bytes(), 1024);
150 }
151
152 #[test]
153 fn test_mcp_config_default_output_schema_hint_bytes_is_1024() {
154 let cfg = McpConfig::default();
155 assert_eq!(cfg.output_schema_hint_bytes, 1024);
156 }
157
158 #[test]
159 fn max_connect_attempts_default_is_3() {
160 let cfg = McpConfig::default();
161 assert_eq!(cfg.max_connect_attempts, 3);
162 }
163
164 #[test]
165 fn max_connect_attempts_accepts_valid_range() {
166 for v in [1u8, 3, 10] {
167 let src = format!("max_connect_attempts = {v}\n");
168 let cfg: McpConfig = toml::from_str(&src)
169 .unwrap_or_else(|e| panic!("max_connect_attempts = {v} should be valid, got: {e}"));
170 assert_eq!(cfg.max_connect_attempts, v);
171 }
172 }
173
174 #[test]
175 fn max_connect_attempts_rejects_zero() {
176 let src = "max_connect_attempts = 0\n";
177 let result = toml::from_str::<McpConfig>(src);
178 assert!(
179 result.is_err(),
180 "max_connect_attempts = 0 should be rejected"
181 );
182 let msg = result.unwrap_err().to_string();
183 assert!(
184 msg.contains("max_connect_attempts"),
185 "error message should mention the field name, got: {msg}"
186 );
187 }
188
189 #[test]
190 fn max_connect_attempts_rejects_eleven() {
191 let src = "max_connect_attempts = 11\n";
192 let result = toml::from_str::<McpConfig>(src);
193 assert!(
194 result.is_err(),
195 "max_connect_attempts = 11 should be rejected"
196 );
197 }
198
199 #[test]
200 fn wildcard_star_allows_any_skill() {
201 let cfg = allow(&["*"]);
202 assert!(is_skill_allowed("anything", &cfg));
203 assert!(is_skill_allowed("web-search", &cfg));
204 }
205
206 #[test]
207 fn empty_allowlist_denies_all() {
208 let cfg = allow(&[]);
209 assert!(!is_skill_allowed("web-search", &cfg));
210 assert!(!is_skill_allowed("shell", &cfg));
211 }
212
213 #[test]
214 fn exact_match_allows_only_that_skill() {
215 let cfg = allow(&["web-search"]);
216 assert!(is_skill_allowed("web-search", &cfg));
217 assert!(!is_skill_allowed("shell", &cfg));
218 assert!(!is_skill_allowed("web-search-extra", &cfg));
219 }
220
221 #[test]
222 fn prefix_wildcard_allows_matching_skills() {
223 let cfg = allow(&["web-*"]);
224 assert!(is_skill_allowed("web-search", &cfg));
225 assert!(is_skill_allowed("web-fetch", &cfg));
226 assert!(!is_skill_allowed("shell", &cfg));
227 assert!(!is_skill_allowed("awesome-web-thing", &cfg));
228 }
229
230 #[test]
231 fn multiple_patterns_or_logic() {
232 let cfg = allow(&["shell", "web-*"]);
233 assert!(is_skill_allowed("shell", &cfg));
234 assert!(is_skill_allowed("web-search", &cfg));
235 assert!(!is_skill_allowed("memory", &cfg));
236 }
237
238 #[test]
239 fn default_config_allows_all() {
240 let cfg = ChannelSkillsConfig::default();
241 assert!(is_skill_allowed("any-skill", &cfg));
242 }
243
244 #[test]
245 fn prefix_wildcard_does_not_match_empty_suffix() {
246 let cfg = allow(&["web-*"]);
247 assert!(is_skill_allowed("web-", &cfg));
251 }
252
253 #[test]
254 fn matching_is_case_sensitive() {
255 let cfg = allow(&["Web-Search"]);
256 assert!(!is_skill_allowed("web-search", &cfg));
257 assert!(is_skill_allowed("Web-Search", &cfg));
258 }
259}
260
261fn default_slack_port() -> u16 {
262 3000
263}
264
265fn default_slack_webhook_host() -> String {
266 "127.0.0.1".into()
267}
268
269fn default_a2a_host() -> String {
270 "0.0.0.0".into()
271}
272
273fn default_a2a_port() -> u16 {
274 8080
275}
276
277fn default_a2a_rate_limit() -> u32 {
278 60
279}
280
281fn default_a2a_max_body() -> usize {
282 1_048_576
283}
284
285fn default_drain_timeout_ms() -> u64 {
286 30_000
287}
288
289fn default_max_dynamic_servers() -> usize {
290 10
291}
292
293fn default_mcp_timeout() -> u64 {
294 30
295}
296
297fn default_oauth_callback_port() -> u16 {
298 18766
299}
300
301fn default_oauth_client_name() -> String {
302 "Zeph".into()
303}
304
305fn default_stream_interval_ms() -> u64 {
306 3000
307}
308
309fn default_max_bot_chain_depth() -> u32 {
310 1
311}
312
313#[derive(Clone, Deserialize, Serialize)]
330pub struct TelegramConfig {
331 pub token: Option<String>,
333 #[serde(default)]
335 pub allowed_users: Vec<String>,
336 #[serde(default)]
338 pub skills: ChannelSkillsConfig,
339 #[serde(default)]
342 pub allowed_tools: Option<Vec<String>>,
343 #[serde(default = "default_stream_interval_ms")]
349 pub stream_interval_ms: u64,
350 #[serde(default)]
354 pub guest_mode: bool,
355 #[serde(default)]
359 pub bot_to_bot: bool,
360 #[serde(default)]
364 pub allowed_bots: Vec<String>,
365 #[serde(default = "default_max_bot_chain_depth")]
376 pub max_bot_chain_depth: u32,
377}
378
379impl std::fmt::Debug for TelegramConfig {
380 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381 f.debug_struct("TelegramConfig")
382 .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
383 .field("allowed_users", &self.allowed_users)
384 .field("skills", &self.skills)
385 .field("allowed_tools", &self.allowed_tools)
386 .field("stream_interval_ms", &self.stream_interval_ms)
387 .field("guest_mode", &self.guest_mode)
388 .field("bot_to_bot", &self.bot_to_bot)
389 .field("allowed_bots_count", &self.allowed_bots.len())
390 .field("max_bot_chain_depth", &self.max_bot_chain_depth)
391 .finish()
392 }
393}
394
395#[derive(Clone, Deserialize, Serialize)]
396pub struct DiscordConfig {
397 pub token: Option<String>,
398 pub application_id: Option<String>,
399 #[serde(default)]
400 pub allowed_user_ids: Vec<String>,
401 #[serde(default)]
402 pub allowed_role_ids: Vec<String>,
403 #[serde(default)]
404 pub allowed_channel_ids: Vec<String>,
405 #[serde(default)]
406 pub skills: ChannelSkillsConfig,
407 #[serde(default)]
409 pub allowed_tools: Option<Vec<String>>,
410}
411
412impl std::fmt::Debug for DiscordConfig {
413 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414 f.debug_struct("DiscordConfig")
415 .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
416 .field("application_id", &self.application_id)
417 .field("allowed_user_ids", &self.allowed_user_ids)
418 .field("allowed_role_ids", &self.allowed_role_ids)
419 .field("allowed_channel_ids", &self.allowed_channel_ids)
420 .field("skills", &self.skills)
421 .field("allowed_tools", &self.allowed_tools)
422 .finish()
423 }
424}
425
426#[derive(Clone, Deserialize, Serialize)]
427pub struct SlackConfig {
428 pub bot_token: Option<String>,
429 pub signing_secret: Option<String>,
430 #[serde(default = "default_slack_webhook_host")]
431 pub webhook_host: String,
432 #[serde(default = "default_slack_port")]
433 pub port: u16,
434 #[serde(default)]
435 pub allowed_user_ids: Vec<String>,
436 #[serde(default)]
437 pub allowed_channel_ids: Vec<String>,
438 #[serde(default)]
439 pub skills: ChannelSkillsConfig,
440 #[serde(default)]
442 pub allowed_tools: Option<Vec<String>>,
443}
444
445impl std::fmt::Debug for SlackConfig {
446 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
447 f.debug_struct("SlackConfig")
448 .field("bot_token", &self.bot_token.as_ref().map(|_| "[REDACTED]"))
449 .field(
450 "signing_secret",
451 &self.signing_secret.as_ref().map(|_| "[REDACTED]"), )
453 .field("webhook_host", &self.webhook_host)
454 .field("port", &self.port)
455 .field("allowed_user_ids", &self.allowed_user_ids)
456 .field("allowed_channel_ids", &self.allowed_channel_ids)
457 .field("skills", &self.skills)
458 .field("allowed_tools", &self.allowed_tools)
459 .finish()
460 }
461}
462
463#[derive(Debug, Clone, Deserialize, Serialize)]
467pub struct IbctKeyConfig {
468 pub key_id: String,
470 pub key_hex: String,
472}
473
474fn default_ibct_ttl() -> u64 {
475 300
476}
477
478fn default_a2a_request_timeout_ms() -> u64 {
479 300_000
480}
481
482#[derive(Deserialize, Serialize)]
488#[allow(clippy::struct_excessive_bools)] pub struct A2aServerConfig {
490 #[serde(default)]
491 pub enabled: bool,
492 #[serde(default = "default_a2a_host")]
493 pub host: String,
494 #[serde(default = "default_a2a_port")]
495 pub port: u16,
496 #[serde(default)]
497 pub public_url: String,
498 #[serde(default)]
499 pub auth_token: Option<String>,
500 #[serde(default = "default_a2a_rate_limit")]
501 pub rate_limit: u32,
502 #[serde(default = "default_true")]
503 pub require_tls: bool,
504 #[serde(default = "default_true")]
505 pub ssrf_protection: bool,
506 #[serde(default = "default_a2a_max_body")]
507 pub max_body_size: usize,
508 #[serde(default = "default_drain_timeout_ms")]
509 pub drain_timeout_ms: u64,
510 #[serde(default)]
514 pub require_auth: bool,
515 #[serde(default)]
520 pub ibct_keys: Vec<IbctKeyConfig>,
521 #[serde(default)]
527 pub ibct_signing_key_vault_ref: Option<String>,
528 #[serde(default = "default_ibct_ttl")]
530 pub ibct_ttl_secs: u64,
531 #[serde(default)]
545 pub advertise_files: bool,
546 #[serde(default = "default_a2a_request_timeout_ms")]
552 pub request_timeout_ms: u64,
553}
554
555impl std::fmt::Debug for A2aServerConfig {
556 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
557 f.debug_struct("A2aServerConfig")
558 .field("enabled", &self.enabled)
559 .field("host", &self.host)
560 .field("port", &self.port)
561 .field("public_url", &self.public_url)
562 .field(
563 "auth_token",
564 &self.auth_token.as_ref().map(|_| "[REDACTED]"),
565 )
566 .field("rate_limit", &self.rate_limit)
567 .field("require_tls", &self.require_tls)
568 .field("ssrf_protection", &self.ssrf_protection)
569 .field("max_body_size", &self.max_body_size)
570 .field("drain_timeout_ms", &self.drain_timeout_ms)
571 .field("require_auth", &self.require_auth)
572 .field("ibct_keys_count", &self.ibct_keys.len())
573 .field(
574 "ibct_signing_key_vault_ref",
575 &self.ibct_signing_key_vault_ref,
576 )
577 .field("ibct_ttl_secs", &self.ibct_ttl_secs)
578 .field("advertise_files", &self.advertise_files)
579 .field("request_timeout_ms", &self.request_timeout_ms)
580 .finish()
581 }
582}
583
584impl Default for A2aServerConfig {
585 fn default() -> Self {
586 Self {
587 enabled: false,
588 host: default_a2a_host(),
589 port: default_a2a_port(),
590 public_url: String::new(),
591 auth_token: None,
592 rate_limit: default_a2a_rate_limit(),
593 require_tls: true,
594 ssrf_protection: true,
595 max_body_size: default_a2a_max_body(),
596 drain_timeout_ms: default_drain_timeout_ms(),
597 require_auth: false,
598 ibct_keys: Vec::new(),
599 ibct_signing_key_vault_ref: None,
600 ibct_ttl_secs: default_ibct_ttl(),
601 advertise_files: false,
602 request_timeout_ms: default_a2a_request_timeout_ms(),
603 }
604 }
605}
606
607#[derive(Debug, Clone, Deserialize, Serialize)]
613#[serde(default)]
614pub struct ToolPruningConfig {
615 pub enabled: bool,
617 pub max_tools: usize,
619 pub pruning_provider: ProviderName,
622 pub min_tools_to_prune: usize,
624 pub always_include: Vec<String>,
626}
627
628impl Default for ToolPruningConfig {
629 fn default() -> Self {
630 Self {
631 enabled: false,
632 max_tools: 15,
633 pruning_provider: ProviderName::default(),
634 min_tools_to_prune: 10,
635 always_include: Vec::new(),
636 }
637 }
638}
639
640#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
645#[serde(rename_all = "lowercase")]
646pub enum ToolDiscoveryStrategyConfig {
647 Embedding,
649 Llm,
651 #[default]
653 None,
654}
655
656#[derive(Debug, Clone, Deserialize, Serialize)]
662#[serde(default)]
663pub struct ToolDiscoveryConfig {
664 pub strategy: ToolDiscoveryStrategyConfig,
666 pub top_k: usize,
668 pub min_similarity: f32,
670 pub embedding_provider: ProviderName,
674 pub always_include: Vec<String>,
676 pub min_tools_to_filter: usize,
678 pub strict: bool,
681}
682
683impl Default for ToolDiscoveryConfig {
684 fn default() -> Self {
685 Self {
686 strategy: ToolDiscoveryStrategyConfig::None,
687 top_k: 10,
688 min_similarity: 0.2,
689 embedding_provider: ProviderName::default(),
690 always_include: Vec::new(),
691 min_tools_to_filter: 10,
692 strict: false,
693 }
694 }
695}
696
697#[derive(Debug, Clone, Deserialize, Serialize)]
699#[allow(clippy::struct_excessive_bools)] pub struct TrustCalibrationConfig {
701 #[serde(default)]
703 pub enabled: bool,
704 #[serde(default = "default_true")]
706 pub probe_on_connect: bool,
707 #[serde(default = "default_true")]
709 pub monitor_invocations: bool,
710 #[serde(default = "default_true")]
712 pub persist_scores: bool,
713 #[serde(default = "default_decay_rate")]
715 pub decay_rate_per_day: f64,
716 #[serde(default = "default_injection_penalty")]
718 pub injection_penalty: f64,
719 #[serde(default)]
721 pub verifier_provider: ProviderName,
722}
723
724fn default_decay_rate() -> f64 {
725 0.01
726}
727
728fn default_injection_penalty() -> f64 {
729 0.25
730}
731
732impl Default for TrustCalibrationConfig {
733 fn default() -> Self {
734 Self {
735 enabled: false,
736 probe_on_connect: true,
737 monitor_invocations: true,
738 persist_scores: true,
739 decay_rate_per_day: default_decay_rate(),
740 injection_penalty: default_injection_penalty(),
741 verifier_provider: ProviderName::default(),
742 }
743 }
744}
745
746fn default_max_description_bytes() -> usize {
747 2048
748}
749
750fn default_max_instructions_bytes() -> usize {
751 2048
752}
753
754fn default_elicitation_timeout() -> u64 {
755 120
756}
757
758fn default_elicitation_queue_capacity() -> usize {
759 16
760}
761
762fn default_output_schema_hint_bytes() -> usize {
763 1024
764}
765
766fn default_max_connect_attempts() -> u8 {
767 3
768}
769
770fn validate_max_connect_attempts<'de, D>(d: D) -> Result<u8, D::Error>
771where
772 D: serde::Deserializer<'de>,
773{
774 let v = u8::deserialize(d)?;
775 if !(1..=10).contains(&v) {
776 return Err(serde::de::Error::custom(format!(
777 "mcp.max_connect_attempts must be in 1..=10 (got {v})"
778 )));
779 }
780 Ok(v)
781}
782
783#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone, Deserialize, Serialize)]
785pub struct McpConfig {
786 #[serde(default)]
787 pub servers: Vec<McpServerConfig>,
788 #[serde(default)]
789 pub allowed_commands: Vec<String>,
790 #[serde(default = "default_max_dynamic_servers")]
791 pub max_dynamic_servers: usize,
792 #[serde(default)]
794 pub pruning: ToolPruningConfig,
795 #[serde(default)]
797 pub trust_calibration: TrustCalibrationConfig,
798 #[serde(default)]
800 pub tool_discovery: ToolDiscoveryConfig,
801 #[serde(default = "default_max_description_bytes")]
803 pub max_description_bytes: usize,
804 #[serde(default = "default_max_instructions_bytes")]
806 pub max_instructions_bytes: usize,
807 #[serde(default)]
811 pub elicitation_enabled: bool,
812 #[serde(default = "default_elicitation_timeout")]
814 pub elicitation_timeout: u64,
815 #[serde(default = "default_elicitation_queue_capacity")]
819 pub elicitation_queue_capacity: usize,
820 #[serde(default = "default_true")]
823 pub elicitation_warn_sensitive_fields: bool,
824 #[serde(
836 default = "default_max_connect_attempts",
837 deserialize_with = "validate_max_connect_attempts"
838 )]
839 pub max_connect_attempts: u8,
840 #[serde(default)]
846 pub lock_tool_list: bool,
847 #[serde(default)]
852 pub default_env_isolation: bool,
853 #[serde(default)]
861 pub forward_output_schema: bool,
862 #[serde(default = "default_output_schema_hint_bytes")]
868 pub output_schema_hint_bytes: usize,
869}
870
871impl Default for McpConfig {
872 fn default() -> Self {
873 Self {
874 servers: Vec::new(),
875 allowed_commands: Vec::new(),
876 max_dynamic_servers: default_max_dynamic_servers(),
877 pruning: ToolPruningConfig::default(),
878 trust_calibration: TrustCalibrationConfig::default(),
879 tool_discovery: ToolDiscoveryConfig::default(),
880 max_description_bytes: default_max_description_bytes(),
881 max_instructions_bytes: default_max_instructions_bytes(),
882 elicitation_enabled: false,
883 elicitation_timeout: default_elicitation_timeout(),
884 elicitation_queue_capacity: default_elicitation_queue_capacity(),
885 elicitation_warn_sensitive_fields: true,
886 lock_tool_list: false,
887 default_env_isolation: false,
888 forward_output_schema: false,
889 output_schema_hint_bytes: default_output_schema_hint_bytes(),
890 max_connect_attempts: default_max_connect_attempts(),
891 }
892 }
893}
894
895#[derive(Clone, Deserialize, Serialize)]
896pub struct McpServerConfig {
897 pub id: String,
898 pub command: Option<String>,
900 #[serde(default)]
901 pub args: Vec<String>,
902 #[serde(default)]
903 pub env: HashMap<String, String>,
904 pub url: Option<String>,
906 #[serde(default = "default_mcp_timeout")]
907 pub timeout: u64,
908 #[serde(default)]
910 pub policy: McpPolicy,
911 #[serde(default)]
914 pub headers: HashMap<String, String>,
915 #[serde(default)]
917 pub oauth: Option<McpOAuthConfig>,
918 #[serde(default)]
920 pub trust_level: McpTrustLevel,
921 #[serde(default)]
925 pub tool_allowlist: Option<Vec<String>>,
926 #[serde(default)]
932 pub expected_tools: Vec<String>,
933 #[serde(default)]
937 pub roots: Vec<McpRootEntry>,
938 #[serde(default)]
941 pub tool_metadata: HashMap<String, ToolSecurityMeta>,
942 #[serde(default)]
946 pub elicitation_enabled: Option<bool>,
947 #[serde(default)]
954 pub env_isolation: Option<bool>,
955}
956
957#[derive(Debug, Clone, Deserialize, Serialize)]
959pub struct McpRootEntry {
960 pub uri: String,
962 #[serde(default)]
964 pub name: Option<String>,
965}
966
967#[derive(Debug, Clone, Deserialize, Serialize)]
969pub struct McpOAuthConfig {
970 #[serde(default)]
972 pub enabled: bool,
973 #[serde(default)]
975 pub token_storage: OAuthTokenStorage,
976 #[serde(default)]
978 pub scopes: Vec<String>,
979 #[serde(default = "default_oauth_callback_port")]
981 pub callback_port: u16,
982 #[serde(default = "default_oauth_client_name")]
984 pub client_name: String,
985}
986
987impl Default for McpOAuthConfig {
988 fn default() -> Self {
989 Self {
990 enabled: false,
991 token_storage: OAuthTokenStorage::default(),
992 scopes: Vec::new(),
993 callback_port: default_oauth_callback_port(),
994 client_name: default_oauth_client_name(),
995 }
996 }
997}
998
999#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1001#[serde(rename_all = "lowercase")]
1002pub enum OAuthTokenStorage {
1003 #[default]
1005 Vault,
1006 Memory,
1008}
1009
1010impl std::fmt::Debug for McpServerConfig {
1011 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1012 let redacted_env: HashMap<&str, &str> = self
1013 .env
1014 .keys()
1015 .map(|k| (k.as_str(), "[REDACTED]"))
1016 .collect();
1017 let redacted_headers: HashMap<&str, &str> = self
1019 .headers
1020 .keys()
1021 .map(|k| (k.as_str(), "[REDACTED]"))
1022 .collect();
1023 f.debug_struct("McpServerConfig")
1024 .field("id", &self.id)
1025 .field("command", &self.command)
1026 .field("args", &self.args)
1027 .field("env", &redacted_env)
1028 .field("url", &self.url)
1029 .field("timeout", &self.timeout)
1030 .field("policy", &self.policy)
1031 .field("headers", &redacted_headers)
1032 .field("oauth", &self.oauth)
1033 .field("trust_level", &self.trust_level)
1034 .field("tool_allowlist", &self.tool_allowlist)
1035 .field("expected_tools", &self.expected_tools)
1036 .field("roots", &self.roots)
1037 .field(
1038 "tool_metadata_keys",
1039 &self.tool_metadata.keys().collect::<Vec<_>>(),
1040 )
1041 .field("elicitation_enabled", &self.elicitation_enabled)
1042 .field("env_isolation", &self.env_isolation)
1043 .finish()
1044 }
1045}