use crate::cli::Cli;
use crate::spec_loader::SpecLocation;
use bon::Builder;
use reqwest::header::HeaderMap;
use rmcp_openapi::{
AuthorizationMode, CliError, Error, Server,
spec::{Filter, Filters},
};
use url::Url;
#[derive(Debug, Clone, Builder)]
pub struct Configuration {
pub spec_location: SpecLocation,
pub base_url: Url,
pub port: u16,
pub bind_address: String,
pub default_headers: HeaderMap,
pub filters: Option<Filters>,
pub authorization_mode: AuthorizationMode,
#[builder(default)]
pub skip_tool_descriptions: bool,
#[builder(default)]
pub skip_parameter_descriptions: bool,
#[builder(default)]
pub stateful: bool,
#[builder(default)]
pub insecure: bool,
}
impl Configuration {
pub fn from_cli(cli: Cli) -> Result<Self, Error> {
let base_url = Url::parse(&cli.base_url)
.map_err(|e| Error::InvalidUrl(format!("Invalid base URL: {e}")))?;
let mut default_headers = HeaderMap::new();
for header_str in cli.headers {
if let Some((key, value)) = header_str.split_once(':') {
let key = key.trim();
let value = value.trim();
if key.is_empty() {
return Err(Error::Cli(CliError::InvalidHeaderFormat {
header: header_str,
}));
}
let header_name =
http::header::HeaderName::from_bytes(key.as_bytes()).map_err(|e| {
Error::Cli(CliError::InvalidHeaderName {
header: header_str.clone(),
source: e,
})
})?;
let header_value = http::header::HeaderValue::from_str(value).map_err(|e| {
Error::Cli(CliError::InvalidHeaderValue {
header: header_str.clone(),
source: e,
})
})?;
default_headers.insert(header_name, header_value);
} else {
return Err(Error::Cli(CliError::InvalidHeaderFormat {
header: header_str,
}));
}
}
let filters = {
let mut f = Filters::builder().build();
if let Some(tags) = cli.tags {
f.tags = Some(Filter::Include(tags));
}
if let Some(methods) = cli.methods {
f.methods = Some(Filter::Include(methods));
}
f.operations_id = match (cli.operationids_include, cli.operationids_exclude) {
(Some(op), None) => Some(Filter::Include(op)),
(None, Some(op)) => Some(Filter::Exclude(op)),
_ => None,
};
if f.tags.is_some() || f.methods.is_some() || f.operations_id.is_some() {
Some(f)
} else {
None
}
};
Ok(Configuration {
spec_location: cli.spec,
base_url,
port: cli.port,
bind_address: cli.bind_address,
default_headers,
filters,
authorization_mode: cli.authorization_mode,
skip_tool_descriptions: cli.skip_tool_descriptions,
skip_parameter_descriptions: cli.skip_parameter_descriptions,
stateful: cli.stateful,
insecure: cli.insecure,
})
}
}
impl Configuration {
pub async fn try_into_server(self) -> Result<Server, Error> {
let openapi_spec = self.spec_location.load_json(self.insecure).await?;
let headers = if self.default_headers.is_empty() {
None
} else {
Some(self.default_headers)
};
let mut server = Server::new(
openapi_spec,
self.base_url,
headers,
self.filters,
self.skip_tool_descriptions,
self.skip_parameter_descriptions,
self.insecure,
);
server.set_authorization_mode(self.authorization_mode);
server.name = Some(env!("CARGO_PKG_NAME").to_string());
server.version = Some(env!("CARGO_PKG_VERSION").to_string());
server.instructions = Some(env!("CARGO_PKG_DESCRIPTION").to_string());
Ok(server)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::Cli;
use crate::spec_loader::SpecLocation;
use url::Url;
#[test]
fn test_header_parsing_valid_formats() {
let cli = Cli {
spec: SpecLocation::Url(Url::parse("https://example.com/spec.json").unwrap()),
base_url: "https://api.example.com".to_string(),
port: 8080,
bind_address: "127.0.0.1".to_string(),
headers: vec![
"Authorization: Bearer token123".to_string(),
"X-API-Key: key456".to_string(),
"Content-Type: application/json".to_string(),
"User-Agent: TestAgent/1.0".to_string(),
],
tags: None,
methods: None,
operationids_include: None,
operationids_exclude: None,
authorization_mode: AuthorizationMode::default(),
skip_tool_descriptions: false,
skip_parameter_descriptions: false,
stateful: false,
insecure: false,
};
let config = Configuration::from_cli(cli).unwrap();
assert_eq!(config.default_headers.len(), 4);
assert_eq!(
config
.default_headers
.get("Authorization")
.map(|v| v.to_str().unwrap()),
Some("Bearer token123")
);
assert_eq!(
config
.default_headers
.get("X-API-Key")
.map(|v| v.to_str().unwrap()),
Some("key456")
);
assert_eq!(
config
.default_headers
.get("Content-Type")
.map(|v| v.to_str().unwrap()),
Some("application/json")
);
assert_eq!(
config
.default_headers
.get("User-Agent")
.map(|v| v.to_str().unwrap()),
Some("TestAgent/1.0")
);
}
#[test]
fn test_header_parsing_with_spaces() {
let cli = Cli {
spec: SpecLocation::Url(Url::parse("https://example.com/spec.json").unwrap()),
base_url: "https://api.example.com".to_string(),
port: 8080,
bind_address: "127.0.0.1".to_string(),
headers: vec![
" Authorization : Bearer token123 ".to_string(),
"X-Custom : value with spaces ".to_string(),
],
tags: None,
methods: None,
operationids_include: None,
operationids_exclude: None,
authorization_mode: AuthorizationMode::default(),
skip_tool_descriptions: false,
skip_parameter_descriptions: false,
stateful: false,
insecure: false,
};
let config = Configuration::from_cli(cli).unwrap();
assert_eq!(config.default_headers.len(), 2);
assert_eq!(
config
.default_headers
.get("Authorization")
.map(|v| v.to_str().unwrap()),
Some("Bearer token123")
);
assert_eq!(
config
.default_headers
.get("X-Custom")
.map(|v| v.to_str().unwrap()),
Some("value with spaces")
);
}
#[test]
fn test_header_parsing_invalid_format_no_equals() {
let cli = Cli {
spec: SpecLocation::Url(Url::parse("https://example.com/spec.json").unwrap()),
base_url: "https://api.example.com".to_string(),
port: 8080,
bind_address: "127.0.0.1".to_string(),
headers: vec!["InvalidHeaderNoEquals".to_string()],
tags: None,
methods: None,
operationids_include: None,
operationids_exclude: None,
authorization_mode: AuthorizationMode::default(),
skip_tool_descriptions: false,
skip_parameter_descriptions: false,
stateful: false,
insecure: false,
};
let result = Configuration::from_cli(cli);
assert!(result.is_err());
let error = result.unwrap_err().to_string();
assert!(error.contains("Invalid header format"));
assert!(error.contains("expected 'name: value' format"));
}
#[test]
fn test_header_parsing_invalid_format_empty_key() {
let cli = Cli {
spec: SpecLocation::Url(Url::parse("https://example.com/spec.json").unwrap()),
base_url: "https://api.example.com".to_string(),
port: 8080,
bind_address: "127.0.0.1".to_string(),
headers: vec![": value".to_string()],
tags: None,
methods: None,
operationids_include: None,
operationids_exclude: None,
authorization_mode: AuthorizationMode::default(),
skip_tool_descriptions: false,
skip_parameter_descriptions: false,
stateful: false,
insecure: false,
};
let result = Configuration::from_cli(cli);
assert!(result.is_err());
let error = result.unwrap_err().to_string();
assert!(error.contains("CLI error"));
assert!(error.contains("Invalid header format"));
}
#[test]
fn test_header_parsing_empty_value_allowed() {
let cli = Cli {
spec: SpecLocation::Url(Url::parse("https://example.com/spec.json").unwrap()),
base_url: "https://api.example.com".to_string(),
port: 8080,
bind_address: "127.0.0.1".to_string(),
headers: vec!["X-Empty-Header:".to_string()],
tags: None,
methods: None,
operationids_include: None,
operationids_exclude: None,
authorization_mode: AuthorizationMode::default(),
skip_tool_descriptions: false,
skip_parameter_descriptions: false,
stateful: false,
insecure: false,
};
let config = Configuration::from_cli(cli).unwrap();
assert_eq!(config.default_headers.len(), 1);
assert_eq!(
config
.default_headers
.get("X-Empty-Header")
.map(|v| v.to_str().unwrap()),
Some("")
);
}
#[test]
fn test_no_headers() {
let cli = Cli {
spec: SpecLocation::Url(Url::parse("https://example.com/spec.json").unwrap()),
base_url: "https://api.example.com".to_string(),
port: 8080,
bind_address: "127.0.0.1".to_string(),
headers: vec![],
tags: None,
methods: None,
operationids_include: None,
operationids_exclude: None,
authorization_mode: AuthorizationMode::default(),
skip_tool_descriptions: false,
skip_parameter_descriptions: false,
stateful: false,
insecure: false,
};
let config = Configuration::from_cli(cli).unwrap();
assert!(config.default_headers.is_empty());
}
#[test]
fn test_header_validation_invalid_header_name() {
let cli = Cli {
spec: SpecLocation::Url(Url::parse("https://example.com/spec.json").unwrap()),
base_url: "https://api.example.com".to_string(),
port: 8080,
bind_address: "127.0.0.1".to_string(),
headers: vec!["Invalid Header Name: value".to_string()],
tags: None,
methods: None,
operationids_include: None,
operationids_exclude: None,
authorization_mode: AuthorizationMode::default(),
skip_tool_descriptions: false,
skip_parameter_descriptions: false,
stateful: false,
insecure: false,
};
let result = Configuration::from_cli(cli);
assert!(result.is_err());
let error = result.unwrap_err().to_string();
assert!(error.contains("CLI error"));
assert!(error.contains("Invalid header name"));
}
#[test]
fn test_header_validation_invalid_header_value() {
let cli = Cli {
spec: SpecLocation::Url(Url::parse("https://example.com/spec.json").unwrap()),
base_url: "https://api.example.com".to_string(),
port: 8080,
bind_address: "127.0.0.1".to_string(),
headers: vec!["Valid-Header: invalid\x00value".to_string()],
tags: None,
methods: None,
operationids_include: None,
operationids_exclude: None,
authorization_mode: AuthorizationMode::default(),
skip_tool_descriptions: false,
skip_parameter_descriptions: false,
stateful: false,
insecure: false,
};
let result = Configuration::from_cli(cli);
assert!(result.is_err());
let error = result.unwrap_err().to_string();
assert!(error.contains("CLI error"));
assert!(error.contains("Invalid header value"));
}
fn minimal_cli() -> Cli {
Cli {
spec: SpecLocation::Url(Url::parse("https://example.com/spec.json").unwrap()),
base_url: "https://api.example.com".to_string(),
port: 8080,
bind_address: "127.0.0.1".to_string(),
headers: vec![],
tags: None,
methods: None,
operationids_include: None,
operationids_exclude: None,
authorization_mode: AuthorizationMode::default(),
skip_tool_descriptions: false,
skip_parameter_descriptions: false,
stateful: false,
insecure: false,
}
}
#[test]
fn insecure_flag_mapped_when_true() {
let mut cli = minimal_cli();
cli.insecure = true;
let config = Configuration::from_cli(cli).unwrap();
assert!(config.insecure);
}
#[test]
fn insecure_flag_defaults_to_false() {
let cli = minimal_cli();
let config = Configuration::from_cli(cli).unwrap();
assert!(!config.insecure);
}
}