1use futures::future::BoxFuture;
2use rmcp::{
3 RoleServer, service::DynService,
4 transport::streamable_http_client::StreamableHttpClientTransportConfig,
5};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::HashMap;
9use std::path::Path;
10
11use super::variables::{VarError, expand_env_vars};
12
13#[derive(Debug, Clone, Deserialize, Serialize)]
15pub struct RawMcpConfig {
16 pub servers: HashMap<String, RawMcpServerConfig>,
17}
18
19#[derive(Debug, Clone, Deserialize, Serialize)]
21#[serde(tag = "type", rename_all = "lowercase")]
22pub enum RawMcpServerConfig {
23 Stdio {
24 command: String,
25
26 #[serde(default)]
27 args: Vec<String>,
28
29 #[serde(default)]
30 env: HashMap<String, String>,
31 },
32
33 Http {
34 url: String,
35
36 #[serde(default)]
37 headers: HashMap<String, String>,
38 },
39
40 Sse {
41 url: String,
42
43 #[serde(default)]
44 headers: HashMap<String, String>,
45 },
46
47 #[serde(rename = "in-memory")]
54 InMemory {
55 #[serde(default)]
56 args: Vec<String>,
57
58 #[serde(default)]
59 input: Option<Value>,
60 },
61}
62
63pub enum ServerConfig {
65 Http {
66 name: String,
67 config: StreamableHttpClientTransportConfig,
68 },
69
70 Stdio {
71 name: String,
72 command: String,
73 args: Vec<String>,
74 env: HashMap<String, String>,
75 },
76
77 InMemory {
78 name: String,
79 server: Box<dyn DynService<RoleServer>>,
80 },
81}
82
83impl ServerConfig {
84 pub fn name(&self) -> &str {
85 match self {
86 ServerConfig::Http { name, .. }
87 | ServerConfig::Stdio { name, .. }
88 | ServerConfig::InMemory { name, .. } => name,
89 }
90 }
91}
92
93impl std::fmt::Debug for ServerConfig {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 ServerConfig::Http { name, config } => f
97 .debug_struct("Http")
98 .field("name", name)
99 .field("config", config)
100 .finish(),
101 ServerConfig::Stdio {
102 name,
103 command,
104 args,
105 env,
106 } => f
107 .debug_struct("Stdio")
108 .field("name", name)
109 .field("command", command)
110 .field("args", args)
111 .field("env", env)
112 .finish(),
113 ServerConfig::InMemory { name, .. } => f
114 .debug_struct("InMemory")
115 .field("name", name)
116 .field("server", &"<DynService>")
117 .finish(),
118 }
119 }
120}
121
122pub enum McpServerConfig {
124 Server(ServerConfig),
125 ToolProxy {
126 name: String,
127 servers: Vec<ServerConfig>,
128 },
129}
130
131impl McpServerConfig {
132 pub fn name(&self) -> &str {
133 match self {
134 McpServerConfig::Server(cfg) => cfg.name(),
135 McpServerConfig::ToolProxy { name, .. } => name,
136 }
137 }
138}
139
140impl From<ServerConfig> for McpServerConfig {
141 fn from(cfg: ServerConfig) -> Self {
142 McpServerConfig::Server(cfg)
143 }
144}
145
146impl std::fmt::Debug for McpServerConfig {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 match self {
149 McpServerConfig::Server(cfg) => cfg.fmt(f),
150 McpServerConfig::ToolProxy { name, servers } => f
151 .debug_struct("ToolProxy")
152 .field("name", name)
153 .field("servers", &format!("{} nested servers", servers.len()))
154 .finish(),
155 }
156 }
157}
158
159pub type ServerFactory = Box<
163 dyn Fn(Vec<String>, Option<Value>) -> BoxFuture<'static, Box<dyn DynService<RoleServer>>>
164 + Send
165 + Sync,
166>;
167
168#[derive(Debug)]
169pub enum ParseError {
170 IoError(std::io::Error),
171 JsonError(serde_json::Error),
172 VarError(VarError),
173 FactoryNotFound(String),
174 InvalidNestedConfig(String),
175}
176
177impl std::fmt::Display for ParseError {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 match self {
180 ParseError::IoError(e) => write!(f, "Failed to read config file: {e}"),
181 ParseError::JsonError(e) => write!(f, "Invalid JSON: {e}"),
182 ParseError::VarError(e) => write!(f, "Variable expansion failed: {e}"),
183 ParseError::FactoryNotFound(name) => {
184 write!(f, "InMemory server factory '{name}' not registered")
185 }
186 ParseError::InvalidNestedConfig(msg) => {
187 write!(f, "Invalid nested config in tool-proxy: {msg}")
188 }
189 }
190 }
191}
192
193impl std::error::Error for ParseError {}
194
195impl From<std::io::Error> for ParseError {
196 fn from(error: std::io::Error) -> Self {
197 ParseError::IoError(error)
198 }
199}
200
201impl From<serde_json::Error> for ParseError {
202 fn from(error: serde_json::Error) -> Self {
203 ParseError::JsonError(error)
204 }
205}
206
207impl From<VarError> for ParseError {
208 fn from(error: VarError) -> Self {
209 ParseError::VarError(error)
210 }
211}
212
213impl RawMcpConfig {
214 pub fn from_json_file(path: impl AsRef<Path>) -> Result<Self, ParseError> {
216 let content = std::fs::read_to_string(path)?;
217 Self::from_json(&content)
218 }
219
220 pub fn from_json(json: &str) -> Result<Self, ParseError> {
222 Ok(serde_json::from_str(json)?)
223 }
224
225 pub async fn into_configs(
227 self,
228 factories: &HashMap<String, ServerFactory>,
229 ) -> Result<Vec<McpServerConfig>, ParseError> {
230 let mut configs = Vec::with_capacity(self.servers.len());
231 for (name, raw_config) in self.servers {
232 configs.push(raw_config.into_config(name, factories).await?);
233 }
234 Ok(configs)
235 }
236}
237
238impl RawMcpServerConfig {
239 pub async fn into_config(
241 self,
242 name: String,
243 factories: &HashMap<String, ServerFactory>,
244 ) -> Result<McpServerConfig, ParseError> {
245 match self {
246 RawMcpServerConfig::Stdio { command, args, env } => Ok(ServerConfig::Stdio {
247 name,
248 command: expand_env_vars(&command)?,
249 args: args
250 .into_iter()
251 .map(|a| expand_env_vars(&a))
252 .collect::<Result<Vec<_>, _>>()?,
253 env: env
254 .into_iter()
255 .map(|(k, v)| Ok((k, expand_env_vars(&v)?)))
256 .collect::<Result<HashMap<_, _>, VarError>>()?,
257 }
258 .into()),
259
260 RawMcpServerConfig::Http { url, headers }
261 | RawMcpServerConfig::Sse { url, headers } => {
262 let auth_header = headers
264 .get("Authorization")
265 .map(|v| expand_env_vars(v))
266 .transpose()?;
267
268 let mut config =
269 StreamableHttpClientTransportConfig::with_uri(expand_env_vars(&url)?);
270 if let Some(auth) = auth_header {
271 config = config.auth_header(auth);
272 }
273 Ok(ServerConfig::Http { name, config }.into())
274 }
275
276 RawMcpServerConfig::InMemory { args, input } => {
277 let servers_val = input.as_ref().and_then(|v| v.get("servers"));
278
279 if let Some(servers_val) = servers_val {
280 return parse_tool_proxy(name, servers_val, factories).await;
281 }
282
283 let server_factory = factories
284 .get(&name)
285 .ok_or_else(|| ParseError::FactoryNotFound(name.clone()))?;
286
287 let expanded_args = args
288 .into_iter()
289 .map(|a| expand_env_vars(&a))
290 .collect::<Result<Vec<_>, VarError>>()?;
291
292 let server = server_factory(expanded_args, input).await;
293 Ok(ServerConfig::InMemory { name, server }.into())
294 }
295 }
296 }
297
298 async fn into_server_config(
301 self,
302 name: String,
303 factories: &HashMap<String, ServerFactory>,
304 ) -> Result<ServerConfig, ParseError> {
305 match self.into_config(name, factories).await? {
306 McpServerConfig::Server(cfg) => Ok(cfg),
307 McpServerConfig::ToolProxy { name, .. } => Err(ParseError::InvalidNestedConfig(
308 format!("tool-proxy '{name}' cannot be nested inside another tool-proxy"),
309 )),
310 }
311 }
312}
313
314async fn parse_tool_proxy(
315 name: String,
316 servers_val: &Value,
317 factories: &HashMap<String, ServerFactory>,
318) -> Result<McpServerConfig, ParseError> {
319 let nested_raw: HashMap<String, RawMcpServerConfig> =
320 serde_json::from_value(servers_val.clone()).map_err(|e| {
321 ParseError::InvalidNestedConfig(format!("failed to parse input.servers: {e}"))
322 })?;
323
324 let mut nested_configs = Vec::with_capacity(nested_raw.len());
325 for (nested_name, nested_raw_cfg) in nested_raw {
326 if matches!(nested_raw_cfg, RawMcpServerConfig::InMemory { .. }) {
327 return Err(ParseError::InvalidNestedConfig(format!(
328 "in-memory servers cannot be nested inside tool-proxy (server: '{nested_name}')"
329 )));
330 }
331
332 nested_configs
333 .push(Box::pin(nested_raw_cfg.into_server_config(nested_name, factories)).await?);
334 }
335
336 Ok(McpServerConfig::ToolProxy {
337 name,
338 servers: nested_configs,
339 })
340}