zagens_runtime_adapters/mcp/
config.rs1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use super::auth::McpAuthConfig;
6
7#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
9pub struct McpConfig {
10 #[serde(default)]
11 pub timeouts: McpTimeouts,
12 #[serde(default, alias = "mcpServers")]
13 pub servers: HashMap<String, McpServerConfig>,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
18#[allow(clippy::struct_field_names)]
19pub struct McpTimeouts {
20 #[serde(default = "default_connect_timeout")]
21 pub connect_timeout: u64,
22 #[serde(default = "default_execute_timeout")]
23 pub execute_timeout: u64,
24 #[serde(default = "default_read_timeout")]
25 pub read_timeout: u64,
26}
27
28fn default_connect_timeout() -> u64 {
29 30
30}
31fn default_execute_timeout() -> u64 {
32 60
33}
34fn default_read_timeout() -> u64 {
35 120
36}
37
38impl Default for McpTimeouts {
39 fn default() -> Self {
40 Self {
41 connect_timeout: default_connect_timeout(),
42 execute_timeout: default_execute_timeout(),
43 read_timeout: default_read_timeout(),
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum McpTransportKind {
53 Stdio,
55 Sse,
57 Http,
59}
60
61impl McpTransportKind {
62 #[must_use]
64 pub fn as_str(self) -> &'static str {
65 match self {
66 Self::Stdio => "stdio",
67 Self::Sse => "sse",
68 Self::Http => "http",
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
75pub struct McpServerConfig {
76 pub command: Option<String>,
77 #[serde(default)]
78 pub args: Vec<String>,
79 #[serde(default)]
80 pub env: HashMap<String, String>,
81 pub url: Option<String>,
82 #[serde(default, alias = "type", skip_serializing_if = "Option::is_none")]
87 pub transport: Option<String>,
88 #[serde(default)]
92 pub headers: HashMap<String, String>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub auth: Option<McpAuthConfig>,
97 #[serde(default)]
98 pub connect_timeout: Option<u64>,
99 #[serde(default)]
100 pub execute_timeout: Option<u64>,
101 #[serde(default)]
102 pub read_timeout: Option<u64>,
103 #[serde(default)]
104 pub disabled: bool,
105 #[serde(default = "default_enabled")]
106 pub enabled: bool,
107 #[serde(default)]
108 pub required: bool,
109 #[serde(default)]
110 pub enabled_tools: Vec<String>,
111 #[serde(default)]
112 pub disabled_tools: Vec<String>,
113}
114
115fn default_enabled() -> bool {
116 true
117}
118
119impl McpServerConfig {
120 pub fn effective_connect_timeout(&self, global: &McpTimeouts) -> u64 {
121 self.connect_timeout.unwrap_or(global.connect_timeout)
122 }
123
124 pub fn effective_execute_timeout(&self, global: &McpTimeouts) -> u64 {
125 self.execute_timeout.unwrap_or(global.execute_timeout)
126 }
127
128 pub fn effective_read_timeout(&self, global: &McpTimeouts) -> u64 {
129 self.read_timeout.unwrap_or(global.read_timeout)
130 }
131
132 pub fn is_enabled(&self) -> bool {
133 self.enabled && !self.disabled
134 }
135
136 pub fn transport_kind(&self) -> anyhow::Result<McpTransportKind> {
142 if let Some(raw) = &self.transport {
143 let kind = match raw.trim().to_ascii_lowercase().as_str() {
144 "stdio" => McpTransportKind::Stdio,
145 "sse" => McpTransportKind::Sse,
146 "http" | "streamable-http" | "streamable_http" | "streamablehttp"
147 | "http-stream" => McpTransportKind::Http,
148 other => anyhow::bail!(
149 "unknown MCP transport '{other}' (expected one of: stdio, sse, http)"
150 ),
151 };
152 match kind {
154 McpTransportKind::Stdio if self.command.is_none() => {
155 anyhow::bail!("transport 'stdio' requires a 'command'")
156 }
157 McpTransportKind::Sse | McpTransportKind::Http if self.url.is_none() => {
158 anyhow::bail!("transport '{}' requires a 'url'", kind.as_str())
159 }
160 _ => {}
161 }
162 return Ok(kind);
163 }
164
165 match (&self.command, &self.url) {
166 (Some(_), _) => Ok(McpTransportKind::Stdio),
167 (None, Some(_)) => Ok(McpTransportKind::Sse),
168 (None, None) => {
169 anyhow::bail!("MCP server config must have either 'command' or 'url'")
170 }
171 }
172 }
173
174 pub fn is_tool_enabled(&self, tool_name: &str) -> bool {
175 let allowed = if self.enabled_tools.is_empty() {
176 true
177 } else {
178 self.enabled_tools.iter().any(|t| t == tool_name)
179 };
180 if !allowed {
181 return false;
182 }
183 !self.disabled_tools.iter().any(|t| t == tool_name)
184 }
185}