use crate::cache::CacheConfig;
use crate::server::auth::{AuthConfig, OAuthConfig};
use rust_mcp_sdk::schema::{Icon, IconTheme};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
const DEFAULT_HTTP_CLIENT_POOL_SIZE: usize = 10;
const DEFAULT_HTTP_CLIENT_POOL_IDLE_TIMEOUT_SECS: u64 = 90;
const DEFAULT_HTTP_CLIENT_CONNECT_TIMEOUT_SECS: u64 = 10;
const DEFAULT_HTTP_CLIENT_TIMEOUT_SECS: u64 = 30;
const DEFAULT_HTTP_CLIENT_READ_TIMEOUT_SECS: u64 = 30;
const DEFAULT_HTTP_CLIENT_MAX_RETRIES: u32 = 3;
const DEFAULT_HTTP_CLIENT_RETRY_INITIAL_DELAY_MS: u64 = 100;
const DEFAULT_HTTP_CLIENT_RETRY_MAX_DELAY_MS: u64 = 10_000;
const DEFAULT_SERVER_PORT: u16 = 8080;
const DEFAULT_SERVER_MAX_CONNECTIONS: usize = 100;
const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 30;
const DEFAULT_RESPONSE_TIMEOUT_SECS: u64 = 60;
const DEFAULT_CACHE_MAX_SIZE: usize = 1000;
const DEFAULT_CACHE_DEFAULT_TTL_SECS: u64 = 3600;
const DEFAULT_RATE_LIMIT_PER_SECOND: u32 = 100;
const DEFAULT_CONCURRENT_REQUEST_LIMIT: usize = 50;
const DEFAULT_MAX_FILE_SIZE_MB: u64 = 100;
const DEFAULT_MAX_FILES: usize = 10;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct AppConfig {
pub server: ServerConfig,
pub cache: CacheConfig,
#[serde(default)]
pub auth: AuthConfig,
#[serde(default)]
pub oauth: OAuthConfig,
pub logging: LoggingConfig,
pub performance: PerformanceConfig,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerConfig {
pub name: String,
#[serde(default = "default_version")]
pub version: String,
pub description: Option<String>,
#[serde(default = "default_icons")]
pub icons: Vec<Icon>,
pub website_url: Option<String>,
pub host: String,
pub port: u16,
pub transport_mode: String,
pub enable_sse: bool,
pub enable_oauth: bool,
pub max_connections: usize,
pub request_timeout_secs: u64,
pub response_timeout_secs: u64,
pub allowed_hosts: Vec<String>,
pub allowed_origins: Vec<String>,
}
fn default_version() -> String {
crate::VERSION.to_string()
}
fn default_icons() -> Vec<Icon> {
vec![
Icon {
src: "https://docs.rs/static/favicon-32x32.png".to_string(),
mime_type: Some("image/png".to_string()),
sizes: vec!["32x32".to_string()],
theme: Some(IconTheme::Light),
},
Icon {
src: "https://docs.rs/static/favicon-32x32.png".to_string(),
mime_type: Some("image/png".to_string()),
sizes: vec!["32x32".to_string()],
theme: Some(IconTheme::Dark),
},
]
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LoggingConfig {
pub level: String,
pub file_path: Option<String>,
pub enable_console: bool,
pub enable_file: bool,
pub max_file_size_mb: u64,
pub max_files: usize,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PerformanceConfig {
pub http_client_pool_size: usize,
pub http_client_pool_idle_timeout_secs: u64,
pub http_client_connect_timeout_secs: u64,
pub http_client_timeout_secs: u64,
pub http_client_read_timeout_secs: u64,
pub http_client_max_retries: u32,
pub http_client_retry_initial_delay_ms: u64,
pub http_client_retry_max_delay_ms: u64,
pub cache_max_size: usize,
pub cache_default_ttl_secs: u64,
pub rate_limit_per_second: u32,
pub concurrent_request_limit: usize,
pub enable_response_compression: bool,
pub enable_metrics: bool,
pub metrics_port: u16,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
name: "crates-docs".to_string(),
version: crate::VERSION.to_string(),
description: Some(
"High-performance Rust crate documentation query MCP server".to_string(),
),
icons: default_icons(),
website_url: Some("https://github.com/KingingWang/crates-docs".to_string()),
host: "127.0.0.1".to_string(),
port: DEFAULT_SERVER_PORT,
transport_mode: "hybrid".to_string(),
enable_sse: true,
enable_oauth: false,
max_connections: DEFAULT_SERVER_MAX_CONNECTIONS,
request_timeout_secs: DEFAULT_REQUEST_TIMEOUT_SECS,
response_timeout_secs: DEFAULT_RESPONSE_TIMEOUT_SECS,
allowed_hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
allowed_origins: vec!["http://localhost:*".to_string()],
}
}
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".to_string(),
file_path: Some("./logs/crates-docs.log".to_string()),
enable_console: true,
enable_file: false, max_file_size_mb: DEFAULT_MAX_FILE_SIZE_MB,
max_files: DEFAULT_MAX_FILES,
}
}
}
impl Default for PerformanceConfig {
fn default() -> Self {
Self {
http_client_pool_size: DEFAULT_HTTP_CLIENT_POOL_SIZE,
http_client_pool_idle_timeout_secs: DEFAULT_HTTP_CLIENT_POOL_IDLE_TIMEOUT_SECS,
http_client_connect_timeout_secs: DEFAULT_HTTP_CLIENT_CONNECT_TIMEOUT_SECS,
http_client_timeout_secs: DEFAULT_HTTP_CLIENT_TIMEOUT_SECS,
http_client_read_timeout_secs: DEFAULT_HTTP_CLIENT_READ_TIMEOUT_SECS,
http_client_max_retries: DEFAULT_HTTP_CLIENT_MAX_RETRIES,
http_client_retry_initial_delay_ms: DEFAULT_HTTP_CLIENT_RETRY_INITIAL_DELAY_MS,
http_client_retry_max_delay_ms: DEFAULT_HTTP_CLIENT_RETRY_MAX_DELAY_MS,
cache_max_size: DEFAULT_CACHE_MAX_SIZE,
cache_default_ttl_secs: DEFAULT_CACHE_DEFAULT_TTL_SECS,
rate_limit_per_second: DEFAULT_RATE_LIMIT_PER_SECOND,
concurrent_request_limit: DEFAULT_CONCURRENT_REQUEST_LIMIT,
enable_response_compression: true,
enable_metrics: true,
metrics_port: 0,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct EnvServerConfig {
pub name: Option<String>,
pub host: Option<String>,
pub port: Option<u16>,
pub transport_mode: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct EnvLoggingConfig {
pub level: Option<String>,
pub enable_console: Option<bool>,
pub enable_file: Option<bool>,
}
#[cfg(feature = "api-key")]
#[derive(Debug, Clone, Default)]
pub struct EnvApiKeyConfig {
pub enabled: Option<bool>,
pub keys: Option<Vec<String>>,
pub header_name: Option<String>,
pub query_param_name: Option<String>,
pub allow_query_param: Option<bool>,
pub key_prefix: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct EnvAppConfig {
pub server: EnvServerConfig,
pub logging: EnvLoggingConfig,
#[cfg(feature = "api-key")]
pub auth_api_key: EnvApiKeyConfig,
}
impl AppConfig {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, crate::error::Error> {
let content = fs::read_to_string(path).map_err(|e| {
crate::error::Error::config("file", format!("Failed to read config file: {e}"))
})?;
let config: Self = toml::from_str(&content).map_err(|e| {
crate::error::Error::parse("config", None, format!("Failed to parse config file: {e}"))
})?;
config.validate()?;
Ok(config)
}
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), crate::error::Error> {
let content = toml::to_string_pretty(self).map_err(|e| {
crate::error::Error::config(
"serialization",
format!("Failed to serialize configuration: {e}"),
)
})?;
if let Some(parent) = path.as_ref().parent() {
fs::create_dir_all(parent).map_err(|e| {
crate::error::Error::config("directory", format!("Failed to create directory: {e}"))
})?;
}
fs::write(path, content).map_err(|e| {
crate::error::Error::config("file", format!("Failed to write config file: {e}"))
})?;
Ok(())
}
pub fn validate(&self) -> Result<(), crate::error::Error> {
if self.server.host.is_empty() {
return Err(crate::error::Error::config("host", "cannot be empty"));
}
if self.server.port == 0 {
return Err(crate::error::Error::config("port", "cannot be 0"));
}
if self.server.max_connections == 0 {
return Err(crate::error::Error::config(
"max_connections",
"cannot be 0",
));
}
let valid_modes = ["stdio", "http", "sse", "hybrid"];
if !valid_modes.contains(&self.server.transport_mode.as_str()) {
return Err(crate::error::Error::config(
"transport_mode",
format!(
"Invalid transport mode: {}, valid values: {:?}",
self.server.transport_mode, valid_modes
),
));
}
let valid_levels = ["trace", "debug", "info", "warn", "error"];
if !valid_levels.contains(&self.logging.level.as_str()) {
return Err(crate::error::Error::config(
"log_level",
format!(
"Invalid log level: {}, valid values: {:?}",
self.logging.level, valid_levels
),
));
}
if self.performance.http_client_pool_size == 0 {
return Err(crate::error::Error::config(
"http_client_pool_size",
"cannot be 0",
));
}
if self.performance.http_client_pool_idle_timeout_secs == 0 {
return Err(crate::error::Error::config(
"http_client_pool_idle_timeout_secs",
"cannot be 0",
));
}
if self.performance.http_client_connect_timeout_secs == 0 {
return Err(crate::error::Error::config(
"http_client_connect_timeout_secs",
"cannot be 0",
));
}
if self.performance.http_client_timeout_secs == 0 {
return Err(crate::error::Error::config(
"http_client_timeout_secs",
"cannot be 0",
));
}
if self.performance.cache_max_size == 0 {
return Err(crate::error::Error::config("cache_max_size", "cannot be 0"));
}
if self.server.enable_oauth {
self.oauth.validate()?;
}
Ok(())
}
pub fn from_env() -> Result<EnvAppConfig, crate::error::Error> {
let mut config = EnvAppConfig::default();
if let Ok(name) = std::env::var("CRATES_DOCS_NAME") {
config.server.name = Some(name);
}
if let Ok(host) = std::env::var("CRATES_DOCS_HOST") {
config.server.host = Some(host);
}
if let Ok(port) = std::env::var("CRATES_DOCS_PORT") {
config.server.port =
Some(port.parse().map_err(|e| {
crate::error::Error::config("port", format!("Invalid port: {e}"))
})?);
}
if let Ok(mode) = std::env::var("CRATES_DOCS_TRANSPORT_MODE") {
config.server.transport_mode = Some(mode);
}
if let Ok(level) = std::env::var("CRATES_DOCS_LOG_LEVEL") {
config.logging.level = Some(level);
}
if let Ok(enable_console) = std::env::var("CRATES_DOCS_ENABLE_CONSOLE") {
config.logging.enable_console = enable_console.parse().ok();
}
if let Ok(enable_file) = std::env::var("CRATES_DOCS_ENABLE_FILE") {
config.logging.enable_file = enable_file.parse().ok();
}
#[cfg(feature = "api-key")]
{
if let Ok(enabled) = std::env::var("CRATES_DOCS_API_KEY_ENABLED") {
config.auth_api_key.enabled = enabled.parse().ok();
}
if let Ok(keys) = std::env::var("CRATES_DOCS_API_KEYS") {
config.auth_api_key.keys = Some(
keys.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(ToOwned::to_owned)
.collect(),
);
}
if let Ok(header_name) = std::env::var("CRATES_DOCS_API_KEY_HEADER") {
config.auth_api_key.header_name = Some(header_name);
}
if let Ok(query_param_name) = std::env::var("CRATES_DOCS_API_KEY_QUERY_PARAM_NAME") {
config.auth_api_key.query_param_name = Some(query_param_name);
}
if let Ok(allow_query_param) = std::env::var("CRATES_DOCS_API_KEY_ALLOW_QUERY") {
config.auth_api_key.allow_query_param = allow_query_param.parse().ok();
}
if let Ok(key_prefix) = std::env::var("CRATES_DOCS_API_KEY_PREFIX") {
config.auth_api_key.key_prefix = Some(key_prefix);
}
}
Ok(config)
}
#[must_use]
pub fn merge(file_config: Option<Self>, env_config: Option<EnvAppConfig>) -> Self {
let mut config = Self::default();
if let Some(file) = file_config {
config = file;
}
if let Some(env) = env_config {
if let Some(name) = env.server.name {
config.server.name = name;
}
if let Some(host) = env.server.host {
config.server.host = host;
}
if let Some(port) = env.server.port {
config.server.port = port;
}
if let Some(transport_mode) = env.server.transport_mode {
config.server.transport_mode = transport_mode;
}
if let Some(level) = env.logging.level {
config.logging.level = level;
}
if let Some(enable_console) = env.logging.enable_console {
config.logging.enable_console = enable_console;
}
if let Some(enable_file) = env.logging.enable_file {
config.logging.enable_file = enable_file;
}
#[cfg(feature = "api-key")]
{
if let Some(enabled) = env.auth_api_key.enabled {
config.auth.api_key.enabled = enabled;
}
if let Some(keys) = env.auth_api_key.keys {
config.auth.api_key.keys = keys;
}
if let Some(header_name) = env.auth_api_key.header_name {
config.auth.api_key.header_name = header_name;
}
if let Some(query_param_name) = env.auth_api_key.query_param_name {
config.auth.api_key.query_param_name = query_param_name;
}
if let Some(allow_query_param) = env.auth_api_key.allow_query_param {
config.auth.api_key.allow_query_param = allow_query_param;
}
if let Some(key_prefix) = env.auth_api_key.key_prefix {
config.auth.api_key.key_prefix = key_prefix;
}
}
}
config
}
}