1use crate::error::Result;
31use crate::server::handler::CratesDocsHandler;
32use crate::server::CratesDocsServer;
33use rust_mcp_sdk::{
34 error::McpSdkError,
35 event_store,
36 mcp_server::{hyper_server, server_runtime, HyperServerOptions, McpServerOptions},
37 McpServer, StdioTransport, ToMcpServerHandler, TransportOptions,
38};
39use std::sync::Arc;
40
41pub async fn run_stdio_server(server: &CratesDocsServer) -> Result<()> {
68 tracing::info!("Starting Stdio MCP server...");
69
70 let server_info = server.server_info();
71 let handler = CratesDocsHandler::new(Arc::new(server.clone()));
72
73 let transport = StdioTransport::new(TransportOptions::default())
75 .map_err(|e| crate::error::Error::mcp("transport", e.to_string()))?;
76
77 let mcp_server: Arc<rust_mcp_sdk::mcp_server::ServerRuntime> =
79 server_runtime::create_server(McpServerOptions {
80 server_details: server_info,
81 transport,
82 handler: handler.to_mcp_server_handler(),
83 task_store: None,
84 client_task_store: None,
85 message_observer: None,
86 });
87
88 tracing::info!("Stdio MCP server started, waiting for connections...");
89 mcp_server
90 .start()
91 .await
92 .map_err(|e: McpSdkError| crate::error::Error::mcp("server_start", e.to_string()))?;
93
94 Ok(())
95}
96
97#[derive(Debug, Clone)]
111pub struct HyperServerConfig {
112 protocol_name: String,
114 sse_support: bool,
116}
117
118impl HyperServerConfig {
119 #[must_use]
123 pub fn http() -> Self {
124 Self {
125 protocol_name: "HTTP".to_string(),
126 sse_support: false,
127 }
128 }
129
130 #[must_use]
134 pub fn sse() -> Self {
135 Self {
136 protocol_name: "SSE".to_string(),
137 sse_support: true,
138 }
139 }
140
141 #[must_use]
145 pub fn hybrid() -> Self {
146 Self {
147 protocol_name: "Hybrid".to_string(),
148 sse_support: true,
149 }
150 }
151
152 #[must_use]
154 pub fn protocol_name(&self) -> &str {
155 &self.protocol_name
156 }
157
158 #[must_use]
160 pub fn sse_support(&self) -> bool {
161 self.sse_support
162 }
163}
164
165#[cfg(all(feature = "api-key", feature = "auth"))]
174fn api_key_auth_enforced(server_config: &crate::config::AppConfig) -> bool {
175 server_config.auth.api_key.enabled
176}
177
178#[cfg(not(all(feature = "api-key", feature = "auth")))]
181fn api_key_auth_enforced(_server_config: &crate::config::AppConfig) -> bool {
182 false
183}
184
185#[cfg(all(feature = "api-key", feature = "auth"))]
191fn build_api_key_auth(
192 server_config: &crate::config::AppConfig,
193) -> Option<Arc<dyn rust_mcp_sdk::auth::AuthProvider>> {
194 server_config.auth.api_key.enabled.then(|| {
195 Arc::new(crate::server::auth::ApiKeyAuthProvider::new(
196 server_config.auth.api_key.clone(),
197 )) as Arc<dyn rust_mcp_sdk::auth::AuthProvider>
198 })
199}
200
201fn warn_if_auth_configured_but_unenforced(server_config: &crate::config::AppConfig) {
211 #[cfg(feature = "api-key")]
212 if server_config.auth.api_key.enabled {
213 if api_key_auth_enforced(server_config) {
214 tracing::info!(
215 "API key authentication is ENFORCED on the HTTP/SSE transport: clients must send \
216 `Authorization: Bearer <key>` or receive 401 (the /health endpoint stays open). \
217 The in-process layer does not read the `X-API-Key` header directly — to keep \
218 using `X-API-Key` and to encrypt traffic with TLS, front the server with the \
219 bundled reverse proxy (docs/reverse-proxy/)."
220 );
221 } else {
222 tracing::warn!(
223 "API key authentication is enabled in configuration but is NOT enforced: this \
224 binary was built without the `auth` feature, so HTTP/SSE requests are \
225 unauthenticated. Rebuild with the `auth` feature (it is in the default set) or \
226 front the server with an authenticating reverse proxy. Do not expose this \
227 server on an untrusted network."
228 );
229 }
230 }
231
232 if server_config.auth.oauth.enabled || server_config.oauth.enabled {
234 tracing::warn!(
235 "OAuth authentication is enabled in configuration but is NOT enforced on the \
236 HTTP/SSE transport: OAuth requests are not validated. Do not rely on it for access \
237 control; use API-key authentication or an authenticating reverse proxy instead."
238 );
239 }
240}
241
242#[cfg(all(feature = "api-key", feature = "auth"))]
251fn warn_if_api_key_header_settings_ignored(server_config: &crate::config::AppConfig) {
252 if !api_key_auth_enforced(server_config) {
253 return;
254 }
255 let non_default_header = !server_config
256 .auth
257 .api_key
258 .header_name
259 .eq_ignore_ascii_case("x-api-key");
260 let query_allowed = server_config.auth.api_key.allow_query_param;
261 if non_default_header || query_allowed {
262 tracing::warn!(
263 header_name = %server_config.auth.api_key.header_name,
264 allow_query_param = query_allowed,
265 "In-process API-key enforcement reads ONLY `Authorization: Bearer <key>`; the \
266 configured `header_name` and `allow_query_param` are ignored by the server and take \
267 effect only at a fronting reverse proxy. Translate your custom header / query param \
268 into `Authorization: Bearer <key>` at the proxy — see docs/reverse-proxy/."
269 );
270 }
271}
272
273fn warn_if_metrics_configured_but_unavailable(server_config: &crate::config::AppConfig) {
281 if server_config.performance.enable_metrics {
282 tracing::warn!(
283 metrics_port = server_config.performance.metrics_port,
284 "performance.enable_metrics is set, but this server does not yet collect or expose \
285 Prometheus metrics: no metrics endpoint is served and no request metrics are recorded. \
286 This setting currently has no effect."
287 );
288 }
289}
290
291fn unenforced_server_limits(server_config: &crate::config::AppConfig) -> Vec<&'static str> {
299 let defaults = crate::config::ServerConfig::default();
300 let mut unenforced = Vec::new();
301 if server_config.server.request_timeout_secs != defaults.request_timeout_secs {
302 unenforced.push("request_timeout_secs");
303 }
304 if server_config.server.response_timeout_secs != defaults.response_timeout_secs {
305 unenforced.push("response_timeout_secs");
306 }
307 if server_config.server.max_connections != defaults.max_connections {
308 unenforced.push("max_connections");
309 }
310 unenforced
311}
312
313fn warn_if_unenforced_server_limits_configured(server_config: &crate::config::AppConfig) {
314 let unenforced = unenforced_server_limits(server_config);
315 if !unenforced.is_empty() {
316 tracing::warn!(
317 fields = unenforced.join(", "),
318 "These server limit settings are configured with non-default values but are NOT \
319 enforced: the HTTP transport applies neither request/response timeouts nor a maximum \
320 connection cap. These settings currently have no effect."
321 );
322 }
323}
324
325fn enable_sse_setting_ignored(configured_enable_sse: bool, sse_active: bool) -> bool {
331 configured_enable_sse != sse_active
332}
333
334fn warn_if_enable_sse_ignored(server_config: &crate::config::AppConfig, sse_active: bool) {
336 if enable_sse_setting_ignored(server_config.server.enable_sse, sse_active) {
337 tracing::warn!(
338 configured_enable_sse = server_config.server.enable_sse,
339 sse_active,
340 "server.enable_sse does not match the active transport and is being ignored: SSE \
341 support is determined solely by transport_mode (sse/hybrid serve SSE, http does not). \
342 Set transport_mode to control SSE; the enable_sse flag has no effect."
343 );
344 }
345}
346
347fn host_is_loopback(host: &str) -> bool {
353 match host.parse::<std::net::IpAddr>() {
354 Ok(ip) => ip.is_loopback(),
355 Err(_) => host.eq_ignore_ascii_case("localhost"),
356 }
357}
358
359fn warn_if_network_exposed(server_config: &crate::config::AppConfig) {
366 if host_is_loopback(&server_config.server.host) {
367 return;
368 }
369 if api_key_auth_enforced(server_config) {
370 tracing::warn!(
371 host = %server_config.server.host,
372 "Server is binding to a non-loopback address and is reachable from other hosts on \
373 the network. API-key authentication IS enforced (requests need `Authorization: \
374 Bearer <key>`), but traffic is sent UNENCRYPTED over plain HTTP: anyone who can \
375 observe the network sees requests and keys in clear text. Terminate TLS with the \
376 bundled reverse proxy (docs/reverse-proxy/) or restrict the network."
377 );
378 } else {
379 tracing::warn!(
380 host = %server_config.server.host,
381 "Server is binding to a non-loopback address and is reachable from other hosts on \
382 the network. The HTTP/SSE transport performs no authentication; put a reverse proxy \
383 with authentication in front of it, restrict the network, or run in stdio mode."
384 );
385 }
386}
387
388fn warn_if_dns_rebinding_protection_disabled(server_config: &crate::config::AppConfig) {
396 if !server_config.server.dns_rebinding_protection {
397 tracing::warn!(
398 "dns_rebinding_protection is disabled: the allowed_hosts/allowed_origins allowlists \
399 are NOT enforced, so a malicious local web page could reach this server via DNS \
400 rebinding. Set server.dns_rebinding_protection = true (with exact host:port and \
401 origin values) to enable Host/Origin validation."
402 );
403 }
404}
405
406pub async fn run_hyper_server(server: &CratesDocsServer, config: HyperServerConfig) -> Result<()> {
435 let server_config = server.config();
436 let server_info = server.server_info();
437 let handler = CratesDocsHandler::new(Arc::new(server.clone()));
438
439 tracing::info!(
440 "Starting {} MCP server on {}:{}...",
441 config.protocol_name(),
442 server_config.server.host,
443 server_config.server.port
444 );
445
446 warn_if_auth_configured_but_unenforced(server_config);
447 #[cfg(all(feature = "api-key", feature = "auth"))]
448 warn_if_api_key_header_settings_ignored(server_config);
449 warn_if_metrics_configured_but_unavailable(server_config);
450 warn_if_unenforced_server_limits_configured(server_config);
451 warn_if_enable_sse_ignored(server_config, config.sse_support());
452 warn_if_network_exposed(server_config);
453 warn_if_dns_rebinding_protection_disabled(server_config);
454
455 let options = HyperServerOptions {
457 host: server_config.server.host.clone(),
458 port: server_config.server.port,
459 transport_options: Arc::new(TransportOptions::default()),
460 sse_support: config.sse_support(),
461 event_store: Some(Arc::new(event_store::InMemoryEventStore::default())),
462 task_store: None,
463 client_task_store: None,
464 allowed_hosts: Some(server_config.server.allowed_hosts.clone()),
465 allowed_origins: Some(server_config.server.allowed_origins.clone()),
466 dns_rebinding_protection: server_config.server.dns_rebinding_protection,
470 health_endpoint: Some("/health".to_string()),
471 #[cfg(all(feature = "api-key", feature = "auth"))]
477 auth: build_api_key_auth(server_config),
478 ..Default::default()
479 };
480
481 if server_config.server.dns_rebinding_protection
482 && server_config.server.allowed_hosts.is_empty()
483 && server_config.server.allowed_origins.is_empty()
484 {
485 tracing::warn!(
486 "dns_rebinding_protection is enabled but both allowed_hosts and allowed_origins are empty; no Host/Origin validation will occur"
487 );
488 }
489
490 let mcp_server =
492 hyper_server::create_server(server_info, handler.to_mcp_server_handler(), options);
493
494 let started_msg = if config.sse_support() && config.protocol_name() != "SSE" {
496 format!(
498 "{} MCP server started, listening on {}:{} (HTTP + SSE)",
499 config.protocol_name(),
500 server_config.server.host,
501 server_config.server.port
502 )
503 } else {
504 format!(
505 "{} MCP server started, listening on {}:{}",
506 config.protocol_name(),
507 server_config.server.host,
508 server_config.server.port
509 )
510 };
511 tracing::info!("{}", started_msg);
512
513 mcp_server
514 .start()
515 .await
516 .map_err(|e: McpSdkError| crate::error::Error::mcp("server_start", e.to_string()))?;
517
518 Ok(())
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
543pub enum TransportMode {
544 Stdio,
546 Http,
548 Sse,
550 Hybrid,
552}
553
554impl std::str::FromStr for TransportMode {
555 type Err = String;
556
557 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
558 match s.to_lowercase().as_str() {
559 "stdio" => Ok(TransportMode::Stdio),
560 "http" => Ok(TransportMode::Http),
561 "sse" => Ok(TransportMode::Sse),
562 "hybrid" => Ok(TransportMode::Hybrid),
563 _ => Err(format!("Unknown transport mode: {s}")),
564 }
565 }
566}
567
568impl std::fmt::Display for TransportMode {
569 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
570 match self {
571 TransportMode::Stdio => write!(f, "stdio"),
572 TransportMode::Http => write!(f, "http"),
573 TransportMode::Sse => write!(f, "sse"),
574 TransportMode::Hybrid => write!(f, "hybrid"),
575 }
576 }
577}
578
579impl TransportMode {
580 #[must_use]
582 pub fn to_hyper_config(&self) -> Option<HyperServerConfig> {
583 match self {
584 TransportMode::Stdio => None,
585 TransportMode::Http => Some(HyperServerConfig::http()),
586 TransportMode::Sse => Some(HyperServerConfig::sse()),
587 TransportMode::Hybrid => Some(HyperServerConfig::hybrid()),
588 }
589 }
590}
591
592pub async fn run_server_with_mode(server: &CratesDocsServer, mode: TransportMode) -> Result<()> {
594 match mode {
595 TransportMode::Stdio => run_stdio_server(server).await,
596 TransportMode::Http | TransportMode::Sse | TransportMode::Hybrid => {
597 let config = mode
598 .to_hyper_config()
599 .expect("Hyper config should exist for HTTP/SSE/Hybrid");
600 run_hyper_server(server, config).await
601 }
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::unenforced_server_limits;
608 use crate::config::AppConfig;
609
610 #[test]
611 fn test_unenforced_limits_empty_for_defaults() {
612 let config = AppConfig::default();
613 assert!(unenforced_server_limits(&config).is_empty());
614 }
615
616 #[test]
617 fn test_unenforced_limits_flags_changed_fields() {
618 let mut config = AppConfig::default();
619 config.server.request_timeout_secs += 1;
620 config.server.max_connections += 1;
621 let flagged = unenforced_server_limits(&config);
622 assert!(flagged.contains(&"request_timeout_secs"));
623 assert!(flagged.contains(&"max_connections"));
624 assert!(!flagged.contains(&"response_timeout_secs"));
625 }
626
627 #[test]
628 fn test_host_is_loopback() {
629 assert!(super::host_is_loopback("127.0.0.1"));
630 assert!(super::host_is_loopback("::1"));
631 assert!(super::host_is_loopback("localhost"));
632 assert!(super::host_is_loopback("LocalHost"));
633 assert!(!super::host_is_loopback("0.0.0.0"));
635 assert!(!super::host_is_loopback("::"));
636 assert!(!super::host_is_loopback("192.168.1.5"));
637 assert!(!super::host_is_loopback("example.com"));
638 }
639
640 #[test]
641 fn test_enable_sse_setting_ignored() {
642 assert!(!super::enable_sse_setting_ignored(true, true));
644 assert!(!super::enable_sse_setting_ignored(false, false));
645 assert!(super::enable_sse_setting_ignored(false, true));
647 assert!(super::enable_sse_setting_ignored(true, false));
648 }
649
650 #[cfg(all(feature = "api-key", feature = "auth"))]
651 #[test]
652 fn test_api_key_auth_enforced_tracks_enabled_flag() {
653 let mut config = AppConfig::default();
654 assert!(!super::api_key_auth_enforced(&config));
656 config.auth.api_key.enabled = true;
658 assert!(super::api_key_auth_enforced(&config));
659 }
660
661 #[cfg(all(feature = "api-key", feature = "auth"))]
662 #[test]
663 fn test_build_api_key_auth_follows_enabled_flag() {
664 let mut config = AppConfig::default();
665 assert!(super::build_api_key_auth(&config).is_none());
666 config.auth.api_key.enabled = true;
667 assert!(super::build_api_key_auth(&config).is_some());
668 }
669}