use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpConfig {
pub enabled: bool,
pub servers: HashMap<String, McpServerConfig>,
pub timeout_seconds: u64,
pub max_connections: usize,
pub auto_start: bool,
pub global_env: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerConfig {
pub name: String,
pub transport: McpTransportConfig,
pub enabled: bool,
pub auto_start: bool,
pub env: HashMap<String, String>,
pub timeout_seconds: Option<u64>,
pub working_directory: Option<PathBuf>,
pub auto_restart: bool,
pub max_restart_attempts: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum McpTransportConfig {
Stdio {
command: String,
args: Vec<String>,
},
Sse {
url: String,
headers: HashMap<String, String>,
},
}
impl Default for McpConfig {
fn default() -> Self {
Self {
enabled: false,
servers: HashMap::new(),
timeout_seconds: 30,
max_connections: 10,
auto_start: true,
global_env: HashMap::new(),
}
}
}
impl Default for McpServerConfig {
fn default() -> Self {
Self {
name: "Unnamed MCP Server".to_string(),
transport: McpTransportConfig::Stdio {
command: "echo".to_string(),
args: vec!["Hello from MCP".to_string()],
},
enabled: true,
auto_start: true,
env: HashMap::new(),
timeout_seconds: None,
working_directory: None,
auto_restart: true,
max_restart_attempts: 3,
}
}
}
impl McpConfig {
pub fn new() -> Self {
Self::default()
}
pub fn enable(&mut self) -> &mut Self {
self.enabled = true;
self
}
pub fn disable(&mut self) -> &mut Self {
self.enabled = false;
self
}
pub fn add_server(&mut self, name: String, config: McpServerConfig) -> &mut Self {
self.servers.insert(name, config);
self
}
pub fn remove_server(&mut self, name: &str) -> Option<McpServerConfig> {
self.servers.remove(name)
}
pub fn get_server(&self, name: &str) -> Option<&McpServerConfig> {
self.servers.get(name)
}
pub fn get_server_mut(&mut self, name: &str) -> Option<&mut McpServerConfig> {
self.servers.get_mut(name)
}
pub fn enabled_servers(&self) -> impl Iterator<Item = (&String, &McpServerConfig)> {
self.servers.iter().filter(|(_, config)| config.enabled)
}
pub fn auto_start_servers(&self) -> impl Iterator<Item = (&String, &McpServerConfig)> {
self.servers
.iter()
.filter(|(_, config)| config.enabled && config.auto_start)
}
pub fn validate(&self) -> Result<(), String> {
if self.timeout_seconds == 0 {
return Err("Timeout must be greater than 0".to_string());
}
if self.max_connections == 0 {
return Err("Max connections must be greater than 0".to_string());
}
for (name, server) in &self.servers {
server.validate().map_err(|e| format!("Server '{}': {}", name, e))?;
}
Ok(())
}
pub fn merged_env(&self, server_name: &str) -> HashMap<String, String> {
let mut env = self.global_env.clone();
if let Some(server) = self.get_server(server_name) {
env.extend(server.env.clone());
}
env
}
}
impl McpServerConfig {
pub fn stdio(name: String, command: String, args: Vec<String>) -> Self {
Self {
name,
transport: McpTransportConfig::Stdio { command, args },
..Default::default()
}
}
pub fn sse(name: String, url: String) -> Self {
Self {
name,
transport: McpTransportConfig::Sse {
url,
headers: HashMap::new(),
},
..Default::default()
}
}
pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
self.env = env;
self
}
pub fn add_env(mut self, key: String, value: String) -> Self {
self.env.insert(key, value);
self
}
pub fn with_timeout(mut self, timeout_seconds: u64) -> Self {
self.timeout_seconds = Some(timeout_seconds);
self
}
pub fn with_working_directory(mut self, dir: PathBuf) -> Self {
self.working_directory = Some(dir);
self
}
pub fn with_auto_restart(mut self, auto_restart: bool, max_attempts: u32) -> Self {
self.auto_restart = auto_restart;
self.max_restart_attempts = max_attempts;
self
}
pub fn validate(&self) -> Result<(), String> {
if self.name.is_empty() {
return Err("Server name cannot be empty".to_string());
}
match &self.transport {
McpTransportConfig::Stdio { command, .. } => {
if command.is_empty() {
return Err("Stdio command cannot be empty".to_string());
}
}
McpTransportConfig::Sse { url, .. } => {
if url.is_empty() {
return Err("SSE URL cannot be empty".to_string());
}
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err("SSE URL must start with http:// or https://".to_string());
}
}
}
if let Some(timeout) = self.timeout_seconds {
if timeout == 0 {
return Err("Server timeout must be greater than 0".to_string());
}
}
Ok(())
}
pub fn effective_timeout(&self, default_timeout: u64) -> u64 {
self.timeout_seconds.unwrap_or(default_timeout)
}
}
impl McpTransportConfig {
pub fn description(&self) -> String {
match self {
McpTransportConfig::Stdio { command, args } => {
if args.is_empty() {
format!("stdio: {}", command)
} else {
format!("stdio: {} {}", command, args.join(" "))
}
}
McpTransportConfig::Sse { url, .. } => {
format!("sse: {}", url)
}
}
}
pub fn is_stdio(&self) -> bool {
matches!(self, McpTransportConfig::Stdio { .. })
}
pub fn is_sse(&self) -> bool {
matches!(self, McpTransportConfig::Sse { .. })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mcp_config_default() {
let config = McpConfig::default();
assert!(!config.enabled);
assert_eq!(config.timeout_seconds, 30);
assert_eq!(config.max_connections, 10);
assert!(config.auto_start);
}
#[test]
fn test_mcp_config_validation() {
let mut config = McpConfig::default();
config.timeout_seconds = 0;
assert!(config.validate().is_err());
config.timeout_seconds = 30;
assert!(config.validate().is_ok());
}
#[test]
fn test_server_config_stdio() {
let config = McpServerConfig::stdio(
"test".to_string(),
"echo".to_string(),
vec!["hello".to_string()],
);
assert_eq!(config.name, "test");
assert!(config.transport.is_stdio());
assert!(config.validate().is_ok());
}
#[test]
fn test_server_config_sse() {
let config = McpServerConfig::sse(
"test".to_string(),
"https://example.com/mcp".to_string(),
);
assert_eq!(config.name, "test");
assert!(config.transport.is_sse());
assert!(config.validate().is_ok());
}
#[test]
fn test_transport_description() {
let stdio = McpTransportConfig::Stdio {
command: "echo".to_string(),
args: vec!["hello".to_string()],
};
assert_eq!(stdio.description(), "stdio: echo hello");
let sse = McpTransportConfig::Sse {
url: "https://example.com".to_string(),
headers: HashMap::new(),
};
assert_eq!(sse.description(), "sse: https://example.com");
}
}