Skip to main content

crates_docs/cli/
serve_cmd.rs

1//! Serve command implementation
2
3use 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/// Start server command
144#[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/// Load configuration
199#[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    // Only override config file when command line arguments are explicitly provided
229    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    // Override command line OAuth parameters (if provided)
259    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    // Override command line API Key parameters (if provided)
271    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    // Validate configuration
315    config
316        .validate()
317        .map_err(|e| format!("Configuration validation failed: {e}"))?;
318
319    Ok(config)
320}