use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum McpTransportType {
#[default]
Stdio,
Http,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct McpToolConfig {
#[serde(rename = "type", default)]
pub transport_type: McpTransportType,
#[serde(default)]
pub command: Option<String>,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: Option<Vec<(String, String)>>,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub auth_token: Option<String>,
#[serde(default)]
pub name: Option<String>,
}
impl McpToolConfig {
pub fn stdio(command: impl Into<String>, args: Vec<String>) -> Self {
Self {
transport_type: McpTransportType::Stdio,
command: Some(command.into()),
args,
..Default::default()
}
}
pub fn npx(package: impl Into<String>, extra_args: Vec<String>) -> Self {
let mut args = vec!["-y".to_string(), package.into()];
args.extend(extra_args);
Self::stdio("npx", args)
}
pub fn http(url: impl Into<String>) -> Self {
Self {
transport_type: McpTransportType::Http,
url: Some(url.into()),
..Default::default()
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_env(mut self, env: Vec<(String, String)>) -> Self {
self.env = Some(env);
self
}
pub fn with_auth_token(mut self, token: impl Into<String>) -> Self {
self.auth_token = Some(token.into());
self
}
pub fn validate(&self) -> Result<(), String> {
match self.transport_type {
McpTransportType::Stdio => {
if self.command.is_none() {
return Err("STDIO transport requires 'command' field".to_string());
}
}
McpTransportType::Http => {
if self.url.is_none() {
return Err("HTTP transport requires 'url' field".to_string());
}
}
}
Ok(())
}
}
#[cfg(feature = "mcp")]
impl McpToolConfig {
pub fn to_transport(&self) -> Result<enki_mcp::McpTransport, String> {
self.validate()?;
match self.transport_type {
McpTransportType::Stdio => Ok(enki_mcp::McpTransport::Stdio {
command: self.command.clone().unwrap(),
args: self.args.clone(),
env: self.env.clone(),
}),
McpTransportType::Http => Ok(enki_mcp::McpTransport::Http {
url: self.url.clone().unwrap(),
auth_token: self.auth_token.clone(),
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_stdio_config() {
let toml = r#"
type = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
"#;
let config: McpToolConfig = toml::from_str(toml).unwrap();
assert_eq!(config.transport_type, McpTransportType::Stdio);
assert_eq!(config.command, Some("npx".to_string()));
assert_eq!(config.args.len(), 3);
assert!(config.validate().is_ok());
}
#[test]
fn test_parse_http_config() {
let toml = r#"
type = "http"
url = "http://localhost:3000/mcp"
auth_token = "secret-token"
"#;
let config: McpToolConfig = toml::from_str(toml).unwrap();
assert_eq!(config.transport_type, McpTransportType::Http);
assert_eq!(config.url, Some("http://localhost:3000/mcp".to_string()));
assert_eq!(config.auth_token, Some("secret-token".to_string()));
assert!(config.validate().is_ok());
}
#[test]
fn test_builder_methods() {
let config = McpToolConfig::npx(
"@modelcontextprotocol/server-filesystem",
vec!["/home".into()],
)
.with_name("filesystem-tools");
assert_eq!(config.transport_type, McpTransportType::Stdio);
assert_eq!(config.command, Some("npx".to_string()));
assert_eq!(
config.args,
vec!["-y", "@modelcontextprotocol/server-filesystem", "/home"]
);
assert_eq!(config.name, Some("filesystem-tools".to_string()));
}
#[test]
fn test_validation_fails_without_command() {
let config = McpToolConfig {
transport_type: McpTransportType::Stdio,
command: None,
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_validation_fails_without_url() {
let config = McpToolConfig {
transport_type: McpTransportType::Http,
url: None,
..Default::default()
};
assert!(config.validate().is_err());
}
}