use crate::error::{ConfigError, McpResult};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use url::Url;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TransportConfig {
Stdio(StdioConfig),
HttpSse(HttpSseConfig),
HttpStream(HttpStreamConfig),
}
impl TransportConfig {
pub fn stdio(command: impl Into<String>, args: &[impl ToString]) -> Self {
Self::Stdio(StdioConfig {
command: command.into(),
args: args.iter().map(|s| s.to_string()).collect(),
working_dir: None,
timeout: Duration::from_secs(30),
environment: HashMap::new(),
})
}
pub fn http_sse(base_url: impl AsRef<str>) -> McpResult<Self> {
let url = base_url
.as_ref()
.parse()
.map_err(|e| ConfigError::InvalidValue {
parameter: "base_url".to_string(),
value: base_url.as_ref().to_string(),
reason: format!("Invalid URL: {}", e),
})?;
Ok(Self::HttpSse(HttpSseConfig {
base_url: url,
timeout: Duration::from_secs(60),
headers: HashMap::new(),
auth: None,
}))
}
pub fn http_stream(base_url: impl AsRef<str>) -> McpResult<Self> {
let url = base_url
.as_ref()
.parse()
.map_err(|e| ConfigError::InvalidValue {
parameter: "base_url".to_string(),
value: base_url.as_ref().to_string(),
reason: format!("Invalid URL: {}", e),
})?;
Ok(Self::HttpStream(HttpStreamConfig {
base_url: url,
timeout: Duration::from_secs(300),
headers: HashMap::new(),
auth: None,
compression: true,
flow_control_window: 65536,
}))
}
pub fn transport_type(&self) -> &'static str {
match self {
Self::Stdio(_) => "stdio",
Self::HttpSse(_) => "http-sse",
Self::HttpStream(_) => "http-stream",
}
}
pub fn validate(&self) -> McpResult<()> {
match self {
Self::Stdio(config) => config.validate(),
Self::HttpSse(config) => config.validate(),
Self::HttpStream(config) => config.validate(),
}
}
pub fn from_file(path: impl AsRef<std::path::Path>) -> McpResult<Self> {
let path = path.as_ref();
let content = std::fs::read_to_string(path).map_err(|_e| ConfigError::FileNotFound {
path: path.display().to_string(),
})?;
let config: Self = match path.extension().and_then(|ext| ext.to_str()) {
Some("json") => {
serde_json::from_str(&content).map_err(|e| ConfigError::InvalidFormat {
path: path.display().to_string(),
reason: e.to_string(),
})?
}
Some("yaml") | Some("yml") => {
serde_yaml::from_str(&content).map_err(|e| ConfigError::InvalidFormat {
path: path.display().to_string(),
reason: e.to_string(),
})?
}
Some("toml") => toml::from_str(&content).map_err(|e| ConfigError::InvalidFormat {
path: path.display().to_string(),
reason: e.to_string(),
})?,
_ => {
return Err(ConfigError::InvalidFormat {
path: path.display().to_string(),
reason: "Unsupported file format. Use .json, .yaml, or .toml".to_string(),
}
.into())
}
};
config.validate()?;
Ok(config)
}
pub fn to_file(&self, path: impl AsRef<std::path::Path>) -> McpResult<()> {
let path = path.as_ref();
let content = match path.extension().and_then(|ext| ext.to_str()) {
Some("json") => {
serde_json::to_string_pretty(self).map_err(|e| ConfigError::InvalidFormat {
path: path.display().to_string(),
reason: e.to_string(),
})?
}
Some("yaml") | Some("yml") => {
serde_yaml::to_string(self).map_err(|e| ConfigError::InvalidFormat {
path: path.display().to_string(),
reason: e.to_string(),
})?
}
Some("toml") => toml::to_string(self).map_err(|e| ConfigError::InvalidFormat {
path: path.display().to_string(),
reason: e.to_string(),
})?,
_ => {
return Err(ConfigError::InvalidFormat {
path: path.display().to_string(),
reason: "Unsupported file format. Use .json, .yaml, or .toml".to_string(),
}
.into())
}
};
std::fs::write(path, content)?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StdioConfig {
pub command: String,
pub args: Vec<String>,
pub working_dir: Option<String>,
#[serde(with = "humantime_serde")]
pub timeout: Duration,
pub environment: HashMap<String, String>,
}
impl StdioConfig {
pub fn new(command: impl Into<String>) -> Self {
Self {
command: command.into(),
args: Vec::new(),
working_dir: None,
timeout: Duration::from_secs(30),
environment: HashMap::new(),
}
}
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
pub fn args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.args.extend(args.into_iter().map(|s| s.into()));
self
}
pub fn working_dir(mut self, dir: impl Into<String>) -> Self {
self.working_dir = Some(dir.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.environment.insert(key.into(), value.into());
self
}
pub fn validate(&self) -> McpResult<()> {
if self.command.is_empty() {
return Err(ConfigError::MissingParameter {
parameter: "command".to_string(),
}
.into());
}
if let Some(ref dir) = self.working_dir {
if !PathBuf::from(dir).exists() {
return Err(ConfigError::InvalidValue {
parameter: "working_dir".to_string(),
value: dir.clone(),
reason: "Directory does not exist".to_string(),
}
.into());
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HttpSseConfig {
pub base_url: Url,
#[serde(with = "humantime_serde")]
pub timeout: Duration,
pub headers: HashMap<String, String>,
pub auth: Option<AuthConfig>,
}
impl HttpSseConfig {
pub fn new(base_url: Url) -> Self {
Self {
base_url,
timeout: Duration::from_secs(60),
headers: HashMap::new(),
auth: None,
}
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
pub fn auth(mut self, auth: AuthConfig) -> Self {
self.auth = Some(auth);
self
}
pub fn validate(&self) -> McpResult<()> {
if self.base_url.scheme() != "http" && self.base_url.scheme() != "https" {
return Err(ConfigError::InvalidValue {
parameter: "base_url".to_string(),
value: self.base_url.to_string(),
reason: "URL must use http or https scheme".to_string(),
}
.into());
}
if let Some(ref auth) = self.auth {
auth.validate()?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HttpStreamConfig {
pub base_url: Url,
#[serde(with = "humantime_serde")]
pub timeout: Duration,
pub headers: HashMap<String, String>,
pub auth: Option<AuthConfig>,
pub compression: bool,
pub flow_control_window: u32,
}
impl HttpStreamConfig {
pub fn new(base_url: Url) -> Self {
Self {
base_url,
timeout: Duration::from_secs(300),
headers: HashMap::new(),
auth: None,
compression: true,
flow_control_window: 65536,
}
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
pub fn auth(mut self, auth: AuthConfig) -> Self {
self.auth = Some(auth);
self
}
pub fn compression(mut self, enabled: bool) -> Self {
self.compression = enabled;
self
}
pub fn flow_control_window(mut self, size: u32) -> Self {
self.flow_control_window = size;
self
}
pub fn validate(&self) -> McpResult<()> {
if self.base_url.scheme() != "http" && self.base_url.scheme() != "https" {
return Err(ConfigError::InvalidValue {
parameter: "base_url".to_string(),
value: self.base_url.to_string(),
reason: "URL must use http or https scheme".to_string(),
}
.into());
}
if self.flow_control_window == 0 {
return Err(ConfigError::InvalidValue {
parameter: "flow_control_window".to_string(),
value: self.flow_control_window.to_string(),
reason: "Flow control window must be greater than 0".to_string(),
}
.into());
}
if let Some(ref auth) = self.auth {
auth.validate()?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[allow(missing_docs)]
pub enum AuthConfig {
Basic { username: String, password: String },
Bearer { token: String },
OAuth {
client_id: String,
client_secret: String,
token_url: Url,
scope: Option<String>,
},
Header { name: String, value: String },
}
impl AuthConfig {
pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
Self::Basic {
username: username.into(),
password: password.into(),
}
}
pub fn bearer(token: impl Into<String>) -> Self {
Self::Bearer {
token: token.into(),
}
}
pub fn oauth(
client_id: impl Into<String>,
client_secret: impl Into<String>,
token_url: Url,
scope: Option<String>,
) -> Self {
Self::OAuth {
client_id: client_id.into(),
client_secret: client_secret.into(),
token_url,
scope,
}
}
pub fn header(name: impl Into<String>, value: impl Into<String>) -> Self {
Self::Header {
name: name.into(),
value: value.into(),
}
}
pub fn validate(&self) -> McpResult<()> {
match self {
Self::Basic { username, password } => {
if username.is_empty() || password.is_empty() {
return Err(ConfigError::InvalidValue {
parameter: "auth".to_string(),
value: "basic".to_string(),
reason: "Username and password cannot be empty".to_string(),
}
.into());
}
}
Self::Bearer { token } => {
if token.is_empty() {
return Err(ConfigError::InvalidValue {
parameter: "auth".to_string(),
value: "bearer".to_string(),
reason: "Token cannot be empty".to_string(),
}
.into());
}
}
Self::OAuth {
client_id,
client_secret,
token_url,
..
} => {
if client_id.is_empty() || client_secret.is_empty() {
return Err(ConfigError::InvalidValue {
parameter: "auth".to_string(),
value: "oauth".to_string(),
reason: "Client ID and secret cannot be empty".to_string(),
}
.into());
}
if token_url.scheme() != "https" {
return Err(ConfigError::InvalidValue {
parameter: "token_url".to_string(),
value: token_url.to_string(),
reason: "OAuth token URL must use HTTPS".to_string(),
}
.into());
}
}
Self::Header { name, value } => {
if name.is_empty() || value.is_empty() {
return Err(ConfigError::InvalidValue {
parameter: "auth".to_string(),
value: "header".to_string(),
reason: "Header name and value cannot be empty".to_string(),
}
.into());
}
}
}
Ok(())
}
}