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    // Using mem::take to move ownership without cloning, leaving default values in place
32    *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/// Start server command
147#[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/// Load configuration
202#[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    // Only override config file when command line arguments are explicitly provided
232    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    // Override command line OAuth parameters (if provided)
262    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    // Override command line API Key parameters (if provided)
274    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    // Validate configuration
318    config
319        .validate()
320        .map_err(|e| format!("Configuration validation failed: {e}"))?;
321
322    Ok(config)
323}