use super::oauth_config::OAuthConfig;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum McpServerConfig {
#[serde(rename = "builtin")]
Builtin {
name: String,
timeout_seconds: u64,
tools: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
auto_bind: Option<Vec<String>>,
},
#[serde(rename = "http")]
Http {
name: String,
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
auth_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
oauth: Option<OAuthConfig>,
timeout_seconds: u64,
tools: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
auto_bind: Option<Vec<String>>,
},
#[serde(rename = "stdio")]
Stdin {
name: String,
command: String,
args: Vec<String>,
timeout_seconds: u64,
tools: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
auto_bind: Option<Vec<String>>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Hash)]
pub enum McpConnectionType {
Builtin,
Stdin,
Http,
}
impl McpServerConfig {
pub fn name(&self) -> &str {
match self {
McpServerConfig::Builtin { name, .. } => name,
McpServerConfig::Http { name, .. } => name,
McpServerConfig::Stdin { name, .. } => name,
}
}
pub fn connection_type(&self) -> McpConnectionType {
match self {
McpServerConfig::Builtin { .. } => McpConnectionType::Builtin,
McpServerConfig::Http { .. } => McpConnectionType::Http,
McpServerConfig::Stdin { .. } => McpConnectionType::Stdin,
}
}
pub fn timeout_seconds(&self) -> u64 {
match self {
McpServerConfig::Builtin {
timeout_seconds, ..
} => *timeout_seconds,
McpServerConfig::Http {
timeout_seconds, ..
} => *timeout_seconds,
McpServerConfig::Stdin {
timeout_seconds, ..
} => *timeout_seconds,
}
}
pub fn tools(&self) -> &[String] {
match self {
McpServerConfig::Builtin { tools, .. } => tools,
McpServerConfig::Http { tools, .. } => tools,
McpServerConfig::Stdin { tools, .. } => tools,
}
}
pub fn auto_bind_roles(&self) -> Option<&[String]> {
match self {
McpServerConfig::Builtin { auto_bind, .. } => auto_bind.as_deref(),
McpServerConfig::Http { auto_bind, .. } => auto_bind.as_deref(),
McpServerConfig::Stdin { auto_bind, .. } => auto_bind.as_deref(),
}
}
pub fn auto_binds_to(&self, role_name: &str) -> bool {
self.auto_bind_roles()
.map(|roles| roles.iter().any(|r| r == role_name))
.unwrap_or(false)
}
pub fn url(&self) -> Option<&str> {
match self {
McpServerConfig::Http { url, .. } => Some(url),
_ => None,
}
}
pub fn auth_token(&self) -> Option<&str> {
match self {
McpServerConfig::Http { auth_token, .. } => auth_token.as_deref(),
_ => None,
}
}
pub fn oauth_config(&self) -> Option<&OAuthConfig> {
match self {
McpServerConfig::Http { oauth, .. } => oauth.as_ref(),
_ => None,
}
}
pub fn has_oauth_config(&self) -> bool {
self.oauth_config().is_some()
}
pub fn is_oauth_enabled(&self) -> bool {
self.oauth_config().is_some()
}
pub fn requires_auth(&self) -> bool {
self.auth_token().is_some() || self.is_oauth_enabled()
}
pub fn command(&self) -> Option<&str> {
match self {
McpServerConfig::Stdin { command, .. } => Some(command),
_ => None,
}
}
pub fn args(&self) -> &[String] {
match self {
McpServerConfig::Stdin { args, .. } => args,
_ => &[],
}
}
pub fn builtin(name: &str, timeout_seconds: u64, tools: Vec<String>) -> Self {
Self::Builtin {
name: name.to_string(),
timeout_seconds,
tools,
auto_bind: None,
}
}
pub fn http(
name: &str,
url: &str,
timeout_seconds: u64,
tools: Vec<String>,
auth_token: Option<String>,
oauth: Option<OAuthConfig>,
) -> Self {
Self::Http {
name: name.to_string(),
url: url.to_string(),
auth_token,
oauth,
timeout_seconds,
tools,
auto_bind: None,
}
}
pub fn stdin(
name: &str,
command: &str,
args: Vec<String>,
timeout_seconds: u64,
tools: Vec<String>,
) -> Self {
Self::Stdin {
name: name.to_string(),
command: command.to_string(),
args,
timeout_seconds,
tools,
auto_bind: None,
}
}
pub fn with_auto_bind(&self, auto_bind: Option<Vec<String>>) -> Self {
match self {
McpServerConfig::Builtin {
name,
timeout_seconds,
tools,
..
} => McpServerConfig::Builtin {
name: name.clone(),
timeout_seconds: *timeout_seconds,
tools: tools.clone(),
auto_bind,
},
McpServerConfig::Http {
name,
url,
auth_token,
oauth,
timeout_seconds,
tools,
..
} => McpServerConfig::Http {
name: name.clone(),
url: url.clone(),
auth_token: auth_token.clone(),
oauth: oauth.clone(),
timeout_seconds: *timeout_seconds,
tools: tools.clone(),
auto_bind,
},
McpServerConfig::Stdin {
name,
command,
args,
timeout_seconds,
tools,
..
} => McpServerConfig::Stdin {
name: name.clone(),
command: command.clone(),
args: args.clone(),
timeout_seconds: *timeout_seconds,
tools: tools.clone(),
auto_bind,
},
}
}
pub fn validate(&self) -> Result<(), String> {
match self {
McpServerConfig::Builtin { name, .. } => {
if name.is_empty() {
return Err("Builtin server name cannot be empty".to_string());
}
}
McpServerConfig::Http {
name, url, oauth, ..
} => {
if name.is_empty() {
return Err("HTTP server name cannot be empty".to_string());
}
if url.is_empty() {
return Err("HTTP server URL cannot be empty".to_string());
}
if let Some(oauth_config) = oauth {
oauth_config
.validate()
.map_err(|e| format!("OAuth configuration validation failed: {}", e))?;
}
}
McpServerConfig::Stdin { name, command, .. } => {
if name.is_empty() {
return Err("Stdin server name cannot be empty".to_string());
}
if command.is_empty() {
return Err("Stdin server command cannot be empty".to_string());
}
}
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct McpConfig {
pub servers: Vec<McpServerConfig>,
pub allowed_tools: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct RoleMcpConfig {
pub server_refs: Vec<String>,
pub allowed_tools: Vec<String>,
}
impl RoleMcpConfig {
pub fn is_enabled(&self) -> bool {
!self.server_refs.is_empty()
}
pub fn get_enabled_servers(
&self,
global_servers: &[McpServerConfig],
role_name: Option<&str>,
) -> Vec<McpServerConfig> {
if self.server_refs.is_empty() && role_name.is_none() {
return Vec::new();
}
let mut result = Vec::new();
let mut added_names = std::collections::HashSet::new();
for server_name in &self.server_refs {
if let Some(server_config) = global_servers.iter().find(|s| s.name() == *server_name) {
let mut server = server_config.clone();
if !self.allowed_tools.is_empty() {
let filtered_tools = self.expand_patterns_for_server(server_name);
server = match server {
McpServerConfig::Builtin {
name,
timeout_seconds,
auto_bind,
..
} => McpServerConfig::Builtin {
name,
timeout_seconds,
tools: filtered_tools,
auto_bind,
},
McpServerConfig::Http {
name,
url,
auth_token,
oauth,
timeout_seconds,
auto_bind,
tools: _,
} => McpServerConfig::Http {
name,
url,
auth_token,
oauth,
timeout_seconds,
tools: filtered_tools,
auto_bind,
},
McpServerConfig::Stdin {
name,
command,
args,
timeout_seconds,
auto_bind,
..
} => McpServerConfig::Stdin {
name,
command,
args,
timeout_seconds,
tools: filtered_tools,
auto_bind,
},
};
}
result.push(server);
added_names.insert(server_name.clone());
} else {
crate::log_debug!(
"Server '{server_name}' referenced by role but not found in global registry"
);
}
}
if let Some(role) = role_name {
for server_config in global_servers {
if server_config.auto_binds_to(role) && !added_names.contains(server_config.name())
{
result.push(server_config.clone());
added_names.insert(server_config.name().to_string());
}
}
}
result
}
fn expand_patterns_for_server(&self, server_name: &str) -> Vec<String> {
let mut expanded_tools = Vec::new();
for pattern in &self.allowed_tools {
if let Some((server_prefix, tool_pattern)) = pattern.split_once(':') {
if server_prefix == server_name {
if tool_pattern == "*" {
return Vec::new();
} else if tool_pattern.ends_with('*') {
expanded_tools.push(tool_pattern.to_string());
} else {
expanded_tools.push(tool_pattern.to_string());
}
}
} else {
expanded_tools.push(pattern.clone());
}
}
expanded_tools
}
}