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 verbose: bool,
47) -> Result<(), Box<dyn std::error::Error>> {
48 if debug || verbose {
52 let debug_config = crate::config::LoggingConfig {
53 level: "debug".to_string(),
54 ..config.logging.clone()
55 };
56 crate::init_logging_with_config(&debug_config)
57 .map_err(|e| format!("Failed to initialize logging system: {e}"))?;
58 } else {
59 crate::init_logging_with_config(&config.logging)
60 .map_err(|e| format!("Failed to initialize logging system: {e}"))?;
61 }
62 Ok(())
63}
64
65fn start_config_reloader(config_path: &std::path::Path, server: &CratesDocsServer) {
66 let config_path_arc = Arc::from(config_path.to_path_buf().into_boxed_path());
67 let current_config = server.config().clone();
68
69 match ConfigReloader::new(config_path_arc, current_config) {
70 Ok(mut reloader) => {
71 tracing::info!(
72 "Configuration hot-reload enabled for {}",
73 config_path.display()
74 );
75
76 tokio::spawn(async move {
77 let mut check_interval = interval(Duration::from_secs(1));
78
79 loop {
80 check_interval.tick().await;
81
82 if !reloader.is_watcher_alive() {
86 tracing::warn!(
87 "Stopping configuration hot-reload task: file watcher disconnected."
88 );
89 break;
90 }
91
92 if let Some(change) = reloader.check_for_changes() {
93 if let Some(changes) = change.changes() {
94 tracing::info!("Configuration file changed:");
95 for change_desc in changes {
96 tracing::info!(" - {}", change_desc);
97 }
98 tracing::warn!(
99 "Detected configuration changes are NOT applied to the already-running server. \
100 Restart the server for these changes to take effect."
101 );
102 tracing::warn!(
103 "Security note: API key and OAuth changes (including key removals) do NOT take \
104 effect until the server is restarted."
105 );
106 }
107 }
108 }
109 });
110 }
111 Err(e) => {
112 tracing::warn!("Failed to enable configuration hot-reload: {}", e);
113 }
114 }
115}
116
117async fn run_server_by_mode(
118 server: &CratesDocsServer,
119 transport_mode: &str,
120) -> Result<(), Box<dyn std::error::Error>> {
121 let mode_str = transport_mode.to_lowercase();
122 match mode_str.as_str() {
123 "stdio" => {
124 tracing::info!("Using Stdio transport mode");
125 transport::run_stdio_server(server)
126 .await
127 .map_err(|e| format!("Failed to start Stdio server: {e}"))?;
128 }
129 "http" => {
130 tracing::info!(
131 "Using HTTP transport mode, listening on {}:{}",
132 server.config().server.host,
133 server.config().server.port
134 );
135 transport::run_hyper_server(server, HyperServerConfig::http())
136 .await
137 .map_err(|e| format!("Failed to start HTTP server: {e}"))?;
138 }
139 "sse" => {
140 tracing::info!(
141 "Using SSE transport mode, listening on {}:{}",
142 server.config().server.host,
143 server.config().server.port
144 );
145 transport::run_hyper_server(server, HyperServerConfig::sse())
146 .await
147 .map_err(|e| format!("Failed to start SSE server: {e}"))?;
148 }
149 "hybrid" => {
150 tracing::info!(
151 "Using hybrid transport mode (HTTP + SSE), listening on {}:{}",
152 server.config().server.host,
153 server.config().server.port
154 );
155 transport::run_hyper_server(server, HyperServerConfig::hybrid())
156 .await
157 .map_err(|e| format!("Failed to start hybrid server: {e}"))?;
158 }
159 _ => {
160 return Err(format!("Unknown transport mode: {transport_mode}").into());
161 }
162 }
163 Ok(())
164}
165
166#[allow(clippy::too_many_arguments)]
168pub async fn run_serve_command(
169 config_path: &PathBuf,
170 debug: bool,
171 verbose: bool,
172 mode: Option<String>,
173 host: Option<String>,
174 port: Option<u16>,
175 enable_oauth: Option<bool>,
176 oauth_client_id: Option<String>,
177 oauth_client_secret: Option<String>,
178 oauth_redirect_uri: Option<String>,
179 enable_api_key: Option<bool>,
180 api_keys: Option<String>,
181 api_key_header: Option<String>,
182 api_key_query_param: Option<bool>,
183) -> Result<(), Box<dyn std::error::Error>> {
184 let config = load_config(
185 config_path,
186 host,
187 port,
188 mode,
189 enable_oauth,
190 oauth_client_id,
191 oauth_client_secret,
192 oauth_redirect_uri,
193 enable_api_key,
194 api_keys,
195 api_key_header,
196 api_key_query_param,
197 )?;
198
199 let transport_mode = &config.server.transport_mode;
200
201 init_logging(&config, debug, verbose)?;
202
203 tracing::info!(
204 "Starting Crates Docs MCP Server v{}",
205 env!("CARGO_PKG_VERSION")
206 );
207
208 let server: CratesDocsServer = CratesDocsServer::new_async(config.clone())
209 .await
210 .map_err(|e| format!("Failed to create server: {e}"))?;
211
212 let mode_str = transport_mode.to_lowercase();
213 let should_enable_reload = matches!(mode_str.as_str(), "http" | "sse" | "hybrid");
214
215 if should_enable_reload && config_path.exists() {
216 start_config_reloader(config_path, &server);
217 }
218
219 run_server_by_mode(&server, transport_mode).await
220}
221
222#[allow(clippy::too_many_arguments)]
224fn load_config(
225 config_path: &PathBuf,
226 host: Option<String>,
227 port: Option<u16>,
228 mode: Option<String>,
229 enable_oauth: Option<bool>,
230 oauth_client_id: Option<String>,
231 oauth_client_secret: Option<String>,
232 oauth_redirect_uri: Option<String>,
233 enable_api_key: Option<bool>,
234 api_keys: Option<String>,
235 api_key_header: Option<String>,
236 api_key_query_param: Option<bool>,
237) -> Result<crate::config::AppConfig, Box<dyn std::error::Error>> {
238 let mut config = if config_path.exists() {
239 tracing::info!("Loading configuration from file: {}", config_path.display());
240 crate::config::AppConfig::from_file(config_path)
241 .map_err(|e| format!("Failed to load config file: {e}"))?
242 } else {
243 tracing::warn!(
244 "Config file does not exist, using default config: {}",
245 config_path.display()
246 );
247 crate::config::AppConfig::default()
248 };
249
250 load_from_env(&mut config)?;
251
252 if let Some(h) = host {
254 config.server.host = h;
255 tracing::info!(
256 "Command line argument overrides host: {}",
257 config.server.host
258 );
259 }
260 if let Some(p) = port {
261 config.server.port = p;
262 tracing::info!(
263 "Command line argument overrides port: {}",
264 config.server.port
265 );
266 }
267 if let Some(m) = mode {
268 config.server.transport_mode = m;
269 tracing::info!(
270 "Command line argument overrides transport_mode: {}",
271 config.server.transport_mode
272 );
273 }
274 if let Some(eo) = enable_oauth {
275 config.server.enable_oauth = eo;
276 tracing::info!(
277 "Command line argument overrides enable_oauth: {}",
278 config.server.enable_oauth
279 );
280 }
281
282 if let Some(client_id) = oauth_client_id {
284 config.oauth.client_id = Some(client_id);
285 config.oauth.enabled = true;
286 }
287 if let Some(client_secret) = oauth_client_secret {
288 config.oauth.client_secret = Some(client_secret);
289 }
290 if let Some(redirect_uri) = oauth_redirect_uri {
291 config.oauth.redirect_uri = Some(redirect_uri);
292 }
293
294 if let Some(eak) = enable_api_key {
296 config.auth.api_key.enabled = eak;
297 tracing::info!(
298 "Command line argument overrides enable_api_key: {}",
299 config.auth.api_key.enabled
300 );
301 }
302 if let Some(keys) = api_keys {
303 let parsed_keys: Vec<String> = keys
304 .split(',')
305 .map(str::trim)
306 .filter(|s| !s.is_empty())
307 .map(ToOwned::to_owned)
308 .collect();
309
310 if !parsed_keys.is_empty() {
311 #[cfg(feature = "api-key")]
312 {
313 config.auth.api_key.keys = normalize_api_keys(&config.auth.api_key, parsed_keys)?;
314 }
315 #[cfg(not(feature = "api-key"))]
316 {
317 config.auth.api_key.keys = parsed_keys;
318 }
319 config.auth.api_key.enabled = true;
320 tracing::info!("Command line argument provided API key material");
321 }
322 }
323 if let Some(header) = api_key_header {
324 config.auth.api_key.header_name = header;
325 tracing::info!(
326 "Command line argument overrides api_key_header: {}",
327 config.auth.api_key.header_name
328 );
329 }
330 if let Some(allow_query) = api_key_query_param {
331 config.auth.api_key.allow_query_param = allow_query;
332 tracing::info!(
333 "Command line argument overrides api_key_query_param: {}",
334 config.auth.api_key.allow_query_param
335 );
336 }
337
338 config
340 .validate()
341 .map_err(|e| format!("Configuration validation failed: {e}"))?;
342
343 Ok(config)
344}