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")]
20#[non_exhaustive]
21pub enum McpTrustLevel {
22 Trusted,
24 #[default]
26 Untrusted,
27 Sandboxed,
29}
30
31impl McpTrustLevel {
32 #[must_use]
36 pub fn restriction_level(self) -> u8 {
37 match self {
38 Self::Trusted => 0,
39 Self::Untrusted => 1,
40 Self::Sandboxed => 2,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Deserialize, Serialize)]
47pub struct RateLimit {
48 pub max_calls_per_minute: u32,
50}
51
52#[derive(Debug, Clone, Default, Deserialize, Serialize)]
56#[serde(default)]
57pub struct McpPolicy {
58 pub allowed_tools: Option<Vec<String>>,
60 pub denied_tools: Vec<String>,
62 pub rate_limit: Option<RateLimit>,
64}
65
66fn default_skill_allowlist() -> Vec<String> {
67 vec!["*".into()]
68}
69
70#[derive(Debug, Clone, Deserialize, Serialize)]
76pub struct ChannelSkillsConfig {
77 #[serde(default = "default_skill_allowlist")]
80 pub allowed: Vec<String>,
81}
82
83impl Default for ChannelSkillsConfig {
84 fn default() -> Self {
85 Self {
86 allowed: default_skill_allowlist(),
87 }
88 }
89}
90
91#[must_use]
96pub fn is_skill_allowed(name: &str, config: &ChannelSkillsConfig) -> bool {
97 config.allowed.iter().any(|p| glob_match(p, name))
98}
99
100fn glob_match(pattern: &str, name: &str) -> bool {
101 if let Some(prefix) = pattern.strip_suffix('*') {
102 if prefix.is_empty() {
103 return true;
104 }
105 name.starts_with(prefix)
106 } else {
107 pattern == name
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 fn allow(patterns: &[&str]) -> ChannelSkillsConfig {
116 ChannelSkillsConfig {
117 allowed: patterns.iter().map(ToString::to_string).collect(),
118 }
119 }
120
121 #[test]
122 fn telegram_config_defaults() {
123 let src = r#"token = "test_token""#;
125 let cfg: TelegramConfig = toml::from_str(src).unwrap();
126 assert!(!cfg.guest_mode);
127 assert!(!cfg.bot_to_bot);
128 assert!(cfg.allowed_bots.is_empty());
129 assert_eq!(cfg.max_bot_chain_depth, 1);
130 }
131
132 #[test]
133 fn telegram_config_explicit_values() {
134 let src = r#"
135token = "test_token"
136guest_mode = true
137bot_to_bot = true
138allowed_bots = ["@bot_a", "@bot_b"]
139max_bot_chain_depth = 5
140"#;
141 let cfg: TelegramConfig = toml::from_str(src).unwrap();
142 assert!(cfg.guest_mode);
143 assert!(cfg.bot_to_bot);
144 assert_eq!(cfg.allowed_bots, vec!["@bot_a", "@bot_b"]);
145 assert_eq!(cfg.max_bot_chain_depth, 5);
146 }
147
148 #[test]
149 fn test_default_output_schema_hint_bytes_is_1024() {
150 assert_eq!(default_output_schema_hint_bytes(), 1024);
151 }
152
153 #[test]
154 fn test_mcp_config_default_output_schema_hint_bytes_is_1024() {
155 let cfg = McpConfig::default();
156 assert_eq!(cfg.output_schema_hint_bytes, 1024);
157 }
158
159 #[test]
160 fn max_connect_attempts_default_is_3() {
161 let cfg = McpConfig::default();
162 assert_eq!(cfg.max_connect_attempts, 3);
163 }
164
165 #[test]
166 fn max_connect_attempts_accepts_valid_range() {
167 for v in [1u8, 3, 10] {
168 let src = format!("max_connect_attempts = {v}\n");
169 let cfg: McpConfig = toml::from_str(&src)
170 .unwrap_or_else(|e| panic!("max_connect_attempts = {v} should be valid, got: {e}"));
171 assert_eq!(cfg.max_connect_attempts, v);
172 }
173 }
174
175 #[test]
176 fn max_connect_attempts_rejects_zero() {
177 let src = "max_connect_attempts = 0\n";
178 let result = toml::from_str::<McpConfig>(src);
179 assert!(
180 result.is_err(),
181 "max_connect_attempts = 0 should be rejected"
182 );
183 let msg = result.unwrap_err().to_string();
184 assert!(
185 msg.contains("max_connect_attempts"),
186 "error message should mention the field name, got: {msg}"
187 );
188 }
189
190 #[test]
191 fn max_connect_attempts_rejects_eleven() {
192 let src = "max_connect_attempts = 11\n";
193 let result = toml::from_str::<McpConfig>(src);
194 assert!(
195 result.is_err(),
196 "max_connect_attempts = 11 should be rejected"
197 );
198 }
199
200 #[test]
201 fn startup_retry_backoff_ms_default_is_1000() {
202 let cfg = McpConfig::default();
203 assert_eq!(cfg.startup_retry_backoff_ms, 1000);
204 }
205
206 #[test]
207 fn startup_retry_backoff_ms_deserializes_from_toml() {
208 let src = "startup_retry_backoff_ms = 500\n";
209 let cfg: McpConfig = toml::from_str(src).expect("valid toml");
210 assert_eq!(cfg.startup_retry_backoff_ms, 500);
211 }
212
213 #[test]
214 fn tool_timeout_secs_default_is_none() {
215 let cfg = McpConfig::default();
216 assert!(cfg.tool_timeout_secs.is_none());
217 }
218
219 #[test]
220 fn tool_timeout_secs_deserializes_from_toml() {
221 let src = "tool_timeout_secs = 120\n";
222 let cfg: McpConfig = toml::from_str(src).expect("valid toml");
223 assert_eq!(cfg.tool_timeout_secs, Some(120));
224 }
225
226 #[test]
227 fn tool_timeout_secs_rejects_above_3600() {
228 let src = "tool_timeout_secs = 3601\n";
229 assert!(toml::from_str::<McpConfig>(src).is_err());
230 }
231
232 #[test]
233 fn tool_timeout_secs_accepts_3600() {
234 let src = "tool_timeout_secs = 3600\n";
235 let cfg: McpConfig = toml::from_str(src).expect("valid toml");
236 assert_eq!(cfg.tool_timeout_secs, Some(3600));
237 }
238
239 #[test]
240 fn wildcard_star_allows_any_skill() {
241 let cfg = allow(&["*"]);
242 assert!(is_skill_allowed("anything", &cfg));
243 assert!(is_skill_allowed("web-search", &cfg));
244 }
245
246 #[test]
247 fn empty_allowlist_denies_all() {
248 let cfg = allow(&[]);
249 assert!(!is_skill_allowed("web-search", &cfg));
250 assert!(!is_skill_allowed("shell", &cfg));
251 }
252
253 #[test]
254 fn exact_match_allows_only_that_skill() {
255 let cfg = allow(&["web-search"]);
256 assert!(is_skill_allowed("web-search", &cfg));
257 assert!(!is_skill_allowed("shell", &cfg));
258 assert!(!is_skill_allowed("web-search-extra", &cfg));
259 }
260
261 #[test]
262 fn prefix_wildcard_allows_matching_skills() {
263 let cfg = allow(&["web-*"]);
264 assert!(is_skill_allowed("web-search", &cfg));
265 assert!(is_skill_allowed("web-fetch", &cfg));
266 assert!(!is_skill_allowed("shell", &cfg));
267 assert!(!is_skill_allowed("awesome-web-thing", &cfg));
268 }
269
270 #[test]
271 fn multiple_patterns_or_logic() {
272 let cfg = allow(&["shell", "web-*"]);
273 assert!(is_skill_allowed("shell", &cfg));
274 assert!(is_skill_allowed("web-search", &cfg));
275 assert!(!is_skill_allowed("memory", &cfg));
276 }
277
278 #[test]
279 fn default_config_allows_all() {
280 let cfg = ChannelSkillsConfig::default();
281 assert!(is_skill_allowed("any-skill", &cfg));
282 }
283
284 #[test]
285 fn prefix_wildcard_does_not_match_empty_suffix() {
286 let cfg = allow(&["web-*"]);
287 assert!(is_skill_allowed("web-", &cfg));
291 }
292
293 #[test]
294 fn matching_is_case_sensitive() {
295 let cfg = allow(&["Web-Search"]);
296 assert!(!is_skill_allowed("web-search", &cfg));
297 assert!(is_skill_allowed("Web-Search", &cfg));
298 }
299}
300
301fn default_slack_port() -> u16 {
302 3000
303}
304
305fn default_slack_webhook_host() -> String {
306 "127.0.0.1".into()
307}
308
309fn default_a2a_host() -> String {
310 "0.0.0.0".into()
311}
312
313fn default_a2a_port() -> u16 {
314 8080
315}
316
317fn default_a2a_rate_limit() -> u32 {
318 60
319}
320
321fn default_a2a_max_body() -> usize {
322 1_048_576
323}
324
325fn default_drain_timeout_ms() -> u64 {
326 30_000
327}
328
329fn default_max_dynamic_servers() -> usize {
330 10
331}
332
333fn default_mcp_timeout() -> u64 {
334 30
335}
336
337fn default_startup_retry_backoff_ms() -> u64 {
338 1000
339}
340
341fn default_tool_timeout_secs() -> Option<u64> {
342 None
343}
344
345fn default_oauth_callback_port() -> u16 {
346 18766
347}
348
349fn default_oauth_client_name() -> String {
350 "Zeph".into()
351}
352
353fn default_stream_interval_ms() -> u64 {
354 3000
355}
356
357fn default_max_bot_chain_depth() -> u32 {
358 1
359}
360
361#[derive(Clone, Deserialize, Serialize)]
378pub struct TelegramConfig {
379 pub token: Option<String>,
381 #[serde(default)]
383 pub allowed_users: Vec<String>,
384 #[serde(default)]
386 pub skills: ChannelSkillsConfig,
387 #[serde(default)]
390 pub allowed_tools: Option<Vec<String>>,
391 #[serde(default = "default_stream_interval_ms")]
397 pub stream_interval_ms: u64,
398 #[serde(default)]
402 pub guest_mode: bool,
403 #[serde(default)]
407 pub bot_to_bot: bool,
408 #[serde(default)]
412 pub allowed_bots: Vec<String>,
413 #[serde(default = "default_max_bot_chain_depth")]
424 pub max_bot_chain_depth: u32,
425}
426
427impl std::fmt::Debug for TelegramConfig {
428 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
429 f.debug_struct("TelegramConfig")
430 .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
431 .field("allowed_users", &self.allowed_users)
432 .field("skills", &self.skills)
433 .field("allowed_tools", &self.allowed_tools)
434 .field("stream_interval_ms", &self.stream_interval_ms)
435 .field("guest_mode", &self.guest_mode)
436 .field("bot_to_bot", &self.bot_to_bot)
437 .field("allowed_bots_count", &self.allowed_bots.len())
438 .field("max_bot_chain_depth", &self.max_bot_chain_depth)
439 .finish()
440 }
441}
442
443#[derive(Clone, Deserialize, Serialize)]
444pub struct DiscordConfig {
445 pub token: Option<String>,
446 pub application_id: Option<String>,
447 #[serde(default)]
448 pub allowed_user_ids: Vec<String>,
449 #[serde(default)]
450 pub allowed_role_ids: Vec<String>,
451 #[serde(default)]
452 pub allowed_channel_ids: Vec<String>,
453 #[serde(default)]
454 pub skills: ChannelSkillsConfig,
455 #[serde(default)]
457 pub allowed_tools: Option<Vec<String>>,
458}
459
460impl std::fmt::Debug for DiscordConfig {
461 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
462 f.debug_struct("DiscordConfig")
463 .field("token", &self.token.as_ref().map(|_| "[REDACTED]"))
464 .field("application_id", &self.application_id)
465 .field("allowed_user_ids", &self.allowed_user_ids)
466 .field("allowed_role_ids", &self.allowed_role_ids)
467 .field("allowed_channel_ids", &self.allowed_channel_ids)
468 .field("skills", &self.skills)
469 .field("allowed_tools", &self.allowed_tools)
470 .finish()
471 }
472}
473
474#[derive(Clone, Deserialize, Serialize)]
475pub struct SlackConfig {
476 pub bot_token: Option<String>,
477 pub signing_secret: Option<String>,
478 #[serde(default = "default_slack_webhook_host")]
479 pub webhook_host: String,
480 #[serde(default = "default_slack_port")]
481 pub port: u16,
482 #[serde(default)]
483 pub allowed_user_ids: Vec<String>,
484 #[serde(default)]
485 pub allowed_channel_ids: Vec<String>,
486 #[serde(default)]
487 pub skills: ChannelSkillsConfig,
488 #[serde(default)]
490 pub allowed_tools: Option<Vec<String>>,
491}
492
493impl std::fmt::Debug for SlackConfig {
494 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
495 f.debug_struct("SlackConfig")
496 .field("bot_token", &self.bot_token.as_ref().map(|_| "[REDACTED]"))
497 .field(
498 "signing_secret",
499 &self.signing_secret.as_ref().map(|_| "[REDACTED]"), )
501 .field("webhook_host", &self.webhook_host)
502 .field("port", &self.port)
503 .field("allowed_user_ids", &self.allowed_user_ids)
504 .field("allowed_channel_ids", &self.allowed_channel_ids)
505 .field("skills", &self.skills)
506 .field("allowed_tools", &self.allowed_tools)
507 .finish()
508 }
509}
510
511#[derive(Debug, Clone, Deserialize, Serialize)]
515pub struct IbctKeyConfig {
516 pub key_id: String,
518 pub key_hex: String,
520}
521
522fn default_ibct_ttl() -> u64 {
523 300
524}
525
526fn default_a2a_request_timeout_ms() -> u64 {
527 300_000
528}
529
530fn default_task_ttl_secs() -> u64 {
531 3600
532}
533
534#[derive(Deserialize, Serialize)]
540#[allow(clippy::struct_excessive_bools)] pub struct A2aServerConfig {
542 #[serde(default)]
543 pub enabled: bool,
544 #[serde(default = "default_a2a_host")]
545 pub host: String,
546 #[serde(default = "default_a2a_port")]
547 pub port: u16,
548 #[serde(default)]
549 pub public_url: String,
550 #[serde(default)]
551 pub auth_token: Option<String>,
552 #[serde(default = "default_a2a_rate_limit")]
553 pub rate_limit: u32,
554 #[serde(default = "default_true")]
555 pub require_tls: bool,
556 #[serde(default = "default_true")]
557 pub ssrf_protection: bool,
558 #[serde(default = "default_a2a_max_body")]
559 pub max_body_size: usize,
560 #[serde(default = "default_drain_timeout_ms")]
561 pub drain_timeout_ms: u64,
562 #[serde(default)]
566 pub require_auth: bool,
567 #[serde(default)]
572 pub ibct_keys: Vec<IbctKeyConfig>,
573 #[serde(default)]
579 pub ibct_signing_key_vault_ref: Option<String>,
580 #[serde(default = "default_ibct_ttl")]
582 pub ibct_ttl_secs: u64,
583 #[serde(default)]
597 pub advertise_files: bool,
598 #[serde(default = "default_a2a_request_timeout_ms")]
604 pub request_timeout_ms: u64,
605 #[serde(default = "default_task_ttl_secs")]
614 pub task_ttl_secs: u64,
615}
616
617impl std::fmt::Debug for A2aServerConfig {
618 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
619 f.debug_struct("A2aServerConfig")
620 .field("enabled", &self.enabled)
621 .field("host", &self.host)
622 .field("port", &self.port)
623 .field("public_url", &self.public_url)
624 .field(
625 "auth_token",
626 &self.auth_token.as_ref().map(|_| "[REDACTED]"),
627 )
628 .field("rate_limit", &self.rate_limit)
629 .field("require_tls", &self.require_tls)
630 .field("ssrf_protection", &self.ssrf_protection)
631 .field("max_body_size", &self.max_body_size)
632 .field("drain_timeout_ms", &self.drain_timeout_ms)
633 .field("require_auth", &self.require_auth)
634 .field("ibct_keys_count", &self.ibct_keys.len())
635 .field(
636 "ibct_signing_key_vault_ref",
637 &self.ibct_signing_key_vault_ref,
638 )
639 .field("ibct_ttl_secs", &self.ibct_ttl_secs)
640 .field("advertise_files", &self.advertise_files)
641 .field("request_timeout_ms", &self.request_timeout_ms)
642 .field("task_ttl_secs", &self.task_ttl_secs)
643 .finish()
644 }
645}
646
647impl Default for A2aServerConfig {
648 fn default() -> Self {
649 Self {
650 enabled: false,
651 host: default_a2a_host(),
652 port: default_a2a_port(),
653 public_url: String::new(),
654 auth_token: None,
655 rate_limit: default_a2a_rate_limit(),
656 require_tls: true,
657 ssrf_protection: true,
658 max_body_size: default_a2a_max_body(),
659 drain_timeout_ms: default_drain_timeout_ms(),
660 require_auth: false,
661 ibct_keys: Vec::new(),
662 ibct_signing_key_vault_ref: None,
663 ibct_ttl_secs: default_ibct_ttl(),
664 advertise_files: false,
665 request_timeout_ms: default_a2a_request_timeout_ms(),
666 task_ttl_secs: default_task_ttl_secs(),
667 }
668 }
669}
670
671#[derive(Debug, Clone, Deserialize, Serialize)]
677#[serde(default)]
678pub struct ToolPruningConfig {
679 pub enabled: bool,
681 pub max_tools: usize,
683 pub pruning_provider: ProviderName,
686 pub min_tools_to_prune: usize,
688 pub always_include: Vec<String>,
690}
691
692impl Default for ToolPruningConfig {
693 fn default() -> Self {
694 Self {
695 enabled: false,
696 max_tools: 15,
697 pruning_provider: ProviderName::default(),
698 min_tools_to_prune: 10,
699 always_include: Vec::new(),
700 }
701 }
702}
703
704#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
709#[serde(rename_all = "lowercase")]
710#[non_exhaustive]
711pub enum ToolDiscoveryStrategyConfig {
712 Embedding,
714 Llm,
716 #[default]
718 None,
719}
720
721#[derive(Debug, Clone, Deserialize, Serialize)]
727#[serde(default)]
728pub struct ToolDiscoveryConfig {
729 pub strategy: ToolDiscoveryStrategyConfig,
731 pub top_k: usize,
733 pub min_similarity: f32,
735 pub embedding_provider: ProviderName,
739 pub always_include: Vec<String>,
741 pub min_tools_to_filter: usize,
743 pub strict: bool,
746}
747
748impl Default for ToolDiscoveryConfig {
749 fn default() -> Self {
750 Self {
751 strategy: ToolDiscoveryStrategyConfig::None,
752 top_k: 10,
753 min_similarity: 0.2,
754 embedding_provider: ProviderName::default(),
755 always_include: Vec::new(),
756 min_tools_to_filter: 10,
757 strict: false,
758 }
759 }
760}
761
762#[derive(Debug, Clone, Deserialize, Serialize)]
764#[allow(clippy::struct_excessive_bools)] pub struct TrustCalibrationConfig {
766 #[serde(default)]
768 pub enabled: bool,
769 #[serde(default = "default_true")]
771 pub probe_on_connect: bool,
772 #[serde(default = "default_true")]
774 pub monitor_invocations: bool,
775 #[serde(default = "default_true")]
777 pub persist_scores: bool,
778 #[serde(default = "default_decay_rate")]
780 pub decay_rate_per_day: f64,
781 #[serde(default = "default_injection_penalty")]
783 pub injection_penalty: f64,
784 #[serde(default)]
786 pub verifier_provider: ProviderName,
787}
788
789fn default_decay_rate() -> f64 {
790 0.01
791}
792
793fn default_injection_penalty() -> f64 {
794 0.25
795}
796
797impl Default for TrustCalibrationConfig {
798 fn default() -> Self {
799 Self {
800 enabled: false,
801 probe_on_connect: true,
802 monitor_invocations: true,
803 persist_scores: true,
804 decay_rate_per_day: default_decay_rate(),
805 injection_penalty: default_injection_penalty(),
806 verifier_provider: ProviderName::default(),
807 }
808 }
809}
810
811fn default_max_description_bytes() -> usize {
812 2048
813}
814
815fn default_max_instructions_bytes() -> usize {
816 2048
817}
818
819fn default_elicitation_timeout() -> u64 {
820 120
821}
822
823fn default_elicitation_queue_capacity() -> usize {
824 16
825}
826
827fn default_output_schema_hint_bytes() -> usize {
828 1024
829}
830
831fn default_max_connect_attempts() -> u8 {
832 3
833}
834
835fn validate_max_connect_attempts<'de, D>(d: D) -> Result<u8, D::Error>
836where
837 D: serde::Deserializer<'de>,
838{
839 let v = u8::deserialize(d)?;
840 if !(1..=10).contains(&v) {
841 return Err(serde::de::Error::custom(format!(
842 "mcp.max_connect_attempts must be in 1..=10 (got {v})"
843 )));
844 }
845 Ok(v)
846}
847
848fn validate_tool_timeout_secs<'de, D>(d: D) -> Result<Option<u64>, D::Error>
849where
850 D: serde::Deserializer<'de>,
851{
852 let v = Option::<u64>::deserialize(d)?;
853 if let Some(n) = v
854 && n > 3600
855 {
856 return Err(serde::de::Error::custom(format!(
857 "mcp.tool_timeout_secs must be \u{2264} 3600 (got {n})"
858 )));
859 }
860 Ok(v)
861}
862
863#[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone, Deserialize, Serialize)]
865pub struct McpConfig {
866 #[serde(default)]
867 pub servers: Vec<McpServerConfig>,
868 #[serde(default)]
869 pub allowed_commands: Vec<String>,
870 #[serde(default = "default_max_dynamic_servers")]
871 pub max_dynamic_servers: usize,
872 #[serde(default)]
874 pub pruning: ToolPruningConfig,
875 #[serde(default)]
877 pub trust_calibration: TrustCalibrationConfig,
878 #[serde(default)]
880 pub tool_discovery: ToolDiscoveryConfig,
881 #[serde(default = "default_max_description_bytes")]
883 pub max_description_bytes: usize,
884 #[serde(default = "default_max_instructions_bytes")]
886 pub max_instructions_bytes: usize,
887 #[serde(default)]
891 pub elicitation_enabled: bool,
892 #[serde(default = "default_elicitation_timeout")]
894 pub elicitation_timeout: u64,
895 #[serde(default = "default_elicitation_queue_capacity")]
899 pub elicitation_queue_capacity: usize,
900 #[serde(default = "default_true")]
903 pub elicitation_warn_sensitive_fields: bool,
904 #[serde(
916 default = "default_max_connect_attempts",
917 deserialize_with = "validate_max_connect_attempts"
918 )]
919 pub max_connect_attempts: u8,
920 #[serde(default)]
926 pub lock_tool_list: bool,
927 #[serde(default)]
932 pub default_env_isolation: bool,
933 #[serde(default)]
941 pub forward_output_schema: bool,
942 #[serde(default = "default_output_schema_hint_bytes")]
948 pub output_schema_hint_bytes: usize,
949 #[serde(default = "default_startup_retry_backoff_ms")]
956 pub startup_retry_backoff_ms: u64,
957 #[serde(
967 default = "default_tool_timeout_secs",
968 deserialize_with = "validate_tool_timeout_secs"
969 )]
970 pub tool_timeout_secs: Option<u64>,
971}
972
973impl Default for McpConfig {
974 fn default() -> Self {
975 Self {
976 servers: Vec::new(),
977 allowed_commands: Vec::new(),
978 max_dynamic_servers: default_max_dynamic_servers(),
979 pruning: ToolPruningConfig::default(),
980 trust_calibration: TrustCalibrationConfig::default(),
981 tool_discovery: ToolDiscoveryConfig::default(),
982 max_description_bytes: default_max_description_bytes(),
983 max_instructions_bytes: default_max_instructions_bytes(),
984 elicitation_enabled: false,
985 elicitation_timeout: default_elicitation_timeout(),
986 elicitation_queue_capacity: default_elicitation_queue_capacity(),
987 elicitation_warn_sensitive_fields: true,
988 lock_tool_list: false,
989 default_env_isolation: false,
990 forward_output_schema: false,
991 output_schema_hint_bytes: default_output_schema_hint_bytes(),
992 max_connect_attempts: default_max_connect_attempts(),
993 startup_retry_backoff_ms: default_startup_retry_backoff_ms(),
994 tool_timeout_secs: None,
995 }
996 }
997}
998
999#[derive(Clone, Deserialize, Serialize)]
1000pub struct McpServerConfig {
1001 pub id: String,
1002 pub command: Option<String>,
1004 #[serde(default)]
1005 pub args: Vec<String>,
1006 #[serde(default)]
1007 pub env: HashMap<String, String>,
1008 pub url: Option<String>,
1010 #[serde(default = "default_mcp_timeout")]
1011 pub timeout: u64,
1012 #[serde(default)]
1014 pub policy: McpPolicy,
1015 #[serde(default)]
1018 pub headers: HashMap<String, String>,
1019 #[serde(default)]
1021 pub oauth: Option<McpOAuthConfig>,
1022 #[serde(default)]
1024 pub trust_level: McpTrustLevel,
1025 #[serde(default)]
1029 pub tool_allowlist: Option<Vec<String>>,
1030 #[serde(default)]
1036 pub expected_tools: Vec<String>,
1037 #[serde(default)]
1041 pub roots: Vec<McpRootEntry>,
1042 #[serde(default)]
1045 pub tool_metadata: HashMap<String, ToolSecurityMeta>,
1046 #[serde(default)]
1050 pub elicitation_enabled: Option<bool>,
1051 #[serde(default)]
1058 pub env_isolation: Option<bool>,
1059}
1060
1061#[derive(Debug, Clone, Deserialize, Serialize)]
1063pub struct McpRootEntry {
1064 pub uri: String,
1066 #[serde(default)]
1068 pub name: Option<String>,
1069}
1070
1071#[derive(Debug, Clone, Deserialize, Serialize)]
1073pub struct McpOAuthConfig {
1074 #[serde(default)]
1076 pub enabled: bool,
1077 #[serde(default)]
1079 pub token_storage: OAuthTokenStorage,
1080 #[serde(default)]
1082 pub scopes: Vec<String>,
1083 #[serde(default = "default_oauth_callback_port")]
1085 pub callback_port: u16,
1086 #[serde(default = "default_oauth_client_name")]
1088 pub client_name: String,
1089}
1090
1091impl Default for McpOAuthConfig {
1092 fn default() -> Self {
1093 Self {
1094 enabled: false,
1095 token_storage: OAuthTokenStorage::default(),
1096 scopes: Vec::new(),
1097 callback_port: default_oauth_callback_port(),
1098 client_name: default_oauth_client_name(),
1099 }
1100 }
1101}
1102
1103#[derive(Debug, Clone, Default, Deserialize, Serialize)]
1105#[serde(rename_all = "lowercase")]
1106#[non_exhaustive]
1107pub enum OAuthTokenStorage {
1108 #[default]
1110 Vault,
1111 Memory,
1113}
1114
1115impl std::fmt::Debug for McpServerConfig {
1116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1117 let redacted_env: HashMap<&str, &str> = self
1118 .env
1119 .keys()
1120 .map(|k| (k.as_str(), "[REDACTED]"))
1121 .collect();
1122 let redacted_headers: HashMap<&str, &str> = self
1124 .headers
1125 .keys()
1126 .map(|k| (k.as_str(), "[REDACTED]"))
1127 .collect();
1128 f.debug_struct("McpServerConfig")
1129 .field("id", &self.id)
1130 .field("command", &self.command)
1131 .field("args", &self.args)
1132 .field("env", &redacted_env)
1133 .field("url", &self.url)
1134 .field("timeout", &self.timeout)
1135 .field("policy", &self.policy)
1136 .field("headers", &redacted_headers)
1137 .field("oauth", &self.oauth)
1138 .field("trust_level", &self.trust_level)
1139 .field("tool_allowlist", &self.tool_allowlist)
1140 .field("expected_tools", &self.expected_tools)
1141 .field("roots", &self.roots)
1142 .field(
1143 "tool_metadata_keys",
1144 &self.tool_metadata.keys().collect::<Vec<_>>(),
1145 )
1146 .field("elicitation_enabled", &self.elicitation_enabled)
1147 .field("env_isolation", &self.env_isolation)
1148 .finish()
1149 }
1150}