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