1use crate::config_reload::ConfigReloader;
4use crate::server::transport::{self, HyperServerConfig};
5use crate::CratesDocsServer;
6use std::path::PathBuf;
7use std::sync::Arc;
8use tokio::time::{interval, Duration};
9
10#[cfg(feature = "api-key")]
11fn normalize_api_keys(
12 api_key_config: &crate::server::auth::ApiKeyConfig,
13 keys: Vec<String>,
14) -> Result<Vec<String>, Box<dyn std::error::Error>> {
15 keys.into_iter()
16 .map(|key| {
17 api_key_config
18 .normalize_key_material(&key)
19 .map_err(|e| format!("Failed to normalize API key material: {e}").into())
20 })
21 .collect()
22}
23
24fn load_from_env(config: &mut crate::config::AppConfig) -> Result<(), Box<dyn std::error::Error>> {
25 let env_config = match crate::config::AppConfig::from_env() {
26 Ok(config) => Some(config),
27 Err(e) if e.to_string().contains("Invalid port") => return Err(e.to_string().into()),
28 Err(_) => None,
29 };
30
31 *config = crate::config::AppConfig::merge(Some(std::mem::take(config)), env_config);
33
34 #[cfg(feature = "api-key")]
35 if !config.auth.api_key.keys.is_empty() {
36 let keys = std::mem::take(&mut config.auth.api_key.keys);
37 config.auth.api_key.keys = normalize_api_keys(&config.auth.api_key, keys)?;
38 }
39
40 Ok(())
41}
42
43fn init_logging(
44 config: &crate::config::AppConfig,
45 debug: bool,
46) -> Result<(), Box<dyn std::error::Error>> {
47 if debug {
48 let debug_config = crate::config::LoggingConfig {
49 level: "debug".to_string(),
50 ..config.logging.clone()
51 };
52 crate::init_logging_with_config(&debug_config)
53 .map_err(|e| format!("Failed to initialize logging system: {e}"))?;
54 } else {
55 crate::init_logging_with_config(&config.logging)
56 .map_err(|e| format!("Failed to initialize logging system: {e}"))?;
57 }
58 Ok(())
59}
60
61fn start_config_reloader(config_path: &std::path::Path, server: &CratesDocsServer) {
62 let config_path_arc = Arc::from(config_path.to_path_buf().into_boxed_path());
63 let current_config = server.config().clone();
64
65 match ConfigReloader::new(config_path_arc, current_config) {
66 Ok(mut reloader) => {
67 tracing::info!(
68 "Configuration hot-reload enabled for {}",
69 config_path.display()
70 );
71
72 tokio::spawn(async move {
73 let mut check_interval = interval(Duration::from_secs(1));
74
75 loop {
76 check_interval.tick().await;
77
78 if let Some(change) = reloader.check_for_changes() {
79 if let Some(changes) = change.changes() {
80 tracing::info!("Configuration file changed:");
81 for change_desc in changes {
82 tracing::info!(" - {}", change_desc);
83 }
84 tracing::warn!("Configuration has been reloaded. Some changes may require server restart.");
85 tracing::warn!("API key changes: New keys are now active. Removed keys are revoked immediately.");
86 }
87 }
88 }
89 });
90 }
91 Err(e) => {
92 tracing::warn!("Failed to enable configuration hot-reload: {}", e);
93 }
94 }
95}
96
97async fn run_server_by_mode(
98 server: &CratesDocsServer,
99 transport_mode: &str,
100) -> Result<(), Box<dyn std::error::Error>> {
101 let mode_str = transport_mode.to_lowercase();
102 match mode_str.as_str() {
103 "stdio" => {
104 tracing::info!("Using Stdio transport mode");
105 transport::run_stdio_server(server)
106 .await
107 .map_err(|e| format!("Failed to start Stdio server: {e}"))?;
108 }
109 "http" => {
110 tracing::info!(
111 "Using HTTP transport mode, listening on {}:{}",
112 server.config().server.host,
113 server.config().server.port
114 );
115 transport::run_hyper_server(server, HyperServerConfig::http())
116 .await
117 .map_err(|e| format!("Failed to start HTTP server: {e}"))?;
118 }
119 "sse" => {
120 tracing::info!(
121 "Using SSE transport mode, listening on {}:{}",
122 server.config().server.host,
123 server.config().server.port
124 );
125 transport::run_hyper_server(server, HyperServerConfig::sse())
126 .await
127 .map_err(|e| format!("Failed to start SSE server: {e}"))?;
128 }
129 "hybrid" => {
130 tracing::info!(
131 "Using hybrid transport mode (HTTP + SSE), listening on {}:{}",
132 server.config().server.host,
133 server.config().server.port
134 );
135 transport::run_hyper_server(server, HyperServerConfig::hybrid())
136 .await
137 .map_err(|e| format!("Failed to start hybrid server: {e}"))?;
138 }
139 _ => {
140 return Err(format!("Unknown transport mode: {transport_mode}").into());
141 }
142 }
143 Ok(())
144}
145
146#[allow(clippy::too_many_arguments)]
148pub async fn run_serve_command(
149 config_path: &PathBuf,
150 debug: bool,
151 mode: Option<String>,
152 host: Option<String>,
153 port: Option<u16>,
154 enable_oauth: Option<bool>,
155 oauth_client_id: Option<String>,
156 oauth_client_secret: Option<String>,
157 oauth_redirect_uri: Option<String>,
158 enable_api_key: Option<bool>,
159 api_keys: Option<String>,
160 api_key_header: Option<String>,
161 api_key_query_param: Option<bool>,
162) -> Result<(), Box<dyn std::error::Error>> {
163 let config = load_config(
164 config_path,
165 host,
166 port,
167 mode,
168 enable_oauth,
169 oauth_client_id,
170 oauth_client_secret,
171 oauth_redirect_uri,
172 enable_api_key,
173 api_keys,
174 api_key_header,
175 api_key_query_param,
176 )?;
177
178 let transport_mode = &config.server.transport_mode;
179
180 init_logging(&config, debug)?;
181
182 tracing::info!(
183 "Starting Crates Docs MCP Server v{}",
184 env!("CARGO_PKG_VERSION")
185 );
186
187 let server: CratesDocsServer = CratesDocsServer::new_async(config.clone())
188 .await
189 .map_err(|e| format!("Failed to create server: {e}"))?;
190
191 let mode_str = transport_mode.to_lowercase();
192 let should_enable_reload = matches!(mode_str.as_str(), "http" | "sse" | "hybrid");
193
194 if should_enable_reload && config_path.exists() {
195 start_config_reloader(config_path, &server);
196 }
197
198 run_server_by_mode(&server, transport_mode).await
199}
200
201#[allow(clippy::too_many_arguments)]
203fn load_config(
204 config_path: &PathBuf,
205 host: Option<String>,
206 port: Option<u16>,
207 mode: Option<String>,
208 enable_oauth: Option<bool>,
209 oauth_client_id: Option<String>,
210 oauth_client_secret: Option<String>,
211 oauth_redirect_uri: Option<String>,
212 enable_api_key: Option<bool>,
213 api_keys: Option<String>,
214 api_key_header: Option<String>,
215 api_key_query_param: Option<bool>,
216) -> Result<crate::config::AppConfig, Box<dyn std::error::Error>> {
217 let mut config = if config_path.exists() {
218 tracing::info!("Loading configuration from file: {}", config_path.display());
219 crate::config::AppConfig::from_file(config_path)
220 .map_err(|e| format!("Failed to load config file: {e}"))?
221 } else {
222 tracing::warn!(
223 "Config file does not exist, using default config: {}",
224 config_path.display()
225 );
226 crate::config::AppConfig::default()
227 };
228
229 load_from_env(&mut config)?;
230
231 if let Some(h) = host {
233 config.server.host = h;
234 tracing::info!(
235 "Command line argument overrides host: {}",
236 config.server.host
237 );
238 }
239 if let Some(p) = port {
240 config.server.port = p;
241 tracing::info!(
242 "Command line argument overrides port: {}",
243 config.server.port
244 );
245 }
246 if let Some(m) = mode {
247 config.server.transport_mode = m;
248 tracing::info!(
249 "Command line argument overrides transport_mode: {}",
250 config.server.transport_mode
251 );
252 }
253 if let Some(eo) = enable_oauth {
254 config.server.enable_oauth = eo;
255 tracing::info!(
256 "Command line argument overrides enable_oauth: {}",
257 config.server.enable_oauth
258 );
259 }
260
261 if let Some(client_id) = oauth_client_id {
263 config.oauth.client_id = Some(client_id);
264 config.oauth.enabled = true;
265 }
266 if let Some(client_secret) = oauth_client_secret {
267 config.oauth.client_secret = Some(client_secret);
268 }
269 if let Some(redirect_uri) = oauth_redirect_uri {
270 config.oauth.redirect_uri = Some(redirect_uri);
271 }
272
273 if let Some(eak) = enable_api_key {
275 config.auth.api_key.enabled = eak;
276 tracing::info!(
277 "Command line argument overrides enable_api_key: {}",
278 config.auth.api_key.enabled
279 );
280 }
281 if let Some(keys) = api_keys {
282 let parsed_keys: Vec<String> = keys
283 .split(',')
284 .map(str::trim)
285 .filter(|s| !s.is_empty())
286 .map(ToOwned::to_owned)
287 .collect();
288
289 if !parsed_keys.is_empty() {
290 #[cfg(feature = "api-key")]
291 {
292 config.auth.api_key.keys = normalize_api_keys(&config.auth.api_key, parsed_keys)?;
293 }
294 #[cfg(not(feature = "api-key"))]
295 {
296 config.auth.api_key.keys = parsed_keys;
297 }
298 config.auth.api_key.enabled = true;
299 tracing::info!("Command line argument provided API key material");
300 }
301 }
302 if let Some(header) = api_key_header {
303 config.auth.api_key.header_name = header;
304 tracing::info!(
305 "Command line argument overrides api_key_header: {}",
306 config.auth.api_key.header_name
307 );
308 }
309 if let Some(allow_query) = api_key_query_param {
310 config.auth.api_key.allow_query_param = allow_query;
311 tracing::info!(
312 "Command line argument overrides api_key_query_param: {}",
313 config.auth.api_key.allow_query_param
314 );
315 }
316
317 config
319 .validate()
320 .map_err(|e| format!("Configuration validation failed: {e}"))?;
321
322 Ok(config)
323}