1use std::{
2 future::Future,
3 net::{IpAddr, SocketAddr},
4 num::NonZeroUsize,
5 path::{Path, PathBuf},
6 pin::Pin,
7 sync::Arc,
8 time::Duration,
9};
10
11use arc_swap::ArcSwap;
12use axum::{
13 body::Body,
14 extract::{ConnectInfo, Request},
15 middleware::Next,
16 response::IntoResponse,
17};
18use rmcp::{
19 ServerHandler,
20 transport::streamable_http_server::{
21 StreamableHttpServerConfig, StreamableHttpService,
22 session::{EventStore, SessionStore, local::LocalSessionManager},
23 },
24};
25use rustls::RootCertStore;
26use secrecy::{ExposeSecret as _, SecretString};
27use tokio::{
28 net::TcpListener,
29 sync::{Semaphore, mpsc},
30};
31use tokio_util::sync::CancellationToken;
32
33use crate::{
34 auth::{
35 AuthConfig, AuthIdentity, AuthState, MtlsConfig, TlsConnInfo, auth_middleware,
36 build_rate_limiter, extract_mtls_identity,
37 },
38 bounded_limiter::{BoundedKeyedLimiter, BoundedLimiterDeny, KeyEvictionPolicy},
39 error::RmcpServerKitError,
40 mtls_revocation::{self, CrlSet, DynamicClientCertVerifier},
41 rbac::{RbacPolicy, ToolRateLimiter, build_tool_rate_limiter_with_policy, rbac_middleware},
42 rbac_context::RbacContextHandler,
43 session_binding::{
44 configured_session_binding_secret, process_session_binding_secret,
45 session_binding_middleware,
46 },
47};
48
49#[allow(
53 clippy::needless_pass_by_value,
54 reason = "consumed at .map_err(anyhow_to_startup) call sites; by-value matches the closure shape"
55)]
56fn anyhow_to_startup(e: anyhow::Error) -> RmcpServerKitError {
57 RmcpServerKitError::Startup(format!("{e:#}"))
58}
59
60#[allow(
66 clippy::needless_pass_by_value,
67 reason = "consumed at .map_err(|e| io_to_startup(...)) call sites; by-value matches the closure shape"
68)]
69fn io_to_startup(op: &str, e: std::io::Error) -> RmcpServerKitError {
70 RmcpServerKitError::Startup(format!("{op}: {e}"))
71}
72
73pub type ReadinessCheck =
78 Arc<dyn Fn() -> Pin<Box<dyn Future<Output = serde_json::Value> + Send>> + Send + Sync>;
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
125#[non_exhaustive]
126pub struct PeerAddr {
127 pub addr: SocketAddr,
129}
130
131impl PeerAddr {
132 #[must_use]
135 pub(crate) const fn new(addr: SocketAddr) -> Self {
136 Self { addr }
137 }
138}
139
140impl<S: Send + Sync> axum::extract::FromRequestParts<S> for PeerAddr {
149 type Rejection = (axum::http::StatusCode, &'static str);
150
151 #[allow(
152 clippy::unused_async_trait_impl,
153 reason = "async is mandated by the axum FromRequestParts trait signature; this impl only reads a request extension synchronously"
154 )]
155 async fn from_request_parts(
156 parts: &mut axum::http::request::Parts,
157 _state: &S,
158 ) -> Result<Self, Self::Rejection> {
159 parts.extensions.get::<Self>().copied().ok_or((
160 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
161 "peer address unavailable: not running under rmcp-server-kit serve()",
162 ))
163 }
164}
165
166#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
189#[non_exhaustive]
190pub struct ClientIp {
191 pub ip: IpAddr,
193}
194
195impl ClientIp {
196 #[must_use]
199 pub(crate) const fn new(ip: IpAddr) -> Self {
200 Self { ip }
201 }
202}
203
204#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Deserialize)]
209#[serde(rename_all = "kebab-case")]
210#[non_exhaustive]
211pub enum ForwardedHeaderMode {
212 XForwardedFor,
214 Forwarded,
216}
217
218struct ForwardResolver {
221 trusted: Vec<ipnet::IpNet>,
222 mode: ForwardedHeaderMode,
223 max_scanned_entries: usize,
224}
225
226#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize)]
247#[serde(default)]
248#[serde(deny_unknown_fields)]
249#[non_exhaustive]
250pub struct SecurityHeadersConfig {
251 pub x_content_type_options: Option<String>,
253 pub x_frame_options: Option<String>,
255 pub cache_control: Option<String>,
257 pub referrer_policy: Option<String>,
259 pub cross_origin_opener_policy: Option<String>,
261 pub cross_origin_resource_policy: Option<String>,
263 pub cross_origin_embedder_policy: Option<String>,
265 pub permissions_policy: Option<String>,
268 pub x_permitted_cross_domain_policies: Option<String>,
270 pub content_security_policy: Option<String>,
273 pub x_dns_prefetch_control: Option<String>,
275 pub strict_transport_security: Option<String>,
280}
281
282#[allow(
284 missing_debug_implementations,
285 reason = "contains callback/trait objects that don't impl Debug"
286)]
287#[allow(
288 clippy::struct_excessive_bools,
289 reason = "server configuration naturally has many boolean feature flags"
290)]
291#[non_exhaustive]
292pub struct McpServerConfig {
293 #[deprecated(
295 since = "0.13.0",
296 note = "use McpServerConfig::new() / with_bind_addr(); direct field access will become pub(crate) in a future major release"
297 )]
298 pub bind_addr: String,
299 #[deprecated(
301 since = "0.13.0",
302 note = "set via McpServerConfig::new(); direct field access will become pub(crate) in a future major release"
303 )]
304 pub name: String,
305 #[deprecated(
307 since = "0.13.0",
308 note = "set via McpServerConfig::new(); direct field access will become pub(crate) in a future major release"
309 )]
310 pub version: String,
311 #[deprecated(
313 since = "0.13.0",
314 note = "use McpServerConfig::with_tls(); direct field access will become pub(crate) in a future major release"
315 )]
316 pub tls_cert_path: Option<PathBuf>,
317 #[deprecated(
319 since = "0.13.0",
320 note = "use McpServerConfig::with_tls(); direct field access will become pub(crate) in a future major release"
321 )]
322 pub tls_key_path: Option<PathBuf>,
323 #[deprecated(
326 since = "0.13.0",
327 note = "use McpServerConfig::with_auth(); direct field access will become pub(crate) in a future major release"
328 )]
329 pub auth: Option<AuthConfig>,
330 #[deprecated(
333 since = "0.13.0",
334 note = "use McpServerConfig::with_rbac(); direct field access will become pub(crate) in a future major release"
335 )]
336 pub rbac: Option<Arc<RbacPolicy>>,
337 pub tool_list_filtering: bool,
340 #[deprecated(
346 since = "0.13.0",
347 note = "use McpServerConfig::with_allowed_origins(); direct field access will become pub(crate) in a future major release"
348 )]
349 pub allowed_origins: Vec<String>,
350 #[deprecated(
353 since = "0.13.0",
354 note = "use McpServerConfig::with_tool_rate_limit(); direct field access will become pub(crate) in a future major release"
355 )]
356 pub tool_rate_limit: Option<u32>,
357 #[deprecated(
363 since = "1.12.0",
364 note = "use McpServerConfig::with_tool_rate_limit_burst(); direct field access will become pub(crate) in a future major release"
365 )]
366 pub tool_rate_limit_burst: Option<u32>,
367 #[deprecated(
380 since = "1.11.0",
381 note = "use McpServerConfig::with_extra_route_rate_limit(); direct field access will become pub(crate) in a future major release"
382 )]
383 pub extra_route_rate_limit: Option<u32>,
384 #[deprecated(
391 since = "1.12.0",
392 note = "use McpServerConfig::with_extra_route_rate_limit_burst(); direct field access will become pub(crate) in a future major release"
393 )]
394 pub extra_route_rate_limit_burst: Option<u32>,
395 #[deprecated(
408 since = "1.14.0",
409 note = "use McpServerConfig::with_extra_route_rate_limit_exempt_paths(); direct field access will become pub(crate) in a future major release"
410 )]
411 pub extra_route_rate_limit_exempt_paths: Vec<String>,
412
413 pub key_eviction_policy: KeyEvictionPolicy,
415
416 pub trusted_forwarder_max_entries: usize,
423 #[deprecated(
431 since = "1.13.0",
432 note = "use McpServerConfig::with_trusted_proxies(); direct field access will become pub(crate) in a future major release"
433 )]
434 pub trusted_proxies: Vec<String>,
435 #[deprecated(
440 since = "1.13.0",
441 note = "use McpServerConfig::with_forwarded_header(); direct field access will become pub(crate) in a future major release"
442 )]
443 pub forwarded_header: Option<ForwardedHeaderMode>,
444 #[deprecated(
447 since = "0.13.0",
448 note = "use McpServerConfig::with_readiness_check(); direct field access will become pub(crate) in a future major release"
449 )]
450 pub readiness_check: Option<ReadinessCheck>,
451 #[deprecated(
454 since = "0.13.0",
455 note = "use McpServerConfig::with_max_request_body(); direct field access will become pub(crate) in a future major release"
456 )]
457 pub max_request_body: usize,
458 #[deprecated(
461 since = "0.13.0",
462 note = "use McpServerConfig::with_request_timeout(); direct field access will become pub(crate) in a future major release"
463 )]
464 pub request_timeout: Duration,
465 #[deprecated(
468 since = "0.13.0",
469 note = "use McpServerConfig::with_shutdown_timeout(); direct field access will become pub(crate) in a future major release"
470 )]
471 pub shutdown_timeout: Duration,
472 #[deprecated(
475 since = "0.13.0",
476 note = "use McpServerConfig::with_session_idle_timeout(); direct field access will become pub(crate) in a future major release"
477 )]
478 pub session_idle_timeout: Duration,
479 pub session_binding: bool,
487 pub session_binding_secret: Option<SecretString>,
491 pub session_store: Option<Arc<dyn SessionStore>>,
493 pub event_store: Option<Arc<dyn EventStore>>,
506 #[deprecated(
509 since = "0.13.0",
510 note = "use McpServerConfig::with_sse_keep_alive(); direct field access will become pub(crate) in a future major release"
511 )]
512 pub sse_keep_alive: Duration,
513 #[deprecated(
517 since = "0.13.0",
518 note = "use McpServerConfig::with_reload_callback(); direct field access will become pub(crate) in a future major release"
519 )]
520 pub on_reload_ready: Option<Box<dyn FnOnce(ReloadHandle) + Send>>,
521 #[deprecated(
528 since = "0.13.0",
529 note = "use McpServerConfig::with_extra_router(); direct field access will become pub(crate) in a future major release"
530 )]
531 pub extra_router: Option<axum::Router>,
532 #[deprecated(
537 since = "0.13.0",
538 note = "use McpServerConfig::with_public_url(); direct field access will become pub(crate) in a future major release"
539 )]
540 pub public_url: Option<String>,
541 #[deprecated(
544 since = "0.13.0",
545 note = "use McpServerConfig::enable_request_header_logging(); direct field access will become pub(crate) in a future major release"
546 )]
547 pub log_request_headers: bool,
548 pub expose_build_metadata: bool,
555 #[deprecated(
558 since = "0.13.0",
559 note = "use McpServerConfig::enable_compression(); direct field access will become pub(crate) in a future major release"
560 )]
561 pub compression_enabled: bool,
562 #[deprecated(
565 since = "0.13.0",
566 note = "use McpServerConfig::enable_compression(); direct field access will become pub(crate) in a future major release"
567 )]
568 pub compression_min_size: u16,
569 #[deprecated(
573 since = "0.13.0",
574 note = "use McpServerConfig::with_max_concurrent_requests(); direct field access will become pub(crate) in a future major release"
575 )]
576 pub max_concurrent_requests: Option<usize>,
577 #[deprecated(
580 since = "0.13.0",
581 note = "use McpServerConfig::enable_admin(); direct field access will become pub(crate) in a future major release"
582 )]
583 pub admin_enabled: bool,
584 #[deprecated(
586 since = "0.13.0",
587 note = "use McpServerConfig::enable_admin(); direct field access will become pub(crate) in a future major release"
588 )]
589 pub admin_role: String,
590 #[cfg(feature = "metrics")]
593 #[deprecated(
594 since = "0.13.0",
595 note = "use McpServerConfig::with_metrics(); direct field access will become pub(crate) in a future major release"
596 )]
597 pub metrics_enabled: bool,
598 #[cfg(feature = "metrics")]
600 #[deprecated(
601 since = "0.13.0",
602 note = "use McpServerConfig::with_metrics(); direct field access will become pub(crate) in a future major release"
603 )]
604 pub metrics_bind: String,
605 #[deprecated(
609 since = "1.5.0",
610 note = "use McpServerConfig::with_security_headers(); direct field access will become pub(crate) in a future major release"
611 )]
612 pub security_headers: SecurityHeadersConfig,
613 #[deprecated(
619 since = "1.9.0",
620 note = "use McpServerConfig::with_tls_handshake_timeout(); direct field access will become pub(crate) in a future major release"
621 )]
622 pub tls_handshake_timeout: Duration,
623 #[deprecated(
630 since = "1.9.0",
631 note = "use McpServerConfig::with_max_concurrent_tls_handshakes(); direct field access will become pub(crate) in a future major release"
632 )]
633 pub max_concurrent_tls_handshakes: usize,
634}
635
636#[allow(
694 missing_debug_implementations,
695 reason = "wraps T which may not implement Debug; manual impl below avoids leaking inner contents into logs"
696)]
697pub struct Validated<T>(T);
698
699impl<T> std::fmt::Debug for Validated<T> {
700 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
701 f.debug_struct("Validated").finish_non_exhaustive()
702 }
703}
704
705impl<T> Validated<T> {
706 #[must_use]
708 pub fn as_inner(&self) -> &T {
709 &self.0
710 }
711
712 #[must_use]
717 pub fn into_inner(self) -> T {
718 self.0
719 }
720}
721
722#[allow(
723 deprecated,
724 reason = "internal builders/validators legitimately read/write the deprecated `pub` fields they were designed to manage"
725)]
726impl McpServerConfig {
727 #[must_use]
735 pub fn new(
736 bind_addr: impl Into<String>,
737 name: impl Into<String>,
738 version: impl Into<String>,
739 ) -> Self {
740 Self {
741 bind_addr: bind_addr.into(),
742 name: name.into(),
743 version: version.into(),
744 tls_cert_path: None,
745 tls_key_path: None,
746 auth: None,
747 rbac: None,
748 tool_list_filtering: true,
749 allowed_origins: Vec::new(),
750 tool_rate_limit: None,
751 readiness_check: None,
752 max_request_body: 1024 * 1024,
753 request_timeout: Duration::from_mins(2),
754 shutdown_timeout: Duration::from_secs(30),
755 session_idle_timeout: Duration::from_mins(20),
756 session_binding: true,
757 session_binding_secret: None,
758 session_store: None,
759 event_store: None,
760 sse_keep_alive: Duration::from_secs(15),
761 on_reload_ready: None,
762 extra_router: None,
763 public_url: None,
764 log_request_headers: false,
765 expose_build_metadata: false,
766 compression_enabled: false,
767 compression_min_size: 1024,
768 max_concurrent_requests: None,
769 admin_enabled: false,
770 admin_role: "admin".to_owned(),
771 #[cfg(feature = "metrics")]
772 metrics_enabled: false,
773 #[cfg(feature = "metrics")]
774 metrics_bind: "127.0.0.1:9090".into(),
775 security_headers: SecurityHeadersConfig::default(),
776 tls_handshake_timeout: DEFAULT_TLS_HANDSHAKE_TIMEOUT,
777 max_concurrent_tls_handshakes: DEFAULT_MAX_CONCURRENT_TLS_HANDSHAKES,
778 extra_route_rate_limit: None,
779 tool_rate_limit_burst: None,
780 extra_route_rate_limit_burst: None,
781 extra_route_rate_limit_exempt_paths: Vec::new(),
782 key_eviction_policy: KeyEvictionPolicy::default(),
783 trusted_forwarder_max_entries: crate::forwarded::MAX_SCANNED_ENTRIES,
784 trusted_proxies: Vec::new(),
785 forwarded_header: None,
786 }
787 }
788
789 #[must_use]
799 pub fn with_auth(mut self, auth: AuthConfig) -> Self {
800 self.auth = Some(auth);
801 self
802 }
803
804 #[must_use]
809 pub fn with_security_headers(mut self, headers: SecurityHeadersConfig) -> Self {
810 self.security_headers = headers;
811 self
812 }
813
814 #[must_use]
818 pub fn with_bind_addr(mut self, addr: impl Into<String>) -> Self {
819 self.bind_addr = addr.into();
820 self
821 }
822
823 #[must_use]
826 pub fn with_rbac(mut self, rbac: Arc<RbacPolicy>) -> Self {
827 self.rbac = Some(rbac);
828 self
829 }
830
831 #[must_use]
837 pub const fn with_tool_list_filtering(mut self, enabled: bool) -> Self {
838 self.tool_list_filtering = enabled;
839 self
840 }
841
842 #[must_use]
846 pub fn with_tls(mut self, cert_path: impl Into<PathBuf>, key_path: impl Into<PathBuf>) -> Self {
847 self.tls_cert_path = Some(cert_path.into());
848 self.tls_key_path = Some(key_path.into());
849 self
850 }
851
852 #[must_use]
856 pub fn with_public_url(mut self, url: impl Into<String>) -> Self {
857 self.public_url = Some(url.into());
858 self
859 }
860
861 #[must_use]
865 pub fn with_allowed_origins<I, S>(mut self, origins: I) -> Self
866 where
867 I: IntoIterator<Item = S>,
868 S: Into<String>,
869 {
870 self.allowed_origins = origins.into_iter().map(Into::into).collect();
871 self
872 }
873
874 #[must_use]
907 pub fn with_extra_router(mut self, router: axum::Router) -> Self {
908 self.extra_router = Some(router);
909 self
910 }
911
912 #[must_use]
918 pub const fn with_trusted_forwarder_max_entries(mut self, max_entries: usize) -> Self {
919 self.trusted_forwarder_max_entries = max_entries;
920 self
921 }
922
923 #[must_use]
926 pub fn with_readiness_check(mut self, check: ReadinessCheck) -> Self {
927 self.readiness_check = Some(check);
928 self
929 }
930
931 #[must_use]
934 pub fn with_max_request_body(mut self, bytes: usize) -> Self {
935 self.max_request_body = bytes;
936 self
937 }
938
939 #[must_use]
941 pub fn with_request_timeout(mut self, timeout: Duration) -> Self {
942 self.request_timeout = timeout;
943 self
944 }
945
946 #[must_use]
948 pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
949 self.shutdown_timeout = timeout;
950 self
951 }
952
953 #[must_use]
955 pub fn with_session_idle_timeout(mut self, timeout: Duration) -> Self {
956 self.session_idle_timeout = timeout;
957 self
958 }
959
960 #[must_use]
964 pub const fn with_session_binding(mut self, enabled: bool) -> Self {
965 self.session_binding = enabled;
966 self
967 }
968
969 #[must_use]
971 pub fn with_session_binding_secret(mut self, secret: SecretString) -> Self {
972 self.session_binding_secret = Some(secret);
973 self
974 }
975
976 #[must_use]
978 pub fn with_session_store(mut self, session_store: Arc<dyn SessionStore>) -> Self {
979 self.session_store = Some(session_store);
980 self
981 }
982
983 #[must_use]
988 pub fn with_event_store(mut self, event_store: Arc<dyn EventStore>) -> Self {
989 self.event_store = Some(event_store);
990 self
991 }
992
993 #[must_use]
995 pub fn with_sse_keep_alive(mut self, interval: Duration) -> Self {
996 self.sse_keep_alive = interval;
997 self
998 }
999
1000 #[must_use]
1004 pub fn with_max_concurrent_requests(mut self, limit: usize) -> Self {
1005 self.max_concurrent_requests = Some(limit);
1006 self
1007 }
1008
1009 #[must_use]
1017 pub fn with_tls_handshake_timeout(mut self, timeout: Duration) -> Self {
1018 self.tls_handshake_timeout = timeout;
1019 self
1020 }
1021
1022 #[must_use]
1031 pub fn with_max_concurrent_tls_handshakes(mut self, limit: usize) -> Self {
1032 self.max_concurrent_tls_handshakes = limit;
1033 self
1034 }
1035
1036 #[must_use]
1039 pub fn with_tool_rate_limit(mut self, per_minute: u32) -> Self {
1040 self.tool_rate_limit = Some(per_minute);
1041 self
1042 }
1043
1044 #[must_use]
1055 pub fn with_extra_route_rate_limit(mut self, per_minute: u32) -> Self {
1056 self.extra_route_rate_limit = Some(per_minute);
1057 self
1058 }
1059
1060 #[must_use]
1065 pub fn with_tool_rate_limit_burst(mut self, burst: u32) -> Self {
1066 self.tool_rate_limit_burst = Some(burst);
1067 self
1068 }
1069
1070 #[must_use]
1076 pub fn with_extra_route_rate_limit_burst(mut self, burst: u32) -> Self {
1077 self.extra_route_rate_limit_burst = Some(burst);
1078 self
1079 }
1080
1081 #[must_use]
1101 pub fn with_extra_route_rate_limit_exempt_paths<I, S>(mut self, paths: I) -> Self
1102 where
1103 I: IntoIterator<Item = S>,
1104 S: Into<String>,
1105 {
1106 self.extra_route_rate_limit_exempt_paths = paths.into_iter().map(Into::into).collect();
1107 self
1108 }
1109
1110 #[must_use]
1112 pub const fn with_key_eviction_policy(mut self, policy: KeyEvictionPolicy) -> Self {
1113 self.key_eviction_policy = policy;
1114 self
1115 }
1116
1117 #[must_use]
1129 pub fn with_trusted_proxies<I, S>(mut self, proxies: I) -> Self
1130 where
1131 I: IntoIterator<Item = S>,
1132 S: Into<String>,
1133 {
1134 self.trusted_proxies = proxies.into_iter().map(Into::into).collect();
1135 self
1136 }
1137
1138 #[must_use]
1143 pub fn with_forwarded_header(mut self, mode: ForwardedHeaderMode) -> Self {
1144 self.forwarded_header = Some(mode);
1145 self
1146 }
1147
1148 #[must_use]
1152 pub fn with_reload_callback<F>(mut self, callback: F) -> Self
1153 where
1154 F: FnOnce(ReloadHandle) + Send + 'static,
1155 {
1156 self.on_reload_ready = Some(Box::new(callback));
1157 self
1158 }
1159
1160 #[must_use]
1164 pub fn enable_compression(mut self, min_size: u16) -> Self {
1165 self.compression_enabled = true;
1166 self.compression_min_size = min_size;
1167 self
1168 }
1169
1170 #[must_use]
1175 pub fn enable_admin(mut self, role: impl Into<String>) -> Self {
1176 self.admin_enabled = true;
1177 self.admin_role = role.into();
1178 self
1179 }
1180
1181 #[must_use]
1184 pub fn enable_request_header_logging(mut self) -> Self {
1185 self.log_request_headers = true;
1186 self
1187 }
1188
1189 #[must_use]
1194 pub fn expose_build_metadata(mut self) -> Self {
1195 self.expose_build_metadata = true;
1196 self
1197 }
1198
1199 #[cfg(feature = "metrics")]
1202 #[must_use]
1203 pub fn with_metrics(mut self, bind: impl Into<String>) -> Self {
1204 self.metrics_enabled = true;
1205 self.metrics_bind = bind.into();
1206 self
1207 }
1208
1209 pub fn validate(self) -> Result<Validated<Self>, RmcpServerKitError> {
1242 self.check()?;
1243 Ok(Validated(self))
1244 }
1245
1246 fn check_burst_knobs(&self) -> Result<(), RmcpServerKitError> {
1253 if self.tool_rate_limit_burst == Some(0) {
1254 return Err(RmcpServerKitError::Config(
1255 "tool_rate_limit_burst must be greater than zero".into(),
1256 ));
1257 }
1258 if self.extra_route_rate_limit_burst == Some(0) {
1259 return Err(RmcpServerKitError::Config(
1260 "extra_route_rate_limit_burst must be greater than zero".into(),
1261 ));
1262 }
1263 if self.trusted_forwarder_max_entries == 0
1264 || self.trusted_forwarder_max_entries
1265 > crate::forwarded::MAX_CONFIGURABLE_SCANNED_ENTRIES
1266 {
1267 return Err(RmcpServerKitError::Config(format!(
1268 "trusted_forwarder_max_entries must be in 1..={}, got {}",
1269 crate::forwarded::MAX_CONFIGURABLE_SCANNED_ENTRIES,
1270 self.trusted_forwarder_max_entries
1271 )));
1272 }
1273 if self.tool_rate_limit_burst.is_some() && self.tool_rate_limit.is_none() {
1274 return Err(RmcpServerKitError::Config(
1275 "tool_rate_limit_burst requires tool_rate_limit to be set".into(),
1276 ));
1277 }
1278 if self.extra_route_rate_limit_burst.is_some() && self.extra_route_rate_limit.is_none() {
1279 return Err(RmcpServerKitError::Config(
1280 "extra_route_rate_limit_burst requires extra_route_rate_limit to be set".into(),
1281 ));
1282 }
1283 if !self.extra_route_rate_limit_exempt_paths.is_empty()
1284 && self.extra_route_rate_limit.is_none()
1285 {
1286 return Err(RmcpServerKitError::Config(
1287 "extra_route_rate_limit_exempt_paths requires extra_route_rate_limit to be set"
1288 .into(),
1289 ));
1290 }
1291 for path in &self.extra_route_rate_limit_exempt_paths {
1292 if path.is_empty() || !path.starts_with('/') {
1293 return Err(RmcpServerKitError::Config(format!(
1294 "extra_route_rate_limit_exempt_paths entries must be non-empty and start with '/': {path:?}"
1295 )));
1296 }
1297 }
1298 if let Some(rl) = self.auth.as_ref().and_then(|a| a.rate_limit.as_ref()) {
1299 if rl.burst == Some(0) {
1300 return Err(RmcpServerKitError::Config(
1301 "auth rate_limit.burst must be greater than zero".into(),
1302 ));
1303 }
1304 if rl.pre_auth_burst == Some(0) {
1305 return Err(RmcpServerKitError::Config(
1306 "auth rate_limit.pre_auth_burst must be greater than zero".into(),
1307 ));
1308 }
1309 }
1310 Ok(())
1311 }
1312
1313 fn check_trusted_forwarder(&self) -> Result<(), RmcpServerKitError> {
1318 for entry in &self.trusted_proxies {
1319 validate_trusted_proxy_entry(entry).map_err(RmcpServerKitError::Config)?;
1320 }
1321 if self.forwarded_header.is_some() && self.trusted_proxies.is_empty() {
1322 return Err(RmcpServerKitError::Config(
1323 "forwarded_header requires trusted_proxies to be nonempty".into(),
1324 ));
1325 }
1326 Ok(())
1327 }
1328
1329 fn check_session_binding_config(&self) -> Result<(), RmcpServerKitError> {
1330 if self.session_store.is_some()
1331 && self.session_binding
1332 && self.auth.as_ref().is_some_and(|auth| auth.enabled)
1333 && self.session_binding_secret.is_none()
1334 {
1335 return Err(RmcpServerKitError::Config(
1336 "session_store with session_binding enabled and auth configured requires \
1337 session_binding_secret: a shared secret is required for cross-instance \
1338 session verification"
1339 .into(),
1340 ));
1341 }
1342
1343 if let Some(secret) = &self.session_binding_secret {
1344 crate::session_binding::validate_configured_secret(
1345 "session_binding_secret",
1346 secret.expose_secret(),
1347 )?;
1348 }
1349 Ok(())
1350 }
1351
1352 fn check(&self) -> Result<(), RmcpServerKitError> {
1356 if let Err(violation) = crate::config::check_shared_config_invariants(
1371 self.admin_enabled,
1372 self.auth.as_ref().is_some_and(|a| a.enabled),
1373 self.tls_cert_path.is_some(),
1374 self.tls_key_path.is_some(),
1375 self.auth.as_ref().is_some_and(|a| a.mtls.is_some()),
1376 ) {
1377 return Err(RmcpServerKitError::Config(
1378 match violation {
1379 crate::config::SharedConfigViolation::AdminRequiresAuth => {
1380 "admin_enabled=true requires auth to be configured and enabled"
1381 }
1382 crate::config::SharedConfigViolation::TlsCertWithoutKey => {
1383 "tls_cert_path is set but tls_key_path is missing"
1384 }
1385 crate::config::SharedConfigViolation::TlsKeyWithoutCert => {
1386 "tls_key_path is set but tls_cert_path is missing"
1387 }
1388 crate::config::SharedConfigViolation::MtlsRequiresTls => {
1389 "auth.mtls requires TLS: set both tls_cert_path and tls_key_path \
1390 (mTLS client certificates cannot be verified on a plaintext listener)"
1391 }
1392 }
1393 .into(),
1394 ));
1395 }
1396
1397 if let Some(auth) = &self.auth {
1398 auth.validate_api_key_names()?;
1399 }
1400
1401 if self.bind_addr.parse::<SocketAddr>().is_err() {
1403 return Err(RmcpServerKitError::Config(format!(
1404 "bind_addr {:?} is not a valid socket address (expected e.g. 127.0.0.1:8080)",
1405 self.bind_addr
1406 )));
1407 }
1408
1409 if let Some(ref url) = self.public_url
1411 && !(url.starts_with("http://") || url.starts_with("https://"))
1412 {
1413 return Err(RmcpServerKitError::Config(format!(
1414 "public_url {url:?} must start with http:// or https://"
1415 )));
1416 }
1417
1418 for origin in &self.allowed_origins {
1420 if !(origin.starts_with("http://") || origin.starts_with("https://")) {
1421 return Err(RmcpServerKitError::Config(format!(
1422 "allowed_origins entry {origin:?} must start with http:// or https://"
1423 )));
1424 }
1425 }
1426
1427 if self.max_request_body == 0 {
1429 return Err(RmcpServerKitError::Config(
1430 "max_request_body must be greater than zero".into(),
1431 ));
1432 }
1433
1434 if self.extra_route_rate_limit == Some(0) {
1438 return Err(RmcpServerKitError::Config(
1439 "extra_route_rate_limit must be greater than zero".into(),
1440 ));
1441 }
1442
1443 self.check_burst_knobs()?;
1445
1446 self.check_trusted_forwarder()?;
1448
1449 #[cfg(feature = "oauth")]
1451 if let Some(auth_cfg) = &self.auth
1452 && let Some(oauth_cfg) = &auth_cfg.oauth
1453 {
1454 oauth_cfg.validate()?;
1455 }
1456
1457 self.check_session_binding_config()?;
1458
1459 validate_security_headers(&self.security_headers)?;
1462
1463 if self.max_concurrent_requests == Some(0) {
1467 return Err(RmcpServerKitError::Config(
1468 "max_concurrent_requests must be greater than zero when set".into(),
1469 ));
1470 }
1471
1472 if let Some(auth_cfg) = &self.auth
1476 && let Some(rl) = &auth_cfg.rate_limit
1477 && rl.max_tracked_keys == 0
1478 {
1479 return Err(RmcpServerKitError::Config(
1480 "auth.rate_limit.max_tracked_keys must be greater than zero".into(),
1481 ));
1482 }
1483
1484 check_auth_capacity_knobs(self.auth.as_ref())?;
1485
1486 if self.tls_handshake_timeout == Duration::ZERO {
1491 return Err(RmcpServerKitError::Config(
1492 "tls_handshake_timeout must be greater than zero".into(),
1493 ));
1494 }
1495
1496 if self.max_concurrent_tls_handshakes == 0 {
1501 return Err(RmcpServerKitError::Config(
1502 "max_concurrent_tls_handshakes must be greater than zero".into(),
1503 ));
1504 }
1505
1506 Ok(())
1507 }
1508}
1509
1510#[allow(
1516 missing_debug_implementations,
1517 reason = "contains Arc<AuthState> with non-Debug fields"
1518)]
1519pub struct ReloadHandle {
1520 auth: Option<Arc<AuthState>>,
1521 rbac: Option<Arc<ArcSwap<RbacPolicy>>>,
1522 crl_set: Option<Arc<CrlSet>>,
1523}
1524
1525impl ReloadHandle {
1526 pub fn try_reload_auth_keys(
1543 &self,
1544 keys: Vec<crate::auth::ApiKeyEntry>,
1545 ) -> Result<(), RmcpServerKitError> {
1546 if let Some(ref auth) = self.auth {
1547 auth.try_reload_keys(keys)
1548 } else {
1549 Ok(())
1550 }
1551 }
1552
1553 pub fn reload_auth_keys(&self, keys: Vec<crate::auth::ApiKeyEntry>) {
1562 if let Err(error) = self.try_reload_auth_keys(keys) {
1563 tracing::error!(%error, "API key hot reload rejected: keys left unchanged");
1564 }
1565 }
1566
1567 pub fn reload_rbac(&self, policy: RbacPolicy) {
1569 if let Some(ref rbac) = self.rbac {
1570 rbac.store(Arc::new(policy));
1571 tracing::info!("RBAC policy reloaded");
1572 }
1573 }
1574
1575 pub async fn refresh_crls(&self) -> Result<(), RmcpServerKitError> {
1585 let Some(ref crl_set) = self.crl_set else {
1586 return Err(RmcpServerKitError::Config(
1587 "CRL refresh requested but mTLS CRL support is not configured".into(),
1588 ));
1589 };
1590
1591 crl_set.force_refresh().await
1592 }
1593}
1594
1595#[allow(
1612 clippy::too_many_lines,
1613 clippy::cognitive_complexity,
1614 reason = "middleware layer order is security-critical and must remain visible at one glance; extracting `&mut Router` helpers would obscure the auth/RBAC/origin/rate-limit ordering"
1615)]
1616struct AppRunParams {
1620 tls_paths: Option<(PathBuf, PathBuf)>,
1622 tls_handshake_timeout: Duration,
1624 max_concurrent_tls_handshakes: usize,
1626 mtls_config: Option<MtlsConfig>,
1628 shutdown_timeout: Duration,
1630 auth_state: Option<Arc<AuthState>>,
1632 rbac_swap: Arc<ArcSwap<RbacPolicy>>,
1634 on_reload_ready: Option<Box<dyn FnOnce(ReloadHandle) + Send>>,
1636 ct: CancellationToken,
1640 session_ct: CancellationToken,
1650 scheme: &'static str,
1652 name: String,
1654}
1655
1656#[allow(
1666 clippy::cognitive_complexity,
1667 reason = "router assembly is intrinsically sequential; splitting harms readability"
1668)]
1669#[allow(
1670 deprecated,
1671 reason = "internal router assembly reads deprecated `pub` config fields by design until 1.0 makes them pub(crate)"
1672)]
1673fn build_app_router<H, F>(
1674 mut config: McpServerConfig,
1675 handler_factory: F,
1676) -> anyhow::Result<(axum::Router, AppRunParams)>
1677where
1678 H: ServerHandler + 'static,
1679 F: Fn() -> H + Send + Sync + Clone + 'static,
1680{
1681 let ct = CancellationToken::new();
1682 let session_ct = CancellationToken::new();
1683
1684 let allowed_hosts = derive_allowed_hosts(&config.bind_addr, config.public_url.as_deref());
1685 tracing::info!(allowed_hosts = %allowed_hosts.join(", "), "configured Streamable HTTP allowed hosts");
1686
1687 if config.max_concurrent_requests.is_none() {
1688 tracing::warn!(
1689 "max_concurrent_requests is unset: in-flight HTTP requests are unlimited; \
1690 set McpServerConfig::with_max_concurrent_requests or front the server with \
1691 an external concurrency limit"
1692 );
1693 }
1694
1695 let rbac_swap = Arc::new(ArcSwap::new(
1698 config
1699 .rbac
1700 .clone()
1701 .unwrap_or_else(|| Arc::new(RbacPolicy::disabled())),
1702 ));
1703
1704 let rbac_for_handler = Arc::clone(&rbac_swap);
1705 let tool_list_filtering = config.tool_list_filtering;
1706 let session_store = config.session_store.take();
1707 let mut rmcp_config = StreamableHttpServerConfig::default()
1708 .with_allowed_hosts(allowed_hosts)
1709 .with_sse_keep_alive(Some(config.sse_keep_alive))
1710 .with_cancellation_token(session_ct.clone());
1711 rmcp_config.session_store = session_store;
1712 let event_store = config.event_store.take();
1713 let mcp_service = StreamableHttpService::new(
1714 move || {
1715 Ok(RbacContextHandler::new(
1716 handler_factory(),
1717 Arc::clone(&rbac_for_handler),
1718 tool_list_filtering,
1719 ))
1720 },
1721 {
1722 let mut mgr = LocalSessionManager::default();
1723 mgr.session_config.keep_alive = Some(config.session_idle_timeout);
1724 if let Some(event_store) = event_store {
1725 mgr = mgr.with_event_store(event_store);
1726 }
1727 mgr.into()
1728 },
1729 rmcp_config,
1730 );
1731
1732 let mut mcp_router = axum::Router::new().nest_service("/mcp", mcp_service);
1734
1735 let auth_state: Option<Arc<AuthState>> = match config.auth {
1739 Some(ref auth_config) if auth_config.enabled => {
1740 let rate_limiter = auth_config.rate_limit.as_ref().map(build_rate_limiter);
1741 let pre_auth_limiter = auth_config
1742 .rate_limit
1743 .as_ref()
1744 .map(crate::auth::build_pre_auth_limiter);
1745
1746 #[cfg(feature = "oauth")]
1747 let jwks_cache = auth_config
1748 .oauth
1749 .as_ref()
1750 .map(|c| crate::oauth::JwksCache::new(c).map(Arc::new))
1751 .transpose()
1752 .map_err(|e| std::io::Error::other(format!("JWKS HTTP client: {e}")))?;
1753
1754 Some(Arc::new(AuthState {
1755 api_keys: ArcSwap::new(Arc::new(auth_config.api_keys.clone())),
1756 rate_limiter,
1757 pre_auth_limiter,
1758 #[cfg(feature = "oauth")]
1759 jwks_cache,
1760 seen_identities: crate::auth::SeenIdentitySet::new(),
1761 counters: crate::auth::AuthCounters::default(),
1762 resource_metadata_url: config.public_url.as_ref().map(|url| {
1770 format!(
1771 "{}/.well-known/oauth-protected-resource/mcp",
1772 url.trim_end_matches('/')
1773 )
1774 }),
1775 }))
1776 }
1777 _ => None,
1778 };
1779
1780 if config.admin_enabled {
1783 let Some(ref auth_state_ref) = auth_state else {
1784 return Err(anyhow::anyhow!(
1785 "admin_enabled=true requires auth to be configured and enabled"
1786 ));
1787 };
1788 let admin_state = crate::admin::AdminState {
1789 started_at: std::time::Instant::now(),
1790 name: config.name.clone(),
1791 version: config.version.clone(),
1792 auth: Some(Arc::clone(auth_state_ref)),
1793 rbac: Arc::clone(&rbac_swap),
1794 };
1795 let admin_cfg = crate::admin::AdminConfig {
1796 role: config.admin_role.clone(),
1797 };
1798 mcp_router = mcp_router.merge(crate::admin::admin_router(admin_state, &admin_cfg));
1799 tracing::info!(role = %config.admin_role, "/admin/* endpoints enabled");
1800 }
1801
1802 if config.session_binding {
1834 let secret = match config.session_binding_secret.as_ref() {
1835 Some(configured) => configured_session_binding_secret(configured)?,
1836 None => process_session_binding_secret().clone(),
1837 };
1838 mcp_router = mcp_router.layer(axum::middleware::from_fn(move |req, next| {
1839 let secret = secret.clone();
1840 session_binding_middleware(secret, req, next)
1841 }));
1842 }
1843
1844 {
1848 let tool_limiter: Option<Arc<ToolRateLimiter>> = config.tool_rate_limit.map(|per_minute| {
1849 build_tool_rate_limiter_with_policy(
1850 per_minute,
1851 config.tool_rate_limit_burst,
1852 config.key_eviction_policy,
1853 )
1854 });
1855
1856 if rbac_swap.load().is_enabled() {
1857 tracing::info!("RBAC enforcement enabled on /mcp");
1858 }
1859 if let Some(limit) = config.tool_rate_limit {
1860 tracing::info!(limit, "tool rate limiting enabled (calls/min per IP)");
1861 }
1862
1863 let rbac_for_mw = Arc::clone(&rbac_swap);
1864 mcp_router = mcp_router.layer(axum::middleware::from_fn(move |req, next| {
1865 let p = rbac_for_mw.load_full();
1866 let tl = tool_limiter.clone();
1867 rbac_middleware(p, tl, req, next)
1868 }));
1869 }
1870
1871 if let Some(ref auth_config) = config.auth
1873 && auth_config.enabled
1874 {
1875 let Some(ref state) = auth_state else {
1876 return Err(anyhow::anyhow!("auth state missing despite enabled config"));
1877 };
1878
1879 let methods: Vec<&str> = [
1880 auth_config.mtls.is_some().then_some("mTLS"),
1881 (!auth_config.api_keys.is_empty()).then_some("bearer"),
1882 #[cfg(feature = "oauth")]
1883 auth_config.oauth.is_some().then_some("oauth-jwt"),
1884 ]
1885 .into_iter()
1886 .flatten()
1887 .collect();
1888
1889 tracing::info!(
1890 methods = %methods.join(", "),
1891 api_keys = auth_config.api_keys.len(),
1892 "auth enabled on /mcp"
1893 );
1894
1895 let state_for_mw = Arc::clone(state);
1896 mcp_router = mcp_router.layer(axum::middleware::from_fn(move |req, next| {
1897 let s = Arc::clone(&state_for_mw);
1898 auth_middleware(s, req, next)
1899 }));
1900 }
1901
1902 mcp_router = mcp_router.layer(tower_http::timeout::TimeoutLayer::with_status_code(
1905 axum::http::StatusCode::REQUEST_TIMEOUT,
1906 config.request_timeout,
1907 ));
1908
1909 mcp_router = mcp_router.layer(tower_http::limit::RequestBodyLimitLayer::new(
1913 config.max_request_body,
1914 ));
1915
1916 let mut effective_origins = config.allowed_origins.clone();
1923 if effective_origins.is_empty()
1924 && let Some(ref url) = config.public_url
1925 {
1926 if let Some(scheme_end) = url.find("://") {
1931 let scheme_with_sep = url.get(..scheme_end + 3).unwrap_or_default();
1932 let after_scheme = url.get(scheme_end + 3..).unwrap_or_default();
1933 let host_end = after_scheme.find('/').unwrap_or(after_scheme.len());
1934 let host = after_scheme.get(..host_end).unwrap_or_default();
1935 let origin = format!("{scheme_with_sep}{host}");
1936 tracing::info!(
1937 %origin,
1938 "auto-derived allowed origin from public_url"
1939 );
1940 effective_origins.push(origin);
1941 }
1942 }
1943 let allowed_origins: Arc<[String]> = Arc::from(effective_origins);
1944 let cors_origins = Arc::clone(&allowed_origins);
1945 let log_request_headers = config.log_request_headers;
1946
1947 let readyz_route = if let Some(check) = config.readiness_check.take() {
1948 axum::routing::get(move || readyz(Arc::clone(&check)))
1949 } else {
1950 axum::routing::get(healthz)
1951 };
1952
1953 #[allow(
1954 unused_mut,
1955 reason = "the binding is only reassigned when the `oauth` feature adds the \
1956 protected-resource-metadata route below"
1957 )]
1958 let mut router = axum::Router::new()
1959 .route("/healthz", axum::routing::get(healthz))
1960 .route("/readyz", readyz_route)
1961 .route(
1962 "/version",
1963 axum::routing::get({
1964 let payload_bytes: Arc<[u8]> = serialize_version_payload(
1969 &config.name,
1970 &config.version,
1971 config.expose_build_metadata,
1972 );
1973 move || {
1974 let p = Arc::clone(&payload_bytes);
1975 async move {
1976 (
1977 [(axum::http::header::CONTENT_TYPE, "application/json")],
1978 p.to_vec(),
1979 )
1980 }
1981 }
1982 }),
1983 )
1984 .merge(mcp_router);
1985
1986 if let Some(extra) = config.extra_router.take() {
1993 let extra = match config.extra_route_rate_limit {
1994 Some(per_minute) => {
1995 let max_tracked_keys =
1996 NonZeroUsize::new(EXTRA_ROUTE_MAX_TRACKED_KEYS).unwrap_or(NonZeroUsize::MIN);
1997 let limiter = build_extra_route_rate_limiter_with_policy(
1998 per_minute,
1999 config.extra_route_rate_limit_burst,
2000 config.key_eviction_policy,
2001 max_tracked_keys,
2002 );
2003 let exempt: Arc<std::collections::HashSet<String>> = Arc::new(
2004 config
2005 .extra_route_rate_limit_exempt_paths
2006 .iter()
2007 .cloned()
2008 .collect(),
2009 );
2010 tracing::info!(
2011 per_minute,
2012 exempt_paths = exempt.len(),
2013 "extra-route per-IP rate limit enabled"
2014 );
2015 extra.layer(axum::middleware::from_fn(move |req, next| {
2016 let l = Arc::clone(&limiter);
2017 let e = Arc::clone(&exempt);
2018 extra_route_rate_limit_middleware(l, e, req, next)
2019 }))
2020 }
2021 None => extra,
2022 };
2023 router = router.merge(extra);
2024 }
2025
2026 let server_url = derive_server_url(&config);
2033 let resource_url = format!("{server_url}/mcp");
2034
2035 #[cfg(feature = "oauth")]
2036 let prm_metadata = if let Some(ref auth_config) = config.auth
2037 && let Some(ref oauth_config) = auth_config.oauth
2038 {
2039 crate::oauth::protected_resource_metadata(&resource_url, &server_url, oauth_config)
2040 } else {
2041 serde_json::json!({ "resource": resource_url })
2042 };
2043 #[cfg(not(feature = "oauth"))]
2044 let prm_metadata = serde_json::json!({ "resource": resource_url });
2045
2046 let prm_root = prm_metadata.clone();
2052 router = router.route(
2053 "/.well-known/oauth-protected-resource",
2054 axum::routing::get(move || {
2055 let m = prm_root.clone();
2056 async move { axum::Json(m) }
2057 }),
2058 );
2059 router = router.route(
2060 "/.well-known/oauth-protected-resource/mcp",
2061 axum::routing::get(move || {
2062 let m = prm_metadata.clone();
2063 async move { axum::Json(m) }
2064 }),
2065 );
2066
2067 #[cfg(feature = "oauth")]
2072 if let Some(ref auth_config) = config.auth
2073 && let Some(ref oauth_config) = auth_config.oauth
2074 && oauth_config.proxy.is_some()
2075 {
2076 router = install_oauth_proxy_routes(
2077 router,
2078 &server_url,
2079 oauth_config,
2080 auth_state.as_ref(),
2081 config.max_request_body,
2082 &config.admin_role,
2083 )?;
2084 }
2085
2086 if !cors_origins.is_empty() {
2095 let cors = tower_http::cors::CorsLayer::new()
2096 .allow_origin(
2097 cors_origins
2098 .iter()
2099 .filter_map(|o| o.parse::<axum::http::HeaderValue>().ok())
2100 .collect::<Vec<_>>(),
2101 )
2102 .allow_methods([
2103 axum::http::Method::GET,
2104 axum::http::Method::POST,
2105 axum::http::Method::OPTIONS,
2106 ])
2107 .allow_headers([
2108 axum::http::header::CONTENT_TYPE,
2109 axum::http::header::AUTHORIZATION,
2110 ]);
2111 router = router.layer(cors);
2112 }
2113
2114 if config.compression_enabled {
2118 use tower_http::compression::Predicate as _;
2119 let predicate = tower_http::compression::DefaultPredicate::new().and(
2120 tower_http::compression::predicate::SizeAbove::new(u64::from(
2121 config.compression_min_size,
2122 )),
2123 );
2124 router = router.layer(
2125 tower_http::compression::CompressionLayer::new()
2126 .gzip(true)
2127 .br(true)
2128 .compress_when(predicate),
2129 );
2130 tracing::info!(
2131 min_size = config.compression_min_size,
2132 "response compression enabled (gzip, br)"
2133 );
2134 }
2135
2136 if let Some(max) = config.max_concurrent_requests {
2139 let overload_handler = tower::ServiceBuilder::new()
2140 .layer(axum::error_handling::HandleErrorLayer::new(
2141 |_err: tower::BoxError| async {
2142 (
2143 axum::http::StatusCode::SERVICE_UNAVAILABLE,
2144 axum::Json(serde_json::json!({
2145 "error": "overloaded",
2146 "error_description": "server is at capacity, retry later"
2147 })),
2148 )
2149 },
2150 ))
2151 .layer(tower::load_shed::LoadShedLayer::new())
2152 .layer(tower::limit::ConcurrencyLimitLayer::new(max));
2153 router = router.layer(overload_handler);
2154 tracing::info!(max, "global concurrency limit enabled");
2155 }
2156
2157 router = router.fallback(|| async {
2161 (
2162 axum::http::StatusCode::NOT_FOUND,
2163 axum::Json(serde_json::json!({
2164 "error": "not_found",
2165 "error_description": "The requested endpoint does not exist"
2166 })),
2167 )
2168 });
2169
2170 #[cfg(feature = "metrics")]
2172 if config.metrics_enabled {
2173 let metrics = Arc::new(
2174 crate::metrics::McpMetrics::new().map_err(|e| anyhow::anyhow!("metrics init: {e}"))?,
2175 );
2176 let m = Arc::clone(&metrics);
2177 router = router.layer(axum::middleware::from_fn(
2178 move |req: Request<Body>, next: Next| {
2179 let m = Arc::clone(&m);
2180 metrics_middleware(m, req, next)
2181 },
2182 ));
2183 let metrics_bind = config.metrics_bind.clone();
2184 let metrics_shutdown = ct.clone();
2185 tokio::spawn(async move {
2186 if let Err(e) =
2187 crate::metrics::serve_metrics(metrics_bind, metrics, metrics_shutdown).await
2188 {
2189 tracing::error!("metrics listener failed: {e}");
2190 }
2191 });
2192 }
2193
2194 let forward_resolver: Option<Arc<ForwardResolver>> = if config.trusted_proxies.is_empty() {
2202 None
2203 } else {
2204 Some(Arc::new(ForwardResolver {
2207 trusted: config
2208 .trusted_proxies
2209 .iter()
2210 .filter_map(|entry| parse_proxy_net(entry))
2211 .collect(),
2212 mode: config
2213 .forwarded_header
2214 .unwrap_or(ForwardedHeaderMode::XForwardedFor),
2215 max_scanned_entries: config.trusted_forwarder_max_entries,
2216 }))
2217 };
2218 if forward_resolver.is_some() {
2219 tracing::info!(
2220 proxies = config.trusted_proxies.len(),
2221 "trusted-forwarder mode enabled: limiters key by resolved client IP"
2222 );
2223 }
2224 router = router.layer(axum::middleware::from_fn(move |req, next| {
2225 let r = forward_resolver.clone();
2226 normalize_peer_addr_middleware(r, req, next)
2227 }));
2228
2229 router = router.layer(axum::middleware::from_fn(move |req, next| {
2241 let origins = Arc::clone(&allowed_origins);
2242 origin_check_middleware(origins, log_request_headers, req, next)
2243 }));
2244
2245 let is_tls = config.tls_cert_path.is_some();
2254 warn_security_header_overrides(&config.security_headers);
2255 let security_headers_cfg = Arc::new(config.security_headers.clone());
2256 router = router.layer(axum::middleware::from_fn(move |req, next| {
2257 let cfg = Arc::clone(&security_headers_cfg);
2258 security_headers_middleware(is_tls, cfg, req, next)
2259 }));
2260
2261 let scheme = if config.tls_cert_path.is_some() {
2262 "https"
2263 } else {
2264 "http"
2265 };
2266
2267 let tls_paths = match (&config.tls_cert_path, &config.tls_key_path) {
2268 (Some(cert), Some(key)) => Some((cert.clone(), key.clone())),
2269 _ => None,
2270 };
2271 let tls_handshake_timeout = config.tls_handshake_timeout;
2272 let max_concurrent_tls_handshakes = config.max_concurrent_tls_handshakes;
2273 let mtls_config = config.auth.as_ref().and_then(|a| a.mtls.as_ref()).cloned();
2274
2275 Ok((
2276 router,
2277 AppRunParams {
2278 tls_paths,
2279 tls_handshake_timeout,
2280 max_concurrent_tls_handshakes,
2281 mtls_config,
2282 shutdown_timeout: config.shutdown_timeout,
2283 auth_state,
2284 rbac_swap,
2285 on_reload_ready: config.on_reload_ready.take(),
2286 ct,
2287 session_ct,
2288 scheme,
2289 name: config.name.clone(),
2290 },
2291 ))
2292}
2293
2294struct CancelOnDrop(CancellationToken);
2307
2308impl Drop for CancelOnDrop {
2309 fn drop(&mut self) {
2310 self.0.cancel();
2311 }
2312}
2313
2314fn spawn_external_shutdown_bridge(
2318 external: CancellationToken,
2319 internal: CancellationToken,
2320) -> tokio::task::JoinHandle<()> {
2321 tokio::spawn(async move {
2322 tokio::select! {
2326 () = external.cancelled() => internal.cancel(),
2327 () = internal.cancelled() => {}
2328 }
2329 })
2330}
2331
2332pub async fn serve<H, F>(
2352 config: Validated<McpServerConfig>,
2353 handler_factory: F,
2354) -> Result<(), RmcpServerKitError>
2355where
2356 H: ServerHandler + 'static,
2357 F: Fn() -> H + Send + Sync + Clone + 'static,
2358{
2359 let config = config.into_inner();
2360 #[allow(
2361 deprecated,
2362 reason = "internal serve() reads `bind_addr` to construct the listener; field becomes pub(crate) in 1.0"
2363 )]
2364 let bind_addr = config.bind_addr.clone();
2365 let (router, params) = build_app_router(config, handler_factory).map_err(anyhow_to_startup)?;
2366 let _cancel_guard = CancelOnDrop(params.ct.clone());
2367
2368 let listener = TcpListener::bind(&bind_addr)
2369 .await
2370 .map_err(|e| io_to_startup(&format!("bind {bind_addr}"), e))?;
2371 log_listening(¶ms.name, params.scheme, &bind_addr);
2372
2373 run_server(
2374 router,
2375 listener,
2376 params.tls_paths,
2377 params.tls_handshake_timeout,
2378 params.max_concurrent_tls_handshakes,
2379 params.mtls_config,
2380 params.shutdown_timeout,
2381 params.auth_state,
2382 params.rbac_swap,
2383 params.on_reload_ready,
2384 params.ct,
2385 params.session_ct,
2386 )
2387 .await
2388 .map_err(anyhow_to_startup)
2389}
2390
2391pub async fn serve_with_listener<H, F>(
2424 listener: TcpListener,
2425 config: Validated<McpServerConfig>,
2426 handler_factory: F,
2427 ready_tx: Option<tokio::sync::oneshot::Sender<SocketAddr>>,
2428 shutdown: Option<CancellationToken>,
2429) -> Result<(), RmcpServerKitError>
2430where
2431 H: ServerHandler + 'static,
2432 F: Fn() -> H + Send + Sync + Clone + 'static,
2433{
2434 let config = config.into_inner();
2435 let local_addr = listener
2436 .local_addr()
2437 .map_err(|e| io_to_startup("listener.local_addr", e))?;
2438 let (router, params) = build_app_router(config, handler_factory).map_err(anyhow_to_startup)?;
2439 let _cancel_guard = CancelOnDrop(params.ct.clone());
2440
2441 log_listening(¶ms.name, params.scheme, &local_addr.to_string());
2442
2443 if let Some(external) = shutdown {
2447 let _bridge_task = spawn_external_shutdown_bridge(external, params.ct.clone());
2448 }
2449
2450 if let Some(tx) = ready_tx {
2454 let _ = tx.send(local_addr);
2456 }
2457
2458 run_server(
2459 router,
2460 listener,
2461 params.tls_paths,
2462 params.tls_handshake_timeout,
2463 params.max_concurrent_tls_handshakes,
2464 params.mtls_config,
2465 params.shutdown_timeout,
2466 params.auth_state,
2467 params.rbac_swap,
2468 params.on_reload_ready,
2469 params.ct,
2470 params.session_ct,
2471 )
2472 .await
2473 .map_err(anyhow_to_startup)
2474}
2475
2476#[allow(
2479 clippy::cognitive_complexity,
2480 reason = "tracing::info! macro expansions inflate the score; logic is trivial"
2481)]
2482fn log_listening(name: &str, scheme: &str, addr: &str) {
2483 tracing::info!("{name} listening on {addr}");
2484 tracing::info!(" MCP endpoint: {scheme}://{addr}/mcp");
2485 tracing::info!(" Health check: {scheme}://{addr}/healthz");
2486 tracing::info!(" Readiness: {scheme}://{addr}/readyz");
2487}
2488
2489#[allow(
2512 clippy::too_many_arguments,
2513 clippy::cognitive_complexity,
2514 reason = "server start-up threads TLS, reload state, and graceful shutdown through one flow"
2515)]
2516async fn run_server(
2520 router: axum::Router,
2521 listener: TcpListener,
2522 tls_paths: Option<(PathBuf, PathBuf)>,
2523 tls_handshake_timeout: Duration,
2524 max_concurrent_tls_handshakes: usize,
2525 mtls_config: Option<MtlsConfig>,
2526 shutdown_timeout: Duration,
2527 auth_state: Option<Arc<AuthState>>,
2528 rbac_swap: Arc<ArcSwap<RbacPolicy>>,
2529 mut on_reload_ready: Option<Box<dyn FnOnce(ReloadHandle) + Send>>,
2530 ct: CancellationToken,
2531 session_ct: CancellationToken,
2532) -> anyhow::Result<()> {
2533 let shutdown_trigger = CancellationToken::new();
2537 {
2538 let trigger = shutdown_trigger.clone();
2539 let parent = ct.clone();
2540 tokio::spawn(async move {
2541 tokio::select! {
2544 () = shutdown_signal() => {}
2545 () = parent.cancelled() => {}
2546 }
2547 trigger.cancel();
2548 });
2549 }
2550
2551 let graceful = {
2552 let trigger = shutdown_trigger.clone();
2553 let ct = ct.clone();
2554 async move {
2555 trigger.cancelled().await;
2556 tracing::info!("shutting down (grace period: {shutdown_timeout:?})");
2557 ct.cancel();
2558 }
2559 };
2560
2561 let force_exit_timer = {
2562 let trigger = shutdown_trigger.clone();
2563 async move {
2564 trigger.cancelled().await;
2565 tokio::time::sleep(shutdown_timeout).await;
2566 }
2567 };
2568
2569 if let Some((cert_path, key_path)) = tls_paths {
2570 let crl_set = if let Some(mtls) = mtls_config.as_ref()
2571 && mtls.crl_enabled
2572 {
2573 let (ca_certs, roots) = load_client_auth_roots(&mtls.ca_cert_path)?;
2574 let (crl_set, discover_rx) =
2575 mtls_revocation::bootstrap_fetch(roots, &ca_certs, mtls.clone())
2576 .await
2577 .map_err(|error| anyhow::anyhow!(error.to_string()))?;
2578 tokio::spawn(mtls_revocation::run_crl_refresher(
2579 Arc::clone(&crl_set),
2580 discover_rx,
2581 ct.clone(),
2582 ));
2583 Some(crl_set)
2584 } else {
2585 None
2586 };
2587
2588 if let Some(cb) = on_reload_ready.take() {
2589 cb(ReloadHandle {
2590 auth: auth_state.clone(),
2591 rbac: Some(Arc::clone(&rbac_swap)),
2592 crl_set: crl_set.clone(),
2593 });
2594 }
2595
2596 let tls_listener = TlsListener::new(
2597 listener,
2598 &cert_path,
2599 &key_path,
2600 mtls_config.as_ref(),
2601 crl_set,
2602 tls_handshake_timeout,
2603 max_concurrent_tls_handshakes,
2604 )?;
2605 let make_svc = router.into_make_service_with_connect_info::<TlsConnInfo>();
2606 tokio::select! {
2609 result = axum::serve(tls_listener, make_svc)
2610 .with_graceful_shutdown(graceful) => { session_ct.cancel(); result?; }
2611 () = force_exit_timer => {
2612 tracing::warn!("shutdown timeout exceeded, forcing exit");
2613 session_ct.cancel();
2614 }
2615 }
2616 } else {
2617 if let Some(cb) = on_reload_ready.take() {
2618 cb(ReloadHandle {
2619 auth: auth_state,
2620 rbac: Some(rbac_swap),
2621 crl_set: None,
2622 });
2623 }
2624
2625 let make_svc = router.into_make_service_with_connect_info::<SocketAddr>();
2626 tokio::select! {
2629 result = axum::serve(listener, make_svc)
2630 .with_graceful_shutdown(graceful) => { session_ct.cancel(); result?; }
2631 () = force_exit_timer => {
2632 tracing::warn!("shutdown timeout exceeded, forcing exit");
2633 session_ct.cancel();
2634 }
2635 }
2636 }
2637
2638 Ok(())
2639}
2640
2641#[cfg(feature = "oauth")]
2650fn install_oauth_proxy_routes(
2651 router: axum::Router,
2652 server_url: &str,
2653 oauth_config: &crate::oauth::OAuthConfig,
2654 auth_state: Option<&Arc<AuthState>>,
2655 max_request_body: usize,
2656 admin_role: &str,
2657) -> Result<axum::Router, RmcpServerKitError> {
2658 let Some(ref proxy) = oauth_config.proxy else {
2659 return Ok(router);
2660 };
2661
2662 let http = crate::oauth::OauthHttpClient::with_config(oauth_config)?;
2665
2666 let proxy_router = axum::Router::new();
2672
2673 let asm = crate::oauth::authorization_server_metadata(server_url, oauth_config);
2674 let proxy_router = proxy_router.route(
2675 "/.well-known/oauth-authorization-server",
2676 axum::routing::get(move || {
2677 let m = asm.clone();
2678 async move { axum::Json(m) }
2679 }),
2680 );
2681
2682 let proxy_authorize = proxy.clone();
2683 let proxy_router = proxy_router.route(
2684 "/authorize",
2685 axum::routing::get(
2686 move |axum::extract::RawQuery(query): axum::extract::RawQuery| {
2687 let p = proxy_authorize.clone();
2688 async move { crate::oauth::handle_authorize(&p, &query.unwrap_or_default()) }
2689 },
2690 ),
2691 );
2692
2693 let proxy_token = proxy.clone();
2694 let token_http = http.clone();
2695 let proxy_router = proxy_router.route(
2696 "/token",
2697 axum::routing::post(move |body: String| {
2698 let p = proxy_token.clone();
2699 let h = token_http.clone();
2700 async move { crate::oauth::handle_token(&h, &p, &body).await }
2701 })
2702 .layer(axum::middleware::from_fn(
2703 oauth_token_cache_headers_middleware,
2704 )),
2705 );
2706
2707 let proxy_register = proxy.clone();
2708 let proxy_router = proxy_router.route(
2709 "/register",
2710 axum::routing::post(move |axum::Json(body): axum::Json<serde_json::Value>| {
2711 let p = proxy_register;
2712 async move { axum::Json(crate::oauth::handle_register(&p, &body)) }
2713 })
2714 .layer(axum::middleware::from_fn(
2715 oauth_token_cache_headers_middleware,
2716 )),
2717 );
2718
2719 let admin_routes_enabled = proxy.expose_admin_endpoints
2720 && (proxy.introspection_url.is_some() || proxy.revocation_url.is_some());
2721 if proxy.expose_admin_endpoints
2722 && !proxy.require_auth_on_admin_endpoints
2723 && proxy.allow_unauthenticated_admin_endpoints
2724 {
2725 tracing::warn!(
2729 "OAuth introspect/revoke endpoints are unauthenticated by explicit \
2730 allow_unauthenticated_admin_endpoints opt-out; ensure an \
2731 authenticated reverse proxy fronts these routes"
2732 );
2733 }
2734
2735 let admin_router = if admin_routes_enabled {
2736 build_oauth_admin_router(proxy, http, auth_state, admin_role)?
2737 } else {
2738 axum::Router::new()
2739 };
2740
2741 let proxy_router =
2745 proxy_router
2746 .merge(admin_router)
2747 .layer(tower_http::limit::RequestBodyLimitLayer::new(
2748 max_request_body,
2749 ));
2750
2751 let router = router.merge(proxy_router);
2752
2753 tracing::info!(
2754 introspect = proxy.expose_admin_endpoints && proxy.introspection_url.is_some(),
2755 revoke = proxy.expose_admin_endpoints && proxy.revocation_url.is_some(),
2756 max_request_body,
2757 "OAuth 2.1 proxy endpoints enabled (/authorize, /token, /register)"
2758 );
2759 Ok(router)
2760}
2761
2762#[cfg(feature = "oauth")]
2768fn build_oauth_admin_router(
2769 proxy: &crate::oauth::OAuthProxyConfig,
2770 http: crate::oauth::OauthHttpClient,
2771 auth_state: Option<&Arc<AuthState>>,
2772 admin_role: &str,
2773) -> Result<axum::Router, RmcpServerKitError> {
2774 let mut admin_router = axum::Router::new();
2775 if proxy.introspection_url.is_some() {
2776 let proxy_introspect = proxy.clone();
2777 let introspect_http = http.clone();
2778 admin_router = admin_router.route(
2779 "/introspect",
2780 axum::routing::post(move |body: String| {
2781 let p = proxy_introspect.clone();
2782 let h = introspect_http.clone();
2783 async move { crate::oauth::handle_introspect(&h, &p, &body).await }
2784 }),
2785 );
2786 }
2787 if proxy.revocation_url.is_some() {
2788 let proxy_revoke = proxy.clone();
2789 let revoke_http = http;
2790 admin_router = admin_router.route(
2791 "/revoke",
2792 axum::routing::post(move |body: String| {
2793 let p = proxy_revoke.clone();
2794 let h = revoke_http.clone();
2795 async move { crate::oauth::handle_revoke(&h, &p, &body).await }
2796 }),
2797 );
2798 }
2799
2800 let admin_router = admin_router.layer(axum::middleware::from_fn(
2801 oauth_token_cache_headers_middleware,
2802 ));
2803
2804 if proxy.require_auth_on_admin_endpoints {
2805 let Some(state) = auth_state else {
2806 return Err(RmcpServerKitError::Startup(
2807 "oauth proxy admin endpoints require auth state".into(),
2808 ));
2809 };
2810 let state_for_mw = Arc::clone(state);
2811 let required_role: Arc<str> = Arc::from(admin_role);
2812 Ok(admin_router
2818 .layer(axum::middleware::from_fn(move |req, next| {
2819 let r = Arc::clone(&required_role);
2820 crate::admin::require_admin_role(r, req, next)
2821 }))
2822 .layer(axum::middleware::from_fn(move |req, next| {
2823 let s = Arc::clone(&state_for_mw);
2824 auth_middleware(s, req, next)
2825 })))
2826 } else {
2827 Ok(admin_router)
2828 }
2829}
2830
2831#[allow(
2838 deprecated,
2839 reason = "internal metadata assembly reads deprecated `pub` config fields by design until 1.0 makes them pub(crate)"
2840)]
2841fn derive_server_url(config: &McpServerConfig) -> String {
2842 config.public_url.as_ref().map_or_else(
2843 || {
2844 let scheme = if config.tls_cert_path.is_some() {
2845 "https"
2846 } else {
2847 "http"
2848 };
2849 format!("{scheme}://{}", config.bind_addr)
2850 },
2851 |url| url.trim_end_matches('/').to_owned(),
2852 )
2853}
2854
2855fn derive_allowed_hosts(bind_addr: &str, public_url: Option<&str>) -> Vec<String> {
2860 let mut hosts = vec![
2861 "localhost".to_owned(),
2862 "127.0.0.1".to_owned(),
2863 "::1".to_owned(),
2864 ];
2865
2866 if let Some(url) = public_url
2867 && let Ok(uri) = url.parse::<axum::http::Uri>()
2868 && let Some(authority) = uri.authority()
2869 {
2870 let host = authority.host().to_owned();
2871 if !hosts.iter().any(|h| h == &host) {
2872 hosts.push(host);
2873 }
2874
2875 let authority = authority.as_str().to_owned();
2876 if !hosts.iter().any(|h| h == &authority) {
2877 hosts.push(authority);
2878 }
2879 }
2880
2881 if let Ok(uri) = format!("http://{bind_addr}").parse::<axum::http::Uri>()
2882 && let Some(authority) = uri.authority()
2883 {
2884 let host = authority.host().to_owned();
2885 if !hosts.iter().any(|h| h == &host) {
2886 hosts.push(host);
2887 }
2888
2889 let authority = authority.as_str().to_owned();
2890 if !hosts.iter().any(|h| h == &authority) {
2891 hosts.push(authority);
2892 }
2893 }
2894
2895 hosts
2896}
2897
2898impl axum::extract::connect_info::Connected<axum::serve::IncomingStream<'_, TlsListener>>
2911 for TlsConnInfo
2912{
2913 fn connect_info(target: axum::serve::IncomingStream<'_, TlsListener>) -> Self {
2914 let addr = *target.remote_addr();
2915 let identity = target.io().identity().cloned();
2916 Self::new(addr, identity)
2917 }
2918}
2919
2920const DEFAULT_TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
2927
2928const DEFAULT_MAX_CONCURRENT_TLS_HANDSHAKES: usize = 256;
2936
2937const TLS_ACCEPT_CHANNEL_CAPACITY: usize = 32;
2942
2943struct TlsListener {
2959 local_addr: SocketAddr,
2962 rx: mpsc::Receiver<(AuthenticatedTlsStream, SocketAddr)>,
2964 acceptor_task: tokio::task::JoinHandle<()>,
2967}
2968
2969impl TlsListener {
2970 fn new(
2971 inner: TcpListener,
2972 cert_path: &Path,
2973 key_path: &Path,
2974 mtls_config: Option<&MtlsConfig>,
2975 crl_set: Option<Arc<CrlSet>>,
2976 handshake_timeout: Duration,
2977 max_concurrent_handshakes: usize,
2978 ) -> anyhow::Result<Self> {
2979 rustls::crypto::ring::default_provider()
2981 .install_default()
2982 .ok();
2983
2984 let certs = load_certs(cert_path)?;
2985 let key = load_key(key_path)?;
2986
2987 let mtls_default_role;
2988
2989 let tls_config = if let Some(mtls) = mtls_config {
2990 mtls_default_role = mtls.default_role.clone();
2991 let verifier: Arc<dyn rustls::server::danger::ClientCertVerifier> = if mtls.crl_enabled
2992 {
2993 let Some(crl_set) = crl_set else {
2994 return Err(anyhow::anyhow!(
2995 "mTLS CRL verifier requested but CRL state was not initialized"
2996 ));
2997 };
2998 Arc::new(DynamicClientCertVerifier::new(crl_set))
2999 } else {
3000 let (_, root_store) = load_client_auth_roots(&mtls.ca_cert_path)?;
3001 if mtls.required {
3002 rustls::server::WebPkiClientVerifier::builder(root_store)
3003 .build()
3004 .map_err(|e| anyhow::anyhow!("mTLS verifier error: {e}"))?
3005 } else {
3006 rustls::server::WebPkiClientVerifier::builder(root_store)
3007 .allow_unauthenticated()
3008 .build()
3009 .map_err(|e| anyhow::anyhow!("mTLS verifier error: {e}"))?
3010 }
3011 };
3012
3013 tracing::info!(
3014 ca = %mtls.ca_cert_path.display(),
3015 required = mtls.required,
3016 crl_enabled = mtls.crl_enabled,
3017 "mTLS client auth configured"
3018 );
3019
3020 rustls::ServerConfig::builder_with_protocol_versions(&[
3021 &rustls::version::TLS12,
3022 &rustls::version::TLS13,
3023 ])
3024 .with_client_cert_verifier(verifier)
3025 .with_single_cert(certs, key)?
3026 } else {
3027 mtls_default_role = "viewer".to_owned();
3028 rustls::ServerConfig::builder_with_protocol_versions(&[
3029 &rustls::version::TLS12,
3030 &rustls::version::TLS13,
3031 ])
3032 .with_no_client_auth()
3033 .with_single_cert(certs, key)?
3034 };
3035
3036 let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(tls_config));
3037 tracing::info!(
3038 "TLS enabled (cert: {}, key: {})",
3039 cert_path.display(),
3040 key_path.display()
3041 );
3042 let local_addr = inner.local_addr()?;
3043 let (tx, rx) = mpsc::channel(TLS_ACCEPT_CHANNEL_CAPACITY);
3044 let acceptor_task = tokio::spawn(run_tls_acceptor(
3045 inner,
3046 acceptor,
3047 mtls_default_role,
3048 tx,
3049 handshake_timeout,
3050 max_concurrent_handshakes,
3051 ));
3052 Ok(Self {
3053 local_addr,
3054 rx,
3055 acceptor_task,
3056 })
3057 }
3058
3059 fn extract_handshake_identity(
3063 tls_stream: &tokio_rustls::server::TlsStream<tokio::net::TcpStream>,
3064 default_role: &str,
3065 addr: SocketAddr,
3066 ) -> Option<AuthIdentity> {
3067 let (_, server_conn) = tls_stream.get_ref();
3068 let cert_der = server_conn.peer_certificates()?.first()?;
3069 let id = extract_mtls_identity(cert_der.as_ref(), default_role)?;
3070 tracing::debug!(name = %id.name, peer = %addr, "mTLS client cert accepted");
3071 Some(id)
3072 }
3073}
3074
3075async fn run_tls_acceptor(
3086 listener: TcpListener,
3087 acceptor: tokio_rustls::TlsAcceptor,
3088 default_role: String,
3089 tx: mpsc::Sender<(AuthenticatedTlsStream, SocketAddr)>,
3090 handshake_timeout: Duration,
3091 max_concurrent_handshakes: usize,
3092) {
3093 let inflight = Arc::new(Semaphore::new(max_concurrent_handshakes));
3094 loop {
3095 let Ok(permit) = Arc::clone(&inflight).acquire_owned().await else {
3099 return;
3101 };
3102 let (stream, addr) = match listener.accept().await {
3103 Ok(pair) => pair,
3104 Err(e) => {
3105 tracing::debug!("TCP accept error: {e}");
3106 continue;
3107 }
3108 };
3109 if tx.is_closed() {
3110 return;
3112 }
3113 let acceptor = acceptor.clone();
3114 let default_role = default_role.clone();
3115 let tx = tx.clone();
3116 tokio::spawn(async move {
3117 let _permit = permit;
3118 match tokio::time::timeout(handshake_timeout, acceptor.accept(stream)).await {
3119 Ok(Ok(tls_stream)) => {
3120 let identity =
3121 TlsListener::extract_handshake_identity(&tls_stream, &default_role, addr);
3122 let wrapped = AuthenticatedTlsStream {
3123 inner: tls_stream,
3124 identity,
3125 };
3126 let _ = tx.send((wrapped, addr)).await;
3129 }
3130 Ok(Err(e)) => {
3131 tracing::debug!("TLS handshake failed from {addr}: {e}");
3132 }
3133 Err(_elapsed) => {
3134 tracing::debug!(
3135 "TLS handshake timed out from {addr} after {handshake_timeout:?}"
3136 );
3137 }
3138 }
3139 });
3140 }
3141}
3142
3143pub(crate) struct AuthenticatedTlsStream {
3155 inner: tokio_rustls::server::TlsStream<tokio::net::TcpStream>,
3156 identity: Option<AuthIdentity>,
3157}
3158
3159impl AuthenticatedTlsStream {
3160 #[must_use]
3162 pub(crate) const fn identity(&self) -> Option<&AuthIdentity> {
3163 self.identity.as_ref()
3164 }
3165}
3166
3167impl std::fmt::Debug for AuthenticatedTlsStream {
3168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3169 f.debug_struct("AuthenticatedTlsStream")
3170 .field("identity", &self.identity.as_ref().map(|id| &id.name))
3171 .finish_non_exhaustive()
3172 }
3173}
3174
3175impl tokio::io::AsyncRead for AuthenticatedTlsStream {
3176 fn poll_read(
3177 mut self: Pin<&mut Self>,
3178 cx: &mut std::task::Context<'_>,
3179 buf: &mut tokio::io::ReadBuf<'_>,
3180 ) -> std::task::Poll<std::io::Result<()>> {
3181 Pin::new(&mut self.inner).poll_read(cx, buf)
3182 }
3183}
3184
3185impl tokio::io::AsyncWrite for AuthenticatedTlsStream {
3186 fn poll_write(
3187 mut self: Pin<&mut Self>,
3188 cx: &mut std::task::Context<'_>,
3189 buf: &[u8],
3190 ) -> std::task::Poll<std::io::Result<usize>> {
3191 Pin::new(&mut self.inner).poll_write(cx, buf)
3192 }
3193
3194 fn poll_flush(
3195 mut self: Pin<&mut Self>,
3196 cx: &mut std::task::Context<'_>,
3197 ) -> std::task::Poll<std::io::Result<()>> {
3198 Pin::new(&mut self.inner).poll_flush(cx)
3199 }
3200
3201 fn poll_shutdown(
3202 mut self: Pin<&mut Self>,
3203 cx: &mut std::task::Context<'_>,
3204 ) -> std::task::Poll<std::io::Result<()>> {
3205 Pin::new(&mut self.inner).poll_shutdown(cx)
3206 }
3207
3208 fn poll_write_vectored(
3209 mut self: Pin<&mut Self>,
3210 cx: &mut std::task::Context<'_>,
3211 bufs: &[std::io::IoSlice<'_>],
3212 ) -> std::task::Poll<std::io::Result<usize>> {
3213 Pin::new(&mut self.inner).poll_write_vectored(cx, bufs)
3214 }
3215
3216 fn is_write_vectored(&self) -> bool {
3217 self.inner.is_write_vectored()
3218 }
3219}
3220
3221impl axum::serve::Listener for TlsListener {
3222 type Io = AuthenticatedTlsStream;
3223 type Addr = SocketAddr;
3224
3225 async fn accept(&mut self) -> (Self::Io, Self::Addr) {
3231 if let Some(pair) = self.rx.recv().await {
3232 return pair;
3233 }
3234 tracing::error!("TLS acceptor task terminated; no further connections will be accepted");
3240 std::future::pending().await
3241 }
3242
3243 fn local_addr(&self) -> std::io::Result<Self::Addr> {
3244 Ok(self.local_addr)
3245 }
3246}
3247
3248impl Drop for TlsListener {
3249 fn drop(&mut self) {
3250 self.acceptor_task.abort();
3253 }
3254}
3255
3256fn load_certs(path: &Path) -> anyhow::Result<Vec<rustls::pki_types::CertificateDer<'static>>> {
3257 use rustls::pki_types::pem::PemObject;
3258 let certs: Vec<_> = rustls::pki_types::CertificateDer::pem_file_iter(path)
3259 .map_err(|e| anyhow::anyhow!("failed to read certs from {}: {e}", path.display()))?
3260 .collect::<Result<_, _>>()
3261 .map_err(|e| anyhow::anyhow!("invalid cert in {}: {e}", path.display()))?;
3262 anyhow::ensure!(
3263 !certs.is_empty(),
3264 "no certificates found in {}",
3265 path.display()
3266 );
3267 Ok(certs)
3268}
3269
3270fn load_client_auth_roots(
3271 path: &Path,
3272) -> anyhow::Result<(
3273 Vec<rustls::pki_types::CertificateDer<'static>>,
3274 Arc<RootCertStore>,
3275)> {
3276 let ca_certs = load_certs(path)?;
3277 let mut root_store = RootCertStore::empty();
3278 for cert in &ca_certs {
3279 root_store
3280 .add(cert.clone())
3281 .map_err(|error| anyhow::anyhow!("invalid CA cert: {error}"))?;
3282 }
3283
3284 Ok((ca_certs, Arc::new(root_store)))
3285}
3286
3287fn load_key(path: &Path) -> anyhow::Result<rustls::pki_types::PrivateKeyDer<'static>> {
3288 use rustls::pki_types::pem::PemObject;
3289 rustls::pki_types::PrivateKeyDer::from_pem_file(path)
3290 .map_err(|e| anyhow::anyhow!("failed to read key from {}: {e}", path.display()))
3291}
3292
3293#[allow(
3295 clippy::unused_async,
3296 reason = "axum route handler signature requires `async fn` even when the body is synchronous"
3297)]
3298async fn healthz() -> impl IntoResponse {
3299 axum::Json(serde_json::json!({
3300 "status": "ok",
3301 }))
3302}
3303
3304fn version_payload(name: &str, version: &str, expose_build_metadata: bool) -> serde_json::Value {
3314 let mut map = serde_json::Map::new();
3315 map.insert("name".into(), name.into());
3316 map.insert("version".into(), version.into());
3317 map.insert(
3318 "rmcp_server_kit_version".into(),
3319 env!("CARGO_PKG_VERSION").into(),
3320 );
3321 if expose_build_metadata {
3322 map.insert(
3323 "build_git_sha".into(),
3324 option_env!("RMCP_SERVER_KIT_BUILD_SHA")
3325 .unwrap_or("unknown")
3326 .into(),
3327 );
3328 map.insert(
3329 "build_timestamp".into(),
3330 option_env!("RMCP_SERVER_KIT_BUILD_TIME")
3331 .unwrap_or("unknown")
3332 .into(),
3333 );
3334 map.insert(
3335 "rust_version".into(),
3336 option_env!("RMCP_SERVER_KIT_RUSTC_VERSION")
3337 .unwrap_or("unknown")
3338 .into(),
3339 );
3340 }
3341 serde_json::Value::Object(map)
3342}
3343
3344fn serialize_version_payload(name: &str, version: &str, expose_build_metadata: bool) -> Arc<[u8]> {
3354 let value = version_payload(name, version, expose_build_metadata);
3355 serde_json::to_vec(&value).map_or_else(|_| Arc::from(&b"{}"[..]), Arc::from)
3356}
3357
3358async fn readyz(check: ReadinessCheck) -> impl IntoResponse {
3363 let status = check().await;
3364 let ready = status
3365 .get("ready")
3366 .and_then(serde_json::Value::as_bool)
3367 .unwrap_or(false);
3368 let code = if ready {
3369 axum::http::StatusCode::OK
3370 } else {
3371 axum::http::StatusCode::SERVICE_UNAVAILABLE
3372 };
3373 (code, axum::Json(status))
3374}
3375
3376async fn shutdown_signal() {
3380 let ctrl_c = tokio::signal::ctrl_c();
3381
3382 #[cfg(unix)]
3383 {
3384 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
3385 Ok(mut term) => {
3386 tokio::select! {
3389 _ = ctrl_c => {}
3390 _ = term.recv() => {}
3391 }
3392 }
3393 Err(e) => {
3394 tracing::warn!(error = %e, "failed to register SIGTERM handler, using SIGINT only");
3395 ctrl_c.await.ok();
3396 }
3397 }
3398 }
3399
3400 #[cfg(not(unix))]
3401 {
3402 ctrl_c.await.ok();
3403 }
3404}
3405
3406#[cfg(feature = "metrics")]
3423fn metrics_labels(req: &Request<Body>) -> (&'static str, String) {
3424 let method = match *req.method() {
3425 axum::http::Method::GET => "GET",
3426 axum::http::Method::POST => "POST",
3427 axum::http::Method::PUT => "PUT",
3428 axum::http::Method::PATCH => "PATCH",
3429 axum::http::Method::DELETE => "DELETE",
3430 axum::http::Method::HEAD => "HEAD",
3431 axum::http::Method::OPTIONS => "OPTIONS",
3432 axum::http::Method::TRACE => "TRACE",
3433 axum::http::Method::CONNECT => "CONNECT",
3434 _ => "OTHER",
3437 };
3438
3439 let path = req
3440 .extensions()
3441 .get::<axum::extract::MatchedPath>()
3442 .map_or_else(
3443 || {
3444 let raw = req.uri().path();
3445 if raw == "/mcp" || raw.starts_with("/mcp/") {
3446 "/mcp".to_owned()
3447 } else {
3448 "<unmatched>".to_owned()
3449 }
3450 },
3451 |matched| matched.as_str().to_owned(),
3452 );
3453
3454 (method, path)
3455}
3456
3457#[cfg(feature = "metrics")]
3467async fn metrics_middleware(
3468 metrics: Arc<crate::metrics::McpMetrics>,
3469 mut req: Request<Body>,
3470 next: Next,
3471) -> axum::response::Response {
3472 let (method, path) = metrics_labels(&req);
3473 let start = std::time::Instant::now();
3474
3475 req.extensions_mut().insert(Arc::clone(&metrics));
3476 let response = next.run(req).await;
3477
3478 let mut status_buf = core::fmt::NumBuffer::<u16>::new();
3479 let status = response.status().as_u16().format_into(&mut status_buf);
3480 let duration = start.elapsed().as_secs_f64();
3481
3482 metrics
3483 .http_requests_total
3484 .with_label_values(&[method, &path, status])
3485 .inc();
3486 metrics
3487 .http_request_duration_seconds
3488 .with_label_values(&[method, &path])
3489 .observe(duration);
3490
3491 response
3492}
3493
3494async fn security_headers_middleware(
3508 is_tls: bool,
3509 cfg: Arc<SecurityHeadersConfig>,
3510 req: Request<Body>,
3511 next: Next,
3512) -> axum::response::Response {
3513 use axum::http::{HeaderName, header};
3514
3515 let mut resp = next.run(req).await;
3516 let headers = resp.headers_mut();
3517
3518 headers.remove(header::SERVER);
3520 headers.remove(HeaderName::from_static("x-powered-by"));
3521
3522 apply_security_header(
3523 headers,
3524 header::X_CONTENT_TYPE_OPTIONS,
3525 cfg.x_content_type_options.as_deref(),
3526 "nosniff",
3527 );
3528 apply_security_header(
3529 headers,
3530 header::X_FRAME_OPTIONS,
3531 cfg.x_frame_options.as_deref(),
3532 "deny",
3533 );
3534 apply_security_header(
3535 headers,
3536 header::CACHE_CONTROL,
3537 cfg.cache_control.as_deref(),
3538 "no-store, max-age=0",
3539 );
3540 apply_security_header(
3541 headers,
3542 header::REFERRER_POLICY,
3543 cfg.referrer_policy.as_deref(),
3544 "no-referrer",
3545 );
3546 apply_security_header(
3547 headers,
3548 HeaderName::from_static("cross-origin-opener-policy"),
3549 cfg.cross_origin_opener_policy.as_deref(),
3550 "same-origin",
3551 );
3552 apply_security_header(
3553 headers,
3554 HeaderName::from_static("cross-origin-resource-policy"),
3555 cfg.cross_origin_resource_policy.as_deref(),
3556 "same-origin",
3557 );
3558 apply_security_header(
3559 headers,
3560 HeaderName::from_static("cross-origin-embedder-policy"),
3561 cfg.cross_origin_embedder_policy.as_deref(),
3562 "require-corp",
3563 );
3564 apply_security_header(
3565 headers,
3566 HeaderName::from_static("permissions-policy"),
3567 cfg.permissions_policy.as_deref(),
3568 "accelerometer=(), camera=(), geolocation=(), microphone=()",
3569 );
3570 apply_security_header(
3571 headers,
3572 HeaderName::from_static("x-permitted-cross-domain-policies"),
3573 cfg.x_permitted_cross_domain_policies.as_deref(),
3574 "none",
3575 );
3576 apply_security_header(
3577 headers,
3578 HeaderName::from_static("content-security-policy"),
3579 cfg.content_security_policy.as_deref(),
3580 "default-src 'none'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests",
3581 );
3582 apply_security_header(
3583 headers,
3584 HeaderName::from_static("x-dns-prefetch-control"),
3585 cfg.x_dns_prefetch_control.as_deref(),
3586 "off",
3587 );
3588
3589 if is_tls {
3590 apply_security_header(
3591 headers,
3592 header::STRICT_TRANSPORT_SECURITY,
3593 cfg.strict_transport_security.as_deref(),
3594 "max-age=63072000; includeSubDomains",
3595 );
3596 }
3597
3598 resp
3599}
3600
3601fn apply_security_header(
3612 headers: &mut axum::http::HeaderMap,
3613 name: axum::http::HeaderName,
3614 override_value: Option<&str>,
3615 default: &'static str,
3616) {
3617 use axum::http::HeaderValue;
3618
3619 match override_value {
3620 None => {
3621 headers.insert(name, HeaderValue::from_static(default));
3622 }
3623 Some("") => {
3624 }
3626 Some(v) => match HeaderValue::from_str(v) {
3627 Ok(hv) => {
3628 headers.insert(name, hv);
3629 }
3630 Err(err) => {
3631 tracing::error!(
3632 header = %name,
3633 error = %err,
3634 "invalid security header override reached middleware; using default"
3635 );
3636 headers.insert(name, HeaderValue::from_static(default));
3637 }
3638 },
3639 }
3640}
3641
3642fn validate_security_headers(cfg: &SecurityHeadersConfig) -> Result<(), RmcpServerKitError> {
3653 use axum::http::HeaderValue;
3654
3655 let fields: &[(&str, Option<&str>)] = &[
3656 (
3657 "x_content_type_options",
3658 cfg.x_content_type_options.as_deref(),
3659 ),
3660 ("x_frame_options", cfg.x_frame_options.as_deref()),
3661 ("cache_control", cfg.cache_control.as_deref()),
3662 ("referrer_policy", cfg.referrer_policy.as_deref()),
3663 (
3664 "cross_origin_opener_policy",
3665 cfg.cross_origin_opener_policy.as_deref(),
3666 ),
3667 (
3668 "cross_origin_resource_policy",
3669 cfg.cross_origin_resource_policy.as_deref(),
3670 ),
3671 (
3672 "cross_origin_embedder_policy",
3673 cfg.cross_origin_embedder_policy.as_deref(),
3674 ),
3675 ("permissions_policy", cfg.permissions_policy.as_deref()),
3676 (
3677 "x_permitted_cross_domain_policies",
3678 cfg.x_permitted_cross_domain_policies.as_deref(),
3679 ),
3680 (
3681 "content_security_policy",
3682 cfg.content_security_policy.as_deref(),
3683 ),
3684 (
3685 "x_dns_prefetch_control",
3686 cfg.x_dns_prefetch_control.as_deref(),
3687 ),
3688 (
3689 "strict_transport_security",
3690 cfg.strict_transport_security.as_deref(),
3691 ),
3692 ];
3693
3694 for (field, value) in fields {
3695 let Some(v) = value else { continue };
3696 if v.is_empty() {
3697 continue;
3698 }
3699 if let Err(err) = HeaderValue::from_str(v) {
3700 return Err(RmcpServerKitError::Config(format!(
3701 "invalid security_headers.{field}: {err}"
3702 )));
3703 }
3704 }
3705
3706 if let Some(v) = cfg.strict_transport_security.as_deref()
3707 && !v.is_empty()
3708 && v.to_ascii_lowercase().contains("preload")
3709 {
3710 return Err(RmcpServerKitError::Config(format!(
3711 "invalid security_headers.strict_transport_security: {v:?} contains the `preload` directive; \
3712 HSTS preload must be opted into explicitly via a dedicated builder, not via this knob"
3713 )));
3714 }
3715
3716 Ok(())
3717}
3718
3719#[cfg(feature = "oauth")]
3734async fn oauth_token_cache_headers_middleware(
3735 req: Request<Body>,
3736 next: Next,
3737) -> axum::response::Response {
3738 use axum::http::{HeaderValue, header};
3739
3740 let mut resp = next.run(req).await;
3741 let headers = resp.headers_mut();
3742 headers.insert(header::PRAGMA, HeaderValue::from_static("no-cache"));
3743 headers.append(header::VARY, HeaderValue::from_static("Authorization"));
3744 resp
3745}
3746
3747async fn normalize_peer_addr_middleware(
3778 resolver: Option<Arc<ForwardResolver>>,
3779 mut req: Request<Body>,
3780 next: Next,
3781) -> axum::response::Response {
3782 let direct = req
3783 .extensions()
3784 .get::<ConnectInfo<SocketAddr>>()
3785 .map(|ci| ci.0);
3786 let from_tls = req
3787 .extensions()
3788 .get::<ConnectInfo<TlsConnInfo>>()
3789 .map(|ci| ci.0.addr);
3790 if let Some(addr) = direct.or(from_tls) {
3791 if direct.is_none() {
3792 req.extensions_mut().insert(ConnectInfo(addr));
3793 }
3794 req.extensions_mut().insert(PeerAddr::new(addr));
3795 let client_ip = match &resolver {
3796 Some(r) => crate::forwarded::resolve_client_ip(
3797 addr.ip(),
3798 req.headers(),
3799 &r.trusted,
3800 r.mode,
3801 r.max_scanned_entries,
3802 )
3803 .unwrap_or_else(|reason| {
3804 tracing::debug!(
3805 reason = ?reason,
3806 "forwarded-header resolution fell back to direct peer"
3807 );
3808 addr.ip()
3809 }),
3810 None => addr.ip(),
3811 };
3812 req.extensions_mut().insert(ClientIp::new(client_ip));
3813 }
3814 next.run(req).await
3815}
3816
3817fn parse_proxy_net(entry: &str) -> Option<ipnet::IpNet> {
3820 if let Ok(net) = entry.parse::<ipnet::IpNet>() {
3821 return Some(net);
3822 }
3823 entry.parse::<IpAddr>().ok().map(ipnet::IpNet::from)
3824}
3825
3826pub(crate) fn validate_trusted_proxy_entry(entry: &str) -> Result<(), String> {
3836 match parse_proxy_net(entry) {
3837 None => Err(format!(
3838 "trusted_proxies entry {entry:?} is neither a CIDR nor an IP address"
3839 )),
3840 Some(net) if net.prefix_len() == 0 => Err(format!(
3841 "trusted_proxies entry {entry:?}: prefix length 0 is forbidden (marks every peer trusted, enabling client-IP spoofing)"
3842 )),
3843 Some(_) => Ok(()),
3844 }
3845}
3846
3847pub(crate) fn limiter_client_ip(extensions: &axum::http::Extensions) -> Option<IpAddr> {
3851 if let Some(client) = extensions.get::<ClientIp>() {
3852 return Some(client.ip);
3853 }
3854 extensions
3855 .get::<ConnectInfo<SocketAddr>>()
3856 .map(|ci| ci.0.ip())
3857 .or_else(|| {
3858 extensions
3859 .get::<ConnectInfo<TlsConnInfo>>()
3860 .map(|ci| ci.0.addr.ip())
3861 })
3862}
3863
3864#[derive(Clone, PartialEq, Eq, Hash, Debug)]
3877pub(crate) enum RateLimitKey {
3878 Ip(IpAddr),
3880 Unattributed,
3882}
3883
3884impl std::fmt::Display for RateLimitKey {
3885 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3886 match self {
3887 Self::Ip(ip) => write!(f, "{ip}"),
3888 Self::Unattributed => f.write_str("unattributed"),
3889 }
3890 }
3891}
3892
3893static UNATTRIBUTED_WARNED: std::sync::atomic::AtomicBool =
3895 std::sync::atomic::AtomicBool::new(false);
3896
3897pub(crate) fn limiter_client_key(extensions: &axum::http::Extensions) -> RateLimitKey {
3908 if let Some(ip) = limiter_client_ip(extensions) {
3909 return RateLimitKey::Ip(ip);
3910 }
3911 if !UNATTRIBUTED_WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
3912 tracing::warn!(
3913 "request carries no resolvable client address; rate limiting is \
3914 falling back to a single shared bucket. This indicates \
3915 rmcp-server-kit middleware composed outside serve()."
3916 );
3917 }
3918 RateLimitKey::Unattributed
3919}
3920
3921pub(crate) type ExtraRouteRateLimiter = BoundedKeyedLimiter<RateLimitKey>;
3925
3926const EXTRA_ROUTE_MAX_TRACKED_KEYS: usize = 10_000;
3932
3933const EXTRA_ROUTE_IDLE_EVICTION: Duration = Duration::from_mins(15);
3936
3937fn build_extra_route_rate_limiter_with_policy(
3944 per_minute: u32,
3945 burst: Option<u32>,
3946 key_eviction_policy: KeyEvictionPolicy,
3947 max_tracked_keys: NonZeroUsize,
3948) -> Arc<ExtraRouteRateLimiter> {
3949 let rate = std::num::NonZeroU32::new(per_minute.max(1)).unwrap_or(std::num::NonZeroU32::MIN);
3950 let mut quota = governor::Quota::per_minute(rate);
3951 if let Some(b) = burst.and_then(std::num::NonZeroU32::new) {
3952 quota = quota.allow_burst(b);
3953 }
3954 Arc::new(BoundedKeyedLimiter::new_with_policy(
3955 quota,
3956 max_tracked_keys,
3957 EXTRA_ROUTE_IDLE_EVICTION,
3958 key_eviction_policy,
3959 ))
3960}
3961
3962async fn extra_route_rate_limit_middleware(
3987 limiter: Arc<ExtraRouteRateLimiter>,
3988 exempt: Arc<std::collections::HashSet<String>>,
3989 req: Request<Body>,
3990 next: Next,
3991) -> axum::response::Response {
3992 if exempt.contains(req.uri().path()) {
3993 return next.run(req).await;
3994 }
3995 let peer_key = limiter_client_key(req.extensions());
3996 match limiter.check_key_detailed(&peer_key) {
3997 Ok(()) => {}
3998 Err(BoundedLimiterDeny::RateLimited(wait)) => {
3999 #[cfg(feature = "metrics")]
4000 crate::metrics::record_rate_limit_deny(req.extensions(), "extra_route");
4001 tracing::warn!(rate_limit_key = %peer_key, "extra route request rate limited");
4002 return RmcpServerKitError::RateLimitedFor {
4003 message: "too many requests to application routes from this source".into(),
4004 retry_after: wait,
4005 }
4006 .into_response();
4007 }
4008 Err(BoundedLimiterDeny::CapacityFull) => {
4009 tracing::warn!(
4010 rate_limit_key = %peer_key,
4011 "extra route limiter rejected unseen key because tracked-key capacity is full"
4012 );
4013 return (
4014 axum::http::StatusCode::SERVICE_UNAVAILABLE,
4015 "rate limiter capacity exhausted",
4016 )
4017 .into_response();
4018 }
4019 }
4020 next.run(req).await
4021}
4022
4023async fn origin_check_middleware(
4029 allowed: Arc<[String]>,
4030 log_request_headers: bool,
4031 req: Request<Body>,
4032 next: Next,
4033) -> axum::response::Response {
4034 let method = req.method().clone();
4035 let path = req.uri().path().to_owned();
4036
4037 log_incoming_request(&method, &path, req.headers(), log_request_headers);
4038
4039 if let Some(origin) = req.headers().get(axum::http::header::ORIGIN) {
4040 let origin_str = origin.to_str().unwrap_or("");
4041 if !allowed.iter().any(|a| a == origin_str) {
4042 tracing::warn!(
4043 origin = origin_str,
4044 %method,
4045 %path,
4046 allowed = ?&*allowed,
4047 "rejected request: Origin not allowed"
4048 );
4049 return (
4050 axum::http::StatusCode::FORBIDDEN,
4051 "Forbidden: Origin not allowed",
4052 )
4053 .into_response();
4054 }
4055 }
4056 next.run(req).await
4057}
4058
4059fn log_incoming_request(
4062 method: &axum::http::Method,
4063 path: &str,
4064 headers: &axum::http::HeaderMap,
4065 log_request_headers: bool,
4066) {
4067 if log_request_headers {
4068 tracing::debug!(
4069 %method,
4070 %path,
4071 headers = %format_request_headers_for_log(headers),
4072 "incoming request"
4073 );
4074 } else {
4075 tracing::debug!(%method, %path, "incoming request");
4076 }
4077}
4078
4079const REDACTED_LOG_HEADERS: [&str; 6] = [
4087 "authorization",
4088 "cookie",
4089 "proxy-authorization",
4090 "forwarded",
4091 "x-forwarded-for",
4092 "x-real-ip",
4093];
4094
4095fn format_request_headers_for_log(headers: &axum::http::HeaderMap) -> String {
4096 headers
4097 .iter()
4098 .map(|(k, v)| {
4099 let name = k.as_str();
4100 if REDACTED_LOG_HEADERS.contains(&name) {
4101 format!("{name}: [REDACTED]")
4102 } else {
4103 format!("{name}: {}", v.to_str().unwrap_or("<non-utf8>"))
4104 }
4105 })
4106 .collect::<Vec<_>>()
4107 .join(", ")
4108}
4109
4110#[allow(
4134 clippy::cognitive_complexity,
4135 reason = "complexity is purely tracing macro expansion (info/warn + match arms); 18 lines of straight-line code, nothing meaningful to extract"
4136)]
4137pub async fn serve_stdio<H>(handler: H) -> Result<(), RmcpServerKitError>
4138where
4139 H: ServerHandler + 'static,
4140{
4141 use rmcp::ServiceExt as _;
4142
4143 tracing::info!("stdio transport: serving on stdin/stdout");
4144 tracing::warn!("stdio mode: auth, RBAC, TLS, and Origin checks are DISABLED");
4145
4146 let transport = rmcp::transport::io::stdio();
4147
4148 let service = handler
4149 .serve(transport)
4150 .await
4151 .map_err(|e| RmcpServerKitError::Startup(format!("stdio initialize failed: {e}")))?;
4152
4153 if let Err(e) = service.waiting().await {
4154 tracing::warn!(error = %e, "stdio session ended with error");
4155 }
4156 tracing::info!("stdio session ended");
4157 Ok(())
4158}
4159
4160#[allow(
4161 deprecated,
4162 reason = "builder methods are the sanctioned transition layer for deprecated public fields"
4163)]
4164impl McpServerConfig {
4165 #[must_use]
4169 pub fn with_tls_paths(mut self, cert_path: Option<PathBuf>, key_path: Option<PathBuf>) -> Self {
4170 self.tls_cert_path = cert_path;
4171 self.tls_key_path = key_path;
4172 self
4173 }
4174
4175 #[must_use]
4179 pub fn with_tls_cert_path(mut self, cert_path: impl Into<PathBuf>) -> Self {
4180 self.tls_cert_path = Some(cert_path.into());
4181 self
4182 }
4183
4184 #[must_use]
4188 pub fn with_tls_key_path(mut self, key_path: impl Into<PathBuf>) -> Self {
4189 self.tls_key_path = Some(key_path.into());
4190 self
4191 }
4192
4193 #[must_use]
4195 pub fn with_optional_auth(mut self, auth: Option<AuthConfig>) -> Self {
4196 self.auth = auth;
4197 self
4198 }
4199
4200 #[must_use]
4202 pub fn with_optional_session_binding_secret(mut self, secret: Option<SecretString>) -> Self {
4203 self.session_binding_secret = secret;
4204 self
4205 }
4206
4207 #[must_use]
4209 pub fn with_optional_tool_rate_limit(mut self, per_minute: Option<u32>) -> Self {
4210 self.tool_rate_limit = per_minute;
4211 self
4212 }
4213
4214 #[must_use]
4216 pub fn with_optional_tool_rate_limit_burst(mut self, burst: Option<u32>) -> Self {
4217 self.tool_rate_limit_burst = burst;
4218 self
4219 }
4220
4221 #[must_use]
4223 pub fn with_optional_extra_route_rate_limit(mut self, per_minute: Option<u32>) -> Self {
4224 self.extra_route_rate_limit = per_minute;
4225 self
4226 }
4227
4228 #[must_use]
4230 pub fn with_optional_extra_route_rate_limit_burst(mut self, burst: Option<u32>) -> Self {
4231 self.extra_route_rate_limit_burst = burst;
4232 self
4233 }
4234
4235 #[must_use]
4237 pub fn with_optional_forwarded_header(mut self, mode: Option<ForwardedHeaderMode>) -> Self {
4238 self.forwarded_header = mode;
4239 self
4240 }
4241
4242 #[must_use]
4244 pub fn with_optional_public_url(mut self, url: Option<String>) -> Self {
4245 self.public_url = url;
4246 self
4247 }
4248
4249 #[must_use]
4253 pub fn with_compression_min_size(mut self, min_size: u16) -> Self {
4254 self.compression_min_size = min_size;
4255 self
4256 }
4257
4258 #[must_use]
4260 pub fn with_compression_enabled(mut self, enabled: bool) -> Self {
4261 self.compression_enabled = enabled;
4262 self
4263 }
4264
4265 #[must_use]
4267 pub fn with_optional_max_concurrent_requests(mut self, limit: Option<usize>) -> Self {
4268 self.max_concurrent_requests = limit;
4269 self
4270 }
4271
4272 #[must_use]
4274 pub fn with_admin_enabled(mut self, enabled: bool) -> Self {
4275 self.admin_enabled = enabled;
4276 self
4277 }
4278
4279 #[must_use]
4282 pub fn with_admin_role(mut self, role: impl Into<String>) -> Self {
4283 self.admin_role = role.into();
4284 self
4285 }
4286
4287 #[must_use]
4289 pub fn with_expose_build_metadata(mut self, enabled: bool) -> Self {
4290 self.expose_build_metadata = enabled;
4291 self
4292 }
4293}
4294
4295fn warn_security_header_overrides(cfg: &SecurityHeadersConfig) {
4296 for (field, value) in security_header_overrides(cfg) {
4297 let action = if value.is_empty() {
4298 "omitted"
4299 } else {
4300 "overridden"
4301 };
4302 tracing::warn!(
4303 security_header = field,
4304 action,
4305 "security header configured; inspect server.security_headers.<security_header>"
4306 );
4307 }
4308}
4309
4310fn security_header_overrides(
4311 cfg: &SecurityHeadersConfig,
4312) -> impl Iterator<Item = (&'static str, &str)> {
4313 [
4314 (
4315 "x_content_type_options",
4316 cfg.x_content_type_options.as_deref(),
4317 ),
4318 ("x_frame_options", cfg.x_frame_options.as_deref()),
4319 ("cache_control", cfg.cache_control.as_deref()),
4320 ("referrer_policy", cfg.referrer_policy.as_deref()),
4321 (
4322 "cross_origin_opener_policy",
4323 cfg.cross_origin_opener_policy.as_deref(),
4324 ),
4325 (
4326 "cross_origin_resource_policy",
4327 cfg.cross_origin_resource_policy.as_deref(),
4328 ),
4329 (
4330 "cross_origin_embedder_policy",
4331 cfg.cross_origin_embedder_policy.as_deref(),
4332 ),
4333 ("permissions_policy", cfg.permissions_policy.as_deref()),
4334 (
4335 "x_permitted_cross_domain_policies",
4336 cfg.x_permitted_cross_domain_policies.as_deref(),
4337 ),
4338 (
4339 "content_security_policy",
4340 cfg.content_security_policy.as_deref(),
4341 ),
4342 (
4343 "x_dns_prefetch_control",
4344 cfg.x_dns_prefetch_control.as_deref(),
4345 ),
4346 (
4347 "strict_transport_security",
4348 cfg.strict_transport_security.as_deref(),
4349 ),
4350 ]
4351 .into_iter()
4352 .filter_map(|(field, value)| value.map(|v| (field, v)))
4353}
4354
4355fn check_auth_capacity_knobs(auth: Option<&AuthConfig>) -> Result<(), RmcpServerKitError> {
4356 if let Some(auth_cfg) = auth {
4357 if let Some(rl) = &auth_cfg.rate_limit {
4358 (rl.max_attempts_per_minute != 0).ok_or_else(|| {
4359 RmcpServerKitError::Config(
4360 "auth.rate_limit.max_attempts_per_minute must be nonzero".into(),
4361 )
4362 })?;
4363 (rl.pre_auth_max_per_minute != Some(0)).ok_or_else(|| {
4368 RmcpServerKitError::Config(
4369 "auth.rate_limit.pre_auth_max_per_minute must be nonzero when set".into(),
4370 )
4371 })?;
4372 }
4373 if let Some(mtls) = &auth_cfg.mtls {
4374 check_mtls_capacity_knobs(mtls)?;
4375 }
4376 auth_cfg.check_oauth_feature()?;
4377 }
4378 Ok(())
4379}
4380
4381fn check_mtls_capacity_knobs(mtls: &MtlsConfig) -> Result<(), RmcpServerKitError> {
4382 (mtls.crl_max_concurrent_fetches != 0).ok_or_else(|| {
4383 RmcpServerKitError::Config("auth.mtls.crl_max_concurrent_fetches must be nonzero".into())
4384 })?;
4385 (mtls.crl_discovery_rate_per_min != 0).ok_or_else(|| {
4386 RmcpServerKitError::Config("auth.mtls.crl_discovery_rate_per_min must be nonzero".into())
4387 })?;
4388 (mtls.crl_max_host_semaphores != 0).ok_or_else(|| {
4389 RmcpServerKitError::Config("auth.mtls.crl_max_host_semaphores must be nonzero".into())
4390 })?;
4391 (mtls.crl_max_seen_urls != 0).ok_or_else(|| {
4392 RmcpServerKitError::Config("auth.mtls.crl_max_seen_urls must be nonzero".into())
4393 })?;
4394 (mtls.crl_max_cache_entries != 0).ok_or_else(|| {
4395 RmcpServerKitError::Config("auth.mtls.crl_max_cache_entries must be nonzero".into())
4396 })?;
4397 (mtls.crl_max_response_bytes != 0).ok_or_else(|| {
4402 RmcpServerKitError::Config("auth.mtls.crl_max_response_bytes must be nonzero".into())
4403 })?;
4404 Ok(())
4405}
4406
4407#[cfg(test)]
4408mod tests {
4409 #![allow(
4410 clippy::unwrap_used,
4411 clippy::expect_used,
4412 clippy::panic,
4413 clippy::indexing_slicing,
4414 clippy::unwrap_in_result,
4415 clippy::print_stdout,
4416 clippy::print_stderr,
4417 deprecated,
4418 reason = "internal unit tests legitimately read/write the deprecated `pub` fields they were designed to verify"
4419 )]
4420 use std::{sync::Arc, time::Duration};
4421
4422 use axum::{
4423 body::Body,
4424 http::{Request, StatusCode, header},
4425 response::IntoResponse,
4426 };
4427 use http_body_util::BodyExt;
4428 use tower::ServiceExt as _;
4429
4430 use super::*;
4431
4432 #[tokio::test]
4435 async fn external_shutdown_bridge_exits_when_internal_token_cancels() {
4436 let external = CancellationToken::new();
4437 let internal = CancellationToken::new();
4438 let bridge = spawn_external_shutdown_bridge(external.clone(), internal.clone());
4439
4440 internal.cancel();
4443
4444 let joined = tokio::time::timeout(Duration::from_secs(2), bridge).await;
4445 assert!(
4446 joined.is_ok(),
4447 "bridge task must exit once the internal token is cancelled, \
4448 otherwise it leaks for the lifetime of the process"
4449 );
4450 }
4451
4452 #[tokio::test]
4453 async fn external_shutdown_bridge_still_forwards_external_cancel() {
4454 let external = CancellationToken::new();
4455 let internal = CancellationToken::new();
4456 let bridge = spawn_external_shutdown_bridge(external.clone(), internal.clone());
4457
4458 external.cancel();
4459
4460 let joined = tokio::time::timeout(Duration::from_secs(2), bridge).await;
4461 assert!(joined.is_ok(), "bridge task must exit on external cancel");
4462 assert!(
4463 internal.is_cancelled(),
4464 "external cancellation must still propagate to the internal token"
4465 );
4466 }
4467
4468 #[test]
4469 fn cancel_on_drop_cancels_its_token() {
4470 let ct = CancellationToken::new();
4471 {
4472 let _guard = CancelOnDrop(ct.clone());
4473 assert!(!ct.is_cancelled());
4474 }
4475 assert!(
4476 ct.is_cancelled(),
4477 "dropping the guard must cancel background startup tasks"
4478 );
4479 }
4480
4481 #[test]
4482 fn validate_rejects_mtls_without_tls() {
4483 for (cert, key) in [
4484 (None, None),
4485 (Some("cert.pem"), None),
4486 (None, Some("key.pem")),
4487 ] {
4488 let mut auth = AuthConfig::with_keys(vec![]);
4489 auth.mtls = Some(valid_mtls_config());
4490 let mut cfg = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0").with_auth(auth);
4491 cfg.tls_cert_path = cert.map(Into::into);
4492 cfg.tls_key_path = key.map(Into::into);
4493
4494 let err = cfg
4495 .validate()
4496 .expect_err("mTLS without both TLS paths must be rejected");
4497 let msg = err.to_string();
4498 assert!(
4499 msg.contains("tls_cert_path") && msg.contains("tls_key_path"),
4500 "cert={cert:?} key={key:?}: {msg}"
4501 );
4502 }
4503 }
4504
4505 #[test]
4506 fn validate_accepts_mtls_with_tls() {
4507 let mut auth = AuthConfig::with_keys(vec![]);
4508 auth.mtls = Some(valid_mtls_config());
4509 let mut cfg = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0").with_auth(auth);
4510 cfg.tls_cert_path = Some("cert.pem".into());
4511 cfg.tls_key_path = Some("key.pem".into());
4512
4513 assert!(cfg.validate().is_ok(), "mTLS with both TLS paths is valid");
4514 }
4515
4516 #[test]
4517 fn validate_rejects_blank_api_key_name() {
4518 let blank = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0").with_auth(
4519 AuthConfig::with_keys(vec![crate::auth::ApiKeyEntry::new("", "hash", "viewer")]),
4520 );
4521 let err = blank
4522 .validate()
4523 .expect_err("blank API-key name must be rejected");
4524 assert!(err.to_string().contains("api_keys[0]"), "{err}");
4525
4526 let whitespace = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0").with_auth(
4527 AuthConfig::with_keys(vec![crate::auth::ApiKeyEntry::new(" ", "hash", "viewer")]),
4528 );
4529 assert!(whitespace.validate().is_err());
4530
4531 let ok = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0").with_auth(
4532 AuthConfig::with_keys(vec![crate::auth::ApiKeyEntry::new(
4533 "viewer-key",
4534 "hash",
4535 "viewer",
4536 )]),
4537 );
4538 assert!(ok.validate().is_ok(), "a normal name must still validate");
4539 }
4540
4541 fn reload_test_state(name: &str) -> (Arc<AuthState>, String) {
4542 let (token, hash) = crate::auth::generate_api_key().unwrap();
4543 let state = Arc::new(AuthState {
4544 api_keys: ArcSwap::from_pointee(vec![crate::auth::ApiKeyEntry::new(name, hash, "ops")]),
4545 rate_limiter: None,
4546 pre_auth_limiter: None,
4547 #[cfg(feature = "oauth")]
4548 jwks_cache: None,
4549 seen_identities: crate::auth::SeenIdentitySet::new(),
4550 counters: crate::auth::AuthCounters::default(),
4551 resource_metadata_url: None,
4552 });
4553 (state, token)
4554 }
4555
4556 #[test]
4557 fn try_reload_auth_keys_rejects_blank_name() {
4558 let (state, _token) = reload_test_state("prev-key");
4559 let handle = ReloadHandle {
4560 auth: Some(state),
4561 rbac: None,
4562 crl_set: None,
4563 };
4564 let err = handle
4565 .try_reload_auth_keys(vec![crate::auth::ApiKeyEntry::new("", "h", "ops")])
4566 .expect_err("blank API-key name must be rejected on reload");
4567 assert!(err.to_string().contains("api_keys[0]"), "{err}");
4568 }
4569
4570 #[test]
4571 fn reload_auth_keys_blank_name_leaves_previous_keys() {
4572 let (state, token) = reload_test_state("prev-key");
4573 let handle = ReloadHandle {
4574 auth: Some(Arc::clone(&state)),
4575 rbac: None,
4576 crl_set: None,
4577 };
4578 handle.reload_auth_keys(vec![crate::auth::ApiKeyEntry::new(" ", "h", "ops")]);
4579
4580 let installed = state.api_keys.load();
4581 assert!(
4582 crate::auth::verify_bearer_token(&token, &installed).is_some(),
4583 "the previous key must still authenticate after a rejected reload"
4584 );
4585 }
4586
4587 #[test]
4590 fn server_config_new_defaults() {
4591 let cfg = McpServerConfig::new("0.0.0.0:8443", "test-server", "1.0.0");
4592 assert_eq!(cfg.bind_addr, "0.0.0.0:8443");
4593 assert_eq!(cfg.name, "test-server");
4594 assert_eq!(cfg.version, "1.0.0");
4595 assert!(cfg.tls_cert_path.is_none());
4596 assert!(cfg.tls_key_path.is_none());
4597 assert!(cfg.auth.is_none());
4598 assert!(cfg.rbac.is_none());
4599 assert!(cfg.allowed_origins.is_empty());
4600 assert!(cfg.tool_rate_limit.is_none());
4601 assert!(cfg.readiness_check.is_none());
4602 assert_eq!(cfg.max_request_body, 1024 * 1024);
4603 assert_eq!(cfg.request_timeout, Duration::from_mins(2));
4604 assert_eq!(cfg.shutdown_timeout, Duration::from_secs(30));
4605 assert!(!cfg.log_request_headers);
4606 assert_eq!(cfg.tls_handshake_timeout, Duration::from_secs(10));
4607 assert_eq!(cfg.max_concurrent_tls_handshakes, 256);
4608 assert!(cfg.session_store.is_none());
4609 assert!(cfg.session_binding_secret.is_none());
4610 }
4611
4612 #[derive(Default)]
4613 struct TestSessionStore;
4614
4615 #[async_trait::async_trait]
4616 impl SessionStore for TestSessionStore {
4617 async fn load(
4618 &self,
4619 _session_id: &str,
4620 ) -> Result<
4621 Option<rmcp::transport::streamable_http_server::session::SessionState>,
4622 rmcp::transport::streamable_http_server::session::SessionStoreError,
4623 > {
4624 Ok(None)
4625 }
4626
4627 async fn store(
4628 &self,
4629 _session_id: &str,
4630 _state: &rmcp::transport::streamable_http_server::session::SessionState,
4631 ) -> Result<(), rmcp::transport::streamable_http_server::session::SessionStoreError>
4632 {
4633 Ok(())
4634 }
4635
4636 async fn delete(
4637 &self,
4638 _session_id: &str,
4639 ) -> Result<(), rmcp::transport::streamable_http_server::session::SessionStoreError>
4640 {
4641 Ok(())
4642 }
4643 }
4644
4645 fn test_session_store() -> Arc<dyn SessionStore> {
4646 Arc::new(TestSessionStore)
4647 }
4648
4649 fn shared_session_binding_secret() -> SecretString {
4650 SecretString::from("0123456789abcdef0123456789abcdef")
4651 }
4652
4653 #[test]
4654 fn session_store_defaults_to_none() {
4655 let cfg = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0");
4656
4657 assert!(cfg.session_store.is_none());
4658 }
4659
4660 #[test]
4661 fn event_store_defaults_to_none() {
4662 let cfg = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0");
4663
4664 assert!(cfg.event_store.is_none());
4665 }
4666
4667 #[test]
4668 fn validate_rejects_session_store_without_binding_secret() {
4669 let cfg = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0")
4670 .with_auth(AuthConfig::with_keys(vec![]))
4671 .with_session_store(test_session_store());
4672
4673 let err = cfg
4674 .validate()
4675 .expect_err("authenticated shared-store binding needs a shared secret");
4676 let msg = err.to_string();
4677 assert!(msg.contains("session_store"), "{msg}");
4678 assert!(msg.contains("session_binding"), "{msg}");
4679 assert!(msg.contains("shared secret"), "{msg}");
4680 }
4681
4682 #[test]
4683 fn validate_allows_session_store_with_binding_secret() {
4684 let cfg = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0")
4685 .with_auth(AuthConfig::with_keys(vec![]))
4686 .with_session_store(test_session_store())
4687 .with_session_binding_secret(shared_session_binding_secret());
4688
4689 assert!(cfg.validate().is_ok());
4690 }
4691
4692 #[test]
4693 fn validate_allows_session_store_when_binding_disabled() {
4694 let cfg = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0")
4695 .with_auth(AuthConfig::with_keys(vec![]))
4696 .with_session_binding(false)
4697 .with_session_store(test_session_store());
4698
4699 assert!(cfg.validate().is_ok());
4700 }
4701
4702 #[test]
4703 fn validate_allows_binding_secret_without_session_store() {
4704 let cfg = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0")
4705 .with_auth(AuthConfig::with_keys(vec![]))
4706 .with_session_binding_secret(shared_session_binding_secret());
4707
4708 assert!(cfg.validate().is_ok());
4709 }
4710
4711 #[test]
4712 fn tls_handshake_builders_set_fields() {
4713 let cfg = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0")
4714 .with_tls_handshake_timeout(Duration::from_secs(3))
4715 .with_max_concurrent_tls_handshakes(64);
4716 assert_eq!(cfg.tls_handshake_timeout, Duration::from_secs(3));
4717 assert_eq!(cfg.max_concurrent_tls_handshakes, 64);
4718 }
4719
4720 #[test]
4721 fn validate_rejects_zero_tls_handshake_timeout() {
4722 let cfg = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0")
4723 .with_tls_handshake_timeout(Duration::ZERO);
4724 let err = cfg.validate().expect_err("zero handshake timeout");
4725 assert!(err.to_string().contains("tls_handshake_timeout"));
4726 }
4727
4728 #[test]
4729 fn validate_rejects_zero_max_concurrent_tls_handshakes() {
4730 let cfg = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0")
4731 .with_max_concurrent_tls_handshakes(0);
4732 let err = cfg.validate().expect_err("zero handshake concurrency");
4733 assert!(err.to_string().contains("max_concurrent_tls_handshakes"));
4734 }
4735
4736 #[test]
4737 fn validate_consumes_and_proves() {
4738 let cfg = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0");
4740 let validated = cfg.validate().expect("valid config");
4741 assert_eq!(validated.as_inner().name, "test-server");
4743 let raw = validated.into_inner();
4745 assert_eq!(raw.name, "test-server");
4746
4747 let mut bad = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0");
4749 bad.max_request_body = 0;
4750 assert!(bad.validate().is_err(), "zero body cap must fail validate");
4751 }
4752
4753 #[test]
4754 fn validate_rejects_zero_max_concurrent_requests() {
4755 let cfg =
4756 McpServerConfig::new("127.0.0.1:8080", "test", "1.0.0").with_max_concurrent_requests(0);
4757 let err = cfg.validate().expect_err("zero concurrency cap must fail");
4758 assert!(
4759 format!("{err}").contains("max_concurrent_requests"),
4760 "error should mention max_concurrent_requests, got: {err}"
4761 );
4762 }
4763
4764 #[test]
4765 fn validate_rejects_zero_max_tracked_keys() {
4766 let rl = crate::auth::RateLimitConfig {
4769 max_attempts_per_minute: 30,
4770 pre_auth_max_per_minute: None,
4771 max_tracked_keys: 0,
4772 idle_eviction: Duration::from_secs(15 * 60),
4773 burst: None,
4774 pre_auth_burst: None,
4775 key_eviction_policy: KeyEvictionPolicy::default(),
4776 };
4777 let auth_cfg = AuthConfig {
4778 enabled: true,
4779 api_keys: Vec::new(),
4780 mtls: None,
4781 rate_limit: Some(rl),
4782 #[cfg(feature = "oauth")]
4783 oauth: None,
4784 #[cfg(not(feature = "oauth"))]
4785 oauth: None,
4786 };
4787 let cfg = McpServerConfig::new("127.0.0.1:8080", "test", "1.0.0").with_auth(auth_cfg);
4788 let err = cfg.validate().expect_err("zero max_tracked_keys must fail");
4789 assert!(
4790 format!("{err}").contains("max_tracked_keys"),
4791 "error should mention max_tracked_keys, got: {err}"
4792 );
4793 }
4794
4795 #[test]
4796 fn derive_allowed_hosts_includes_public_host() {
4797 let hosts = derive_allowed_hosts("0.0.0.0:8080", Some("https://mcp.example.com/mcp"));
4798 assert!(
4799 hosts.iter().any(|h| h == "mcp.example.com"),
4800 "public_url host must be allowed"
4801 );
4802 }
4803
4804 #[test]
4805 fn derive_allowed_hosts_includes_bind_authority() {
4806 let hosts = derive_allowed_hosts("127.0.0.1:8080", None);
4807 assert!(
4808 hosts.iter().any(|h| h == "127.0.0.1"),
4809 "bind host must be allowed"
4810 );
4811 assert!(
4812 hosts.iter().any(|h| h == "127.0.0.1:8080"),
4813 "bind authority must be allowed"
4814 );
4815 }
4816
4817 #[tokio::test]
4820 async fn healthz_returns_ok_json() {
4821 let resp = healthz().await.into_response();
4822 assert_eq!(resp.status(), StatusCode::OK);
4823 let body = resp.into_body().collect().await.unwrap().to_bytes();
4824 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
4825 assert_eq!(json["status"], "ok");
4826 assert!(
4827 json.get("name").is_none(),
4828 "healthz must not expose server name"
4829 );
4830 assert!(
4831 json.get("version").is_none(),
4832 "healthz must not expose version"
4833 );
4834 }
4835
4836 #[tokio::test]
4839 async fn readyz_returns_ok_when_ready() {
4840 let check: ReadinessCheck =
4841 Arc::new(|| Box::pin(async { serde_json::json!({"ready": true, "db": "connected"}) }));
4842 let resp = readyz(check).await.into_response();
4843 assert_eq!(resp.status(), StatusCode::OK);
4844 let body = resp.into_body().collect().await.unwrap().to_bytes();
4845 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
4846 assert_eq!(json["ready"], true);
4847 assert!(
4848 json.get("name").is_none(),
4849 "readyz must not expose server name"
4850 );
4851 assert!(
4852 json.get("version").is_none(),
4853 "readyz must not expose version"
4854 );
4855 assert_eq!(json["db"], "connected");
4856 }
4857
4858 #[tokio::test]
4859 async fn readyz_returns_503_when_not_ready() {
4860 let check: ReadinessCheck =
4861 Arc::new(|| Box::pin(async { serde_json::json!({"ready": false}) }));
4862 let resp = readyz(check).await.into_response();
4863 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
4864 }
4865
4866 #[tokio::test]
4867 async fn readyz_returns_503_when_ready_missing() {
4868 let check: ReadinessCheck =
4869 Arc::new(|| Box::pin(async { serde_json::json!({"status": "starting"}) }));
4870 let resp = readyz(check).await.into_response();
4871 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
4873 }
4874
4875 fn peer_probe_router() -> axum::Router {
4880 async fn probe(req: Request<Body>) -> String {
4881 let ci = req
4882 .extensions()
4883 .get::<ConnectInfo<SocketAddr>>()
4884 .map(|c| c.0.to_string())
4885 .unwrap_or_default();
4886 let pa = req
4887 .extensions()
4888 .get::<PeerAddr>()
4889 .map(|p| p.addr.to_string())
4890 .unwrap_or_default();
4891 format!("{ci}|{pa}")
4892 }
4893 axum::Router::new()
4894 .route("/probe", axum::routing::get(probe))
4895 .layer(axum::middleware::from_fn(|req, next| {
4896 normalize_peer_addr_middleware(None, req, next)
4897 }))
4898 }
4899
4900 async fn body_string(resp: axum::response::Response) -> String {
4901 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
4902 String::from_utf8(bytes.to_vec()).unwrap()
4903 }
4904
4905 #[tokio::test]
4906 async fn normalize_preserves_existing_connect_info_and_mirrors_peer_addr() {
4907 let plain: SocketAddr = "10.0.0.1:1111".parse().unwrap();
4910 let tls: SocketAddr = "10.0.0.2:2222".parse().unwrap();
4911 let req = Request::builder()
4912 .uri("/probe")
4913 .extension(ConnectInfo(plain))
4914 .extension(ConnectInfo(TlsConnInfo::new(tls, None)))
4915 .body(Body::empty())
4916 .unwrap();
4917 let resp = peer_probe_router().oneshot(req).await.unwrap();
4918 assert_eq!(resp.status(), StatusCode::OK);
4919 assert_eq!(body_string(resp).await, format!("{plain}|{plain}"));
4920 }
4921
4922 #[tokio::test]
4923 async fn normalize_inserts_connect_info_and_peer_addr_from_tls() {
4924 let tls: SocketAddr = "192.168.1.7:50443".parse().unwrap();
4925 let req = Request::builder()
4926 .uri("/probe")
4927 .extension(ConnectInfo(TlsConnInfo::new(tls, None)))
4928 .body(Body::empty())
4929 .unwrap();
4930 let resp = peer_probe_router().oneshot(req).await.unwrap();
4931 assert_eq!(resp.status(), StatusCode::OK);
4932 assert_eq!(body_string(resp).await, format!("{tls}|{tls}"));
4933 }
4934
4935 #[tokio::test]
4936 async fn normalize_no_op_without_any_connect_info() {
4937 let req = Request::builder()
4938 .uri("/probe")
4939 .body(Body::empty())
4940 .unwrap();
4941 let resp = peer_probe_router().oneshot(req).await.unwrap();
4942 assert_eq!(resp.status(), StatusCode::OK);
4943 assert_eq!(body_string(resp).await, "|");
4944 }
4945
4946 #[tokio::test]
4947 async fn peer_addr_extractor_rejects_when_absent() {
4948 async fn h(peer: PeerAddr) -> String {
4949 peer.addr.to_string()
4950 }
4951 let app = axum::Router::new().route("/p", axum::routing::get(h));
4952 let req = Request::builder().uri("/p").body(Body::empty()).unwrap();
4953 let resp = app.oneshot(req).await.unwrap();
4954 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
4955 }
4956
4957 #[tokio::test]
4958 async fn peer_addr_extractor_returns_value_when_present() {
4959 async fn h(peer: PeerAddr) -> String {
4960 peer.addr.to_string()
4961 }
4962 let addr: SocketAddr = "127.0.0.1:9999".parse().unwrap();
4963 let app = axum::Router::new().route("/p", axum::routing::get(h));
4964 let req = Request::builder()
4965 .uri("/p")
4966 .extension(PeerAddr::new(addr))
4967 .body(Body::empty())
4968 .unwrap();
4969 let resp = app.oneshot(req).await.unwrap();
4970 assert_eq!(resp.status(), StatusCode::OK);
4971 assert_eq!(body_string(resp).await, addr.to_string());
4972 }
4973
4974 #[tokio::test]
4975 async fn peer_addr_via_extension_extractor() {
4976 async fn h(axum::Extension(peer): axum::Extension<PeerAddr>) -> String {
4977 peer.addr.to_string()
4978 }
4979 let addr: SocketAddr = "127.0.0.1:4242".parse().unwrap();
4980 let app = axum::Router::new().route("/p", axum::routing::get(h));
4981 let req = Request::builder()
4982 .uri("/p")
4983 .extension(PeerAddr::new(addr))
4984 .body(Body::empty())
4985 .unwrap();
4986 let resp = app.oneshot(req).await.unwrap();
4987 assert_eq!(resp.status(), StatusCode::OK);
4988 assert_eq!(body_string(resp).await, addr.to_string());
4989 }
4990
4991 fn limited_router(per_minute: u32) -> axum::Router {
4996 limited_router_with_burst(per_minute, None)
4997 }
4998
4999 fn limited_router_with_burst(per_minute: u32, burst: Option<u32>) -> axum::Router {
5001 limited_router_full(per_minute, burst, &[])
5002 }
5003
5004 fn limited_router_full(
5008 per_minute: u32,
5009 burst: Option<u32>,
5010 exempt_paths: &[&str],
5011 ) -> axum::Router {
5012 let limiter = build_extra_route_rate_limiter_with_policy(
5013 per_minute,
5014 burst,
5015 KeyEvictionPolicy::default(),
5016 NonZeroUsize::new(EXTRA_ROUTE_MAX_TRACKED_KEYS).unwrap_or(NonZeroUsize::MIN),
5017 );
5018 let exempt: Arc<std::collections::HashSet<String>> =
5019 Arc::new(exempt_paths.iter().map(|s| (*s).to_owned()).collect());
5020 axum::Router::new()
5021 .route("/limited", axum::routing::get(|| async { "ok" }))
5022 .route("/exempt", axum::routing::get(|| async { "ok" }))
5023 .layer(axum::middleware::from_fn(move |req, next| {
5024 let l = Arc::clone(&limiter);
5025 let e = Arc::clone(&exempt);
5026 extra_route_rate_limit_middleware(l, e, req, next)
5027 }))
5028 }
5029
5030 fn limited_req(ip: &str) -> Request<Body> {
5031 limited_req_to(ip, "/limited")
5032 }
5033
5034 fn limited_req_to(ip: &str, path: &str) -> Request<Body> {
5035 let addr: SocketAddr = format!("{ip}:40000").parse().unwrap();
5036 Request::builder()
5037 .uri(path)
5038 .extension(ConnectInfo(addr))
5039 .body(Body::empty())
5040 .unwrap()
5041 }
5042
5043 #[tokio::test]
5044 async fn extra_route_limiter_denies_over_quota() {
5045 let app = limited_router(2);
5046 for i in 0..2 {
5047 let resp = app.clone().oneshot(limited_req("10.1.1.1")).await.unwrap();
5048 assert_eq!(resp.status(), StatusCode::OK, "request {i} should pass");
5049 }
5050 let resp = app.clone().oneshot(limited_req("10.1.1.1")).await.unwrap();
5051 assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
5052 let body = body_string(resp).await;
5053 assert!(
5054 body.contains("too many requests to application routes"),
5055 "deny body should match the limiter message, got: {body}"
5056 );
5057 }
5058
5059 fn one_tracked_key() -> NonZeroUsize {
5060 NonZeroUsize::new(1).unwrap_or(NonZeroUsize::MIN)
5061 }
5062
5063 #[tokio::test]
5064 async fn extra_route_limiter_capacity_full_returns_503_without_retry_after() {
5065 let limiter = build_extra_route_rate_limiter_with_policy(
5066 10,
5067 None,
5068 KeyEvictionPolicy::RejectNew,
5069 one_tracked_key(),
5070 );
5071 let exempt = Arc::new(std::collections::HashSet::new());
5072 let app = axum::Router::new()
5073 .route("/limited", axum::routing::get(|| async { "ok" }))
5074 .layer(axum::middleware::from_fn(move |req, next| {
5075 let l = Arc::clone(&limiter);
5076 let e = Arc::clone(&exempt);
5077 extra_route_rate_limit_middleware(l, e, req, next)
5078 }));
5079 let established = app.clone().oneshot(limited_req("10.1.1.1")).await.unwrap();
5080 assert_eq!(established.status(), StatusCode::OK);
5081
5082 let denied = app.clone().oneshot(limited_req("10.1.1.2")).await.unwrap();
5083
5084 assert_eq!(denied.status(), StatusCode::SERVICE_UNAVAILABLE);
5085 assert!(denied.headers().get(header::RETRY_AFTER).is_none());
5086 }
5087
5088 #[tokio::test]
5089 async fn extra_route_limiter_isolates_keys() {
5090 let app = limited_router(2);
5091 for _ in 0..2 {
5092 let resp = app.clone().oneshot(limited_req("10.2.2.2")).await.unwrap();
5093 assert_eq!(resp.status(), StatusCode::OK);
5094 }
5095 let exhausted = app.clone().oneshot(limited_req("10.2.2.2")).await.unwrap();
5096 assert_eq!(exhausted.status(), StatusCode::TOO_MANY_REQUESTS);
5097 let other = app.clone().oneshot(limited_req("10.3.3.3")).await.unwrap();
5099 assert_eq!(other.status(), StatusCode::OK);
5100 }
5101
5102 #[tokio::test]
5103 async fn extra_route_limiter_bounds_requests_without_peer() {
5104 let app = limited_router(1);
5108 let mk = || {
5109 Request::builder()
5110 .uri("/limited")
5111 .body(Body::empty())
5112 .unwrap()
5113 };
5114 let first = app.clone().oneshot(mk()).await.unwrap();
5115 assert_eq!(
5116 first.status(),
5117 StatusCode::OK,
5118 "first request consumes quota"
5119 );
5120 let second = app.clone().oneshot(mk()).await.unwrap();
5121 assert_eq!(
5122 second.status(),
5123 StatusCode::TOO_MANY_REQUESTS,
5124 "unattributable requests must share a bounded bucket, not bypass the limiter"
5125 );
5126 }
5127
5128 #[test]
5129 fn limiter_client_key_falls_back_to_unattributed() {
5130 let empty = axum::http::Extensions::new();
5131 assert_eq!(limiter_client_key(&empty), RateLimitKey::Unattributed);
5132 }
5133
5134 #[test]
5135 fn unattributed_key_is_distinct_from_unspecified_ip() {
5136 let unspecified = RateLimitKey::Ip("0.0.0.0".parse::<IpAddr>().unwrap());
5140 assert_ne!(unspecified, RateLimitKey::Unattributed);
5141
5142 let mut set = std::collections::HashSet::new();
5143 set.insert(unspecified);
5144 set.insert(RateLimitKey::Unattributed);
5145 assert_eq!(set.len(), 2, "the two keys must hash to distinct buckets");
5146 }
5147
5148 #[test]
5149 fn rate_limit_key_display_does_not_fabricate_an_ip() {
5150 assert_eq!(
5151 RateLimitKey::Ip("10.1.2.3".parse::<IpAddr>().unwrap()).to_string(),
5152 "10.1.2.3"
5153 );
5154 assert_eq!(RateLimitKey::Unattributed.to_string(), "unattributed");
5155 }
5156
5157 #[tokio::test]
5158 async fn extra_route_limiter_extracts_tls_conn_info() {
5159 let app = limited_router(2);
5160 let mk = || {
5161 let addr: SocketAddr = "192.168.9.9:55555".parse().unwrap();
5162 Request::builder()
5163 .uri("/limited")
5164 .extension(ConnectInfo(TlsConnInfo::new(addr, None)))
5165 .body(Body::empty())
5166 .unwrap()
5167 };
5168 for _ in 0..2 {
5169 assert_eq!(
5170 app.clone().oneshot(mk()).await.unwrap().status(),
5171 StatusCode::OK
5172 );
5173 }
5174 let resp = app.clone().oneshot(mk()).await.unwrap();
5175 assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
5176 }
5177
5178 #[tokio::test]
5179 async fn extra_route_limiter_exempt_path_bypasses_quota() {
5180 let app = limited_router_full(1, None, &["/exempt"]);
5183 for i in 0..5 {
5184 let resp = app
5185 .clone()
5186 .oneshot(limited_req_to("10.6.6.6", "/exempt"))
5187 .await
5188 .unwrap();
5189 assert_eq!(resp.status(), StatusCode::OK, "exempt request {i}");
5190 }
5191 let resp = app.clone().oneshot(limited_req("10.6.6.6")).await.unwrap();
5193 assert_eq!(resp.status(), StatusCode::OK);
5194 let resp = app.clone().oneshot(limited_req("10.6.6.6")).await.unwrap();
5196 assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
5197 }
5198
5199 #[tokio::test]
5200 async fn extra_route_limiter_exemption_is_raw_exact_match() {
5201 let app = limited_router_full(1, None, &["/exempt"]);
5204 let ok = app
5205 .clone()
5206 .oneshot(limited_req_to("10.7.7.7", "/exempt/"))
5207 .await
5208 .unwrap();
5209 assert_eq!(
5210 ok.status(),
5211 StatusCode::NOT_FOUND,
5212 "variant path routes 404"
5213 );
5214 let denied = app
5216 .clone()
5217 .oneshot(limited_req_to("10.7.7.7", "/limited"))
5218 .await
5219 .unwrap();
5220 assert_eq!(denied.status(), StatusCode::TOO_MANY_REQUESTS);
5221 }
5222
5223 #[cfg(feature = "metrics")]
5224 #[tokio::test]
5225 async fn extra_route_limiter_deny_increments_counter_exempt_does_not() {
5226 let metrics = Arc::new(crate::metrics::McpMetrics::new().unwrap());
5227 let app = limited_router_full(1, None, &["/exempt"]);
5228 let mk = |path: &str| {
5229 let addr: SocketAddr = "10.8.8.8:40000".parse().unwrap();
5230 Request::builder()
5231 .uri(path)
5232 .extension(ConnectInfo(addr))
5233 .extension(Arc::clone(&metrics))
5234 .body(Body::empty())
5235 .unwrap()
5236 };
5237 let counter = || {
5238 metrics
5239 .rate_limited_total
5240 .with_label_values(&["extra_route"])
5241 .get()
5242 };
5243 for _ in 0..3 {
5245 assert_eq!(
5246 app.clone().oneshot(mk("/exempt")).await.unwrap().status(),
5247 StatusCode::OK
5248 );
5249 }
5250 assert_eq!(counter(), 0, "exempt requests must not count as denies");
5251 assert_eq!(
5253 app.clone().oneshot(mk("/limited")).await.unwrap().status(),
5254 StatusCode::OK
5255 );
5256 assert_eq!(counter(), 0);
5257 assert_eq!(
5258 app.clone().oneshot(mk("/limited")).await.unwrap().status(),
5259 StatusCode::TOO_MANY_REQUESTS
5260 );
5261 assert_eq!(counter(), 1, "deny must increment the extra_route label");
5262 }
5263
5264 #[test]
5265 fn validate_rejects_exempt_paths_without_base_knob() {
5266 let cfg = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0")
5267 .with_extra_route_rate_limit_exempt_paths(["/ok"]);
5268 let err = cfg.validate().expect_err("exempt paths without rate limit");
5269 assert!(err.to_string().contains("requires extra_route_rate_limit"));
5270 }
5271
5272 #[test]
5273 fn validate_rejects_malformed_exempt_paths() {
5274 for bad in ["", "no-slash"] {
5275 let cfg = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0")
5276 .with_extra_route_rate_limit(10)
5277 .with_extra_route_rate_limit_exempt_paths([bad]);
5278 let err = cfg.validate().expect_err("malformed exempt path");
5279 assert!(
5280 err.to_string()
5281 .contains("must be non-empty and start with '/'"),
5282 "entry {bad:?}: {err}"
5283 );
5284 }
5285 }
5286
5287 #[test]
5288 fn validate_accepts_wellformed_exempt_paths() {
5289 let cfg = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0")
5290 .with_extra_route_rate_limit(10)
5291 .with_extra_route_rate_limit_exempt_paths(["/.well-known/oauth-authorization-server"]);
5292 assert!(cfg.validate().is_ok());
5293 }
5294
5295 #[test]
5296 fn validate_rejects_zero_extra_route_rate_limit() {
5297 let cfg = McpServerConfig::new("127.0.0.1:8080", "test-server", "1.0.0")
5298 .with_extra_route_rate_limit(0);
5299 let err = cfg.validate().expect_err("zero extra route rate limit");
5300 assert!(err.to_string().contains("extra_route_rate_limit"));
5301 }
5302
5303 #[tokio::test]
5304 async fn extra_route_limiter_burst_allows_initial_spike() {
5305 let app = limited_router_with_burst(1, Some(3));
5306 for i in 0..3 {
5307 let resp = app.clone().oneshot(limited_req("10.4.4.4")).await.unwrap();
5308 assert_eq!(resp.status(), StatusCode::OK, "burst request {i}");
5309 }
5310 let resp = app.clone().oneshot(limited_req("10.4.4.4")).await.unwrap();
5311 assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
5312 }
5313
5314 #[tokio::test]
5315 async fn extra_route_limiter_deny_sets_retry_after() {
5316 let app = limited_router(1);
5317 let ok = app.clone().oneshot(limited_req("10.5.5.5")).await.unwrap();
5318 assert_eq!(ok.status(), StatusCode::OK);
5319 let denied = app.clone().oneshot(limited_req("10.5.5.5")).await.unwrap();
5320 assert_eq!(denied.status(), StatusCode::TOO_MANY_REQUESTS);
5321 let retry_after = denied
5322 .headers()
5323 .get(header::RETRY_AFTER)
5324 .expect("Retry-After present")
5325 .to_str()
5326 .unwrap()
5327 .parse::<u64>()
5328 .unwrap();
5329 assert!(retry_after >= 1, "delta-seconds must be >= 1");
5330 }
5331
5332 #[test]
5333 fn validate_rejects_zero_burst_knobs() {
5334 let err = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0")
5335 .with_tool_rate_limit(10)
5336 .with_tool_rate_limit_burst(0)
5337 .validate()
5338 .expect_err("zero tool burst");
5339 assert!(err.to_string().contains("tool_rate_limit_burst"));
5340
5341 let err = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0")
5342 .with_extra_route_rate_limit(10)
5343 .with_extra_route_rate_limit_burst(0)
5344 .validate()
5345 .expect_err("zero extra route burst");
5346 assert!(err.to_string().contains("extra_route_rate_limit_burst"));
5347 }
5348
5349 #[test]
5350 fn validate_rejects_orphan_burst_knobs() {
5351 let err = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0")
5352 .with_tool_rate_limit_burst(5)
5353 .validate()
5354 .expect_err("orphan tool burst");
5355 assert!(err.to_string().contains("requires tool_rate_limit"));
5356
5357 let err = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0")
5358 .with_extra_route_rate_limit_burst(5)
5359 .validate()
5360 .expect_err("orphan extra route burst");
5361 assert!(err.to_string().contains("requires extra_route_rate_limit"));
5362 }
5363
5364 #[test]
5365 fn validate_rejects_zero_auth_bursts() {
5366 let auth = AuthConfig::with_keys(vec![])
5367 .with_rate_limit(crate::auth::RateLimitConfig::new(10).with_burst(0));
5368 let err = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0")
5369 .with_auth(auth)
5370 .validate()
5371 .expect_err("zero auth burst");
5372 assert!(err.to_string().contains("rate_limit.burst"));
5373
5374 let auth = AuthConfig::with_keys(vec![])
5375 .with_rate_limit(crate::auth::RateLimitConfig::new(10).with_pre_auth_burst(0));
5376 let err = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0")
5377 .with_auth(auth)
5378 .validate()
5379 .expect_err("zero pre-auth burst");
5380 assert!(err.to_string().contains("pre_auth_burst"));
5381 }
5382
5383 #[test]
5384 fn validate_rejects_zero_pre_auth_max_per_minute() {
5385 let auth = AuthConfig::with_keys(vec![])
5386 .with_rate_limit(crate::auth::RateLimitConfig::new(10).with_pre_auth_max_per_minute(0));
5387 let err = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0")
5388 .with_auth(auth)
5389 .validate()
5390 .expect_err("zero pre-auth rate");
5391 assert!(err.to_string().contains("pre_auth_max_per_minute"));
5392 }
5393
5394 fn valid_mtls_config() -> MtlsConfig {
5395 MtlsConfig {
5396 ca_cert_path: "memory://ca.pem".into(),
5397 required: true,
5398 default_role: "viewer".into(),
5399 crl_enabled: true,
5400 crl_refresh_interval: None,
5401 crl_fetch_timeout: Duration::from_secs(30),
5402 crl_stale_grace: Duration::from_secs(24 * 60 * 60),
5403 crl_deny_on_unavailable: false,
5404 crl_end_entity_only: false,
5405 crl_allow_http: true,
5406 crl_enforce_expiration: true,
5407 crl_max_concurrent_fetches: 4,
5408 crl_max_response_bytes: 5 * 1024 * 1024,
5409 crl_discovery_rate_per_min: 60,
5410 crl_max_host_semaphores: 1024,
5411 crl_max_seen_urls: 4096,
5412 crl_max_cache_entries: 1024,
5413 }
5414 }
5415
5416 #[test]
5417 fn validate_rejects_zero_crl_max_response_bytes() {
5418 let mut mtls = valid_mtls_config();
5419 mtls.crl_max_response_bytes = 0;
5420 let mut auth = AuthConfig::with_keys(vec![]);
5421 auth.mtls = Some(mtls);
5422
5423 let mut cfg = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0").with_auth(auth);
5426 cfg.tls_cert_path = Some("cert.pem".into());
5427 cfg.tls_key_path = Some("key.pem".into());
5428
5429 let err = cfg.validate().expect_err("zero CRL response cap");
5430 assert!(err.to_string().contains("crl_max_response_bytes"));
5431 }
5432
5433 #[test]
5436 fn validate_accepts_pre_auth_burst_without_explicit_pre_auth_rate() {
5437 let auth = AuthConfig::with_keys(vec![])
5438 .with_rate_limit(crate::auth::RateLimitConfig::new(10).with_pre_auth_burst(50));
5439 let cfg = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0").with_auth(auth);
5440 assert!(cfg.validate().is_ok(), "pre_auth_burst has no orphan rule");
5441 }
5442
5443 #[test]
5446 fn trusted_forwarder_max_entries_bounds_are_enforced() {
5447 let cfg = |n: usize| {
5448 McpServerConfig::new("127.0.0.1:8080", "t", "0")
5449 .with_trusted_forwarder_max_entries(n)
5450 .validate()
5451 };
5452 assert!(cfg(0).is_err(), "0 would pin every client to the proxy");
5453 assert!(
5454 cfg(crate::forwarded::MAX_CONFIGURABLE_SCANNED_ENTRIES + 1).is_err(),
5455 "above the ceiling would re-open the header-bomb vector"
5456 );
5457 assert!(cfg(1).is_ok());
5458 assert!(cfg(crate::forwarded::MAX_SCANNED_ENTRIES).is_ok());
5459 assert!(cfg(crate::forwarded::MAX_CONFIGURABLE_SCANNED_ENTRIES).is_ok());
5460 }
5461
5462 #[test]
5463 fn trusted_forwarder_max_entries_defaults_to_the_module_constant() {
5464 let cfg = McpServerConfig::new("127.0.0.1:8080", "t", "0");
5465 assert_eq!(
5466 cfg.trusted_forwarder_max_entries,
5467 crate::forwarded::MAX_SCANNED_ENTRIES
5468 );
5469 }
5470
5471 fn forward_resolver(trusted: &[&str], mode: ForwardedHeaderMode) -> Arc<ForwardResolver> {
5472 Arc::new(ForwardResolver {
5473 trusted: trusted.iter().map(|s| s.parse().unwrap()).collect(),
5474 mode,
5475 max_scanned_entries: crate::forwarded::MAX_SCANNED_ENTRIES,
5476 })
5477 }
5478
5479 fn forwarded_probe_router(resolver: Option<Arc<ForwardResolver>>) -> axum::Router {
5481 async fn probe(req: Request<Body>) -> String {
5482 let pa = req
5483 .extensions()
5484 .get::<PeerAddr>()
5485 .map(|p| p.addr.ip().to_string())
5486 .unwrap_or_default();
5487 let ci = req
5488 .extensions()
5489 .get::<ClientIp>()
5490 .map(|c| c.ip.to_string())
5491 .unwrap_or_default();
5492 format!("{pa}|{ci}")
5493 }
5494 axum::Router::new()
5495 .route("/probe", axum::routing::get(probe))
5496 .layer(axum::middleware::from_fn(move |req, next| {
5497 let r = resolver.clone();
5498 normalize_peer_addr_middleware(r, req, next)
5499 }))
5500 }
5501
5502 fn probe_req(peer: &str, header: Option<(&str, &str)>) -> Request<Body> {
5503 let addr: SocketAddr = peer.parse().unwrap();
5504 let mut builder = Request::builder()
5505 .uri("/probe")
5506 .extension(ConnectInfo(addr));
5507 if let Some((name, value)) = header {
5508 builder = builder.header(name, value);
5509 }
5510 builder.body(Body::empty()).unwrap()
5511 }
5512
5513 #[tokio::test]
5514 async fn client_ip_equals_direct_without_resolver() {
5515 let app = forwarded_probe_router(None);
5516 let resp = app
5517 .oneshot(probe_req(
5518 "10.1.2.3:4444",
5519 Some(("x-forwarded-for", "203.0.113.7")),
5520 ))
5521 .await
5522 .unwrap();
5523 assert_eq!(
5524 body_string(resp).await,
5525 "10.1.2.3|10.1.2.3",
5526 "feature off: header ignored, ClientIp == direct"
5527 );
5528 }
5529
5530 #[tokio::test]
5531 async fn client_ip_resolved_for_trusted_peer() {
5532 let app = forwarded_probe_router(Some(forward_resolver(
5533 &["10.0.0.0/8"],
5534 ForwardedHeaderMode::XForwardedFor,
5535 )));
5536 let resp = app
5537 .oneshot(probe_req(
5538 "10.0.0.1:9999",
5539 Some(("x-forwarded-for", "203.0.113.7")),
5540 ))
5541 .await
5542 .unwrap();
5543 assert_eq!(
5544 body_string(resp).await,
5545 "10.0.0.1|203.0.113.7",
5546 "PeerAddr stays direct while ClientIp resolves"
5547 );
5548 }
5549
5550 #[tokio::test]
5551 async fn client_ip_falls_back_to_direct_on_malformed_header() {
5552 let app = forwarded_probe_router(Some(forward_resolver(
5553 &["10.0.0.0/8"],
5554 ForwardedHeaderMode::XForwardedFor,
5555 )));
5556 let resp = app
5557 .oneshot(probe_req(
5558 "10.0.0.1:9999",
5559 Some(("x-forwarded-for", "not-an-ip")),
5560 ))
5561 .await
5562 .unwrap();
5563 assert_eq!(
5564 body_string(resp).await,
5565 "10.0.0.1|10.0.0.1",
5566 "malformed chain falls back to the direct peer"
5567 );
5568 }
5569
5570 #[test]
5571 fn forwarded_header_mode_deserializes_kebab_case() {
5572 #[derive(serde::Deserialize)]
5573 struct Wrapper {
5574 mode: ForwardedHeaderMode,
5575 }
5576 let w: Wrapper = toml::from_str(r#"mode = "x-forwarded-for""#).unwrap();
5577 assert_eq!(w.mode, ForwardedHeaderMode::XForwardedFor);
5578 let w: Wrapper = toml::from_str(r#"mode = "forwarded""#).unwrap();
5579 assert_eq!(w.mode, ForwardedHeaderMode::Forwarded);
5580 assert!(
5581 toml::from_str::<Wrapper>(r#"mode = "XForwardedFor""#).is_err(),
5582 "PascalCase wire value must be rejected"
5583 );
5584 }
5585
5586 #[test]
5587 fn validate_rejects_bad_trusted_proxy_entry() {
5588 let cfg = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0")
5589 .with_trusted_proxies(["not-a-cidr"]);
5590 let err = cfg.validate().expect_err("bad CIDR");
5591 assert!(err.to_string().contains("trusted_proxies"));
5592 }
5593
5594 #[test]
5595 fn validate_rejects_zero_prefix_trusted_proxy() {
5596 for entry in ["0.0.0.0/0", "::/0"] {
5597 let cfg =
5598 McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0").with_trusted_proxies([entry]);
5599 let err = cfg.validate().expect_err("zero-prefix CIDR");
5600 assert!(
5601 err.to_string().contains("prefix length 0"),
5602 "entry {entry}: {err}"
5603 );
5604 }
5605 }
5606
5607 #[test]
5608 fn validate_accepts_cidr_and_bare_ip_proxy_entries() {
5609 let cfg = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0").with_trusted_proxies([
5610 "10.0.0.0/8",
5611 "192.0.2.1",
5612 "2001:db8::1",
5613 ]);
5614 assert!(cfg.validate().is_ok(), "CIDRs and bare IPs are accepted");
5615 }
5616
5617 #[test]
5618 fn validate_rejects_forwarded_header_without_proxies() {
5619 let cfg = McpServerConfig::new("127.0.0.1:8080", "t", "1.0.0")
5620 .with_forwarded_header(ForwardedHeaderMode::Forwarded);
5621 let err = cfg.validate().expect_err("mode without proxies");
5622 assert!(err.to_string().contains("requires trusted_proxies"));
5623 }
5624
5625 fn origin_router(origins: Vec<String>, log_request_headers: bool) -> axum::Router {
5629 let allowed: Arc<[String]> = Arc::from(origins);
5630 axum::Router::new()
5631 .route("/test", axum::routing::get(|| async { "ok" }))
5632 .layer(axum::middleware::from_fn(move |req, next| {
5633 let a = Arc::clone(&allowed);
5634 origin_check_middleware(a, log_request_headers, req, next)
5635 }))
5636 }
5637
5638 #[tokio::test]
5639 async fn origin_allowed_passes() {
5640 let app = origin_router(vec!["http://localhost:3000".into()], false);
5641 let req = Request::builder()
5642 .uri("/test")
5643 .header(header::ORIGIN, "http://localhost:3000")
5644 .body(Body::empty())
5645 .unwrap();
5646 let resp = app.oneshot(req).await.unwrap();
5647 assert_eq!(resp.status(), StatusCode::OK);
5648 }
5649
5650 #[tokio::test]
5651 async fn origin_rejected_returns_403() {
5652 let app = origin_router(vec!["http://localhost:3000".into()], false);
5653 let req = Request::builder()
5654 .uri("/test")
5655 .header(header::ORIGIN, "http://evil.com")
5656 .body(Body::empty())
5657 .unwrap();
5658 let resp = app.oneshot(req).await.unwrap();
5659 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
5660 }
5661
5662 #[tokio::test]
5663 async fn no_origin_header_passes() {
5664 let app = origin_router(vec!["http://localhost:3000".into()], false);
5665 let req = Request::builder().uri("/test").body(Body::empty()).unwrap();
5666 let resp = app.oneshot(req).await.unwrap();
5667 assert_eq!(resp.status(), StatusCode::OK);
5668 }
5669
5670 #[tokio::test]
5671 async fn empty_allowlist_rejects_any_origin() {
5672 let app = origin_router(vec![], false);
5673 let req = Request::builder()
5674 .uri("/test")
5675 .header(header::ORIGIN, "http://anything.com")
5676 .body(Body::empty())
5677 .unwrap();
5678 let resp = app.oneshot(req).await.unwrap();
5679 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
5680 }
5681
5682 #[tokio::test]
5683 async fn empty_allowlist_passes_without_origin() {
5684 let app = origin_router(vec![], false);
5685 let req = Request::builder().uri("/test").body(Body::empty()).unwrap();
5686 let resp = app.oneshot(req).await.unwrap();
5687 assert_eq!(resp.status(), StatusCode::OK);
5688 }
5689
5690 #[test]
5691 fn format_request_headers_redacts_sensitive_values() {
5692 let mut headers = axum::http::HeaderMap::new();
5693 headers.insert("authorization", "Bearer secret-token".parse().unwrap());
5694 headers.insert("cookie", "sid=abc".parse().unwrap());
5695 headers.insert("x-request-id", "req-123".parse().unwrap());
5696
5697 let out = format_request_headers_for_log(&headers);
5698 assert!(out.contains("authorization: [REDACTED]"));
5699 assert!(out.contains("cookie: [REDACTED]"));
5700 assert!(out.contains("x-request-id: req-123"));
5701 assert!(!out.contains("secret-token"));
5702 }
5703
5704 #[test]
5705 fn format_request_headers_redacts_forwarding_headers() {
5706 let mut headers = axum::http::HeaderMap::new();
5707 headers.insert("forwarded", "for=203.0.113.9;by=10.1.2.3".parse().unwrap());
5708 headers.insert("x-forwarded-for", "203.0.113.9, 10.1.2.3".parse().unwrap());
5709 headers.insert("x-real-ip", "203.0.113.9".parse().unwrap());
5710 headers.insert("x-request-id", "req-123".parse().unwrap());
5711
5712 let out = format_request_headers_for_log(&headers);
5713 for name in ["forwarded", "x-forwarded-for", "x-real-ip"] {
5714 assert!(
5715 out.contains(&format!("{name}: [REDACTED]")),
5716 "{name} carries client IP / proxy topology and must not reach logs; got {out}"
5717 );
5718 }
5719 assert!(
5720 !out.contains("203.0.113.9") && !out.contains("10.1.2.3"),
5721 "no forwarded address may survive redaction; got {out}"
5722 );
5723 assert!(out.contains("x-request-id: req-123"));
5724 }
5725
5726 fn security_router(is_tls: bool) -> axum::Router {
5729 security_router_with(is_tls, SecurityHeadersConfig::default())
5730 }
5731
5732 fn security_router_with(is_tls: bool, cfg: SecurityHeadersConfig) -> axum::Router {
5733 let cfg = Arc::new(cfg);
5734 axum::Router::new()
5735 .route("/test", axum::routing::get(|| async { "ok" }))
5736 .layer(axum::middleware::from_fn(move |req, next| {
5737 let c = Arc::clone(&cfg);
5738 security_headers_middleware(is_tls, c, req, next)
5739 }))
5740 }
5741
5742 #[tokio::test]
5743 async fn security_headers_set_on_response() {
5744 let app = security_router(false);
5745 let req = Request::builder().uri("/test").body(Body::empty()).unwrap();
5746 let resp = app.oneshot(req).await.unwrap();
5747 assert_eq!(resp.status(), StatusCode::OK);
5748
5749 let h = resp.headers();
5750 assert_eq!(h.get("x-content-type-options").unwrap(), "nosniff");
5751 assert_eq!(h.get("x-frame-options").unwrap(), "deny");
5752 assert_eq!(h.get("cache-control").unwrap(), "no-store, max-age=0");
5753 assert_eq!(h.get("referrer-policy").unwrap(), "no-referrer");
5754 assert_eq!(h.get("cross-origin-opener-policy").unwrap(), "same-origin");
5755 assert_eq!(
5756 h.get("cross-origin-resource-policy").unwrap(),
5757 "same-origin"
5758 );
5759 assert_eq!(
5760 h.get("cross-origin-embedder-policy").unwrap(),
5761 "require-corp"
5762 );
5763 assert_eq!(h.get("x-permitted-cross-domain-policies").unwrap(), "none");
5764 assert!(
5765 h.get("permissions-policy")
5766 .unwrap()
5767 .to_str()
5768 .unwrap()
5769 .contains("camera=()"),
5770 "permissions-policy must restrict browser features"
5771 );
5772 assert_eq!(
5773 h.get("content-security-policy").unwrap(),
5774 "default-src 'none'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests"
5775 );
5776 assert_eq!(h.get("x-dns-prefetch-control").unwrap(), "off");
5777 assert!(h.get("strict-transport-security").is_none());
5779 }
5780
5781 #[tokio::test]
5782 async fn hsts_set_when_tls_enabled() {
5783 let app = security_router(true);
5784 let req = Request::builder().uri("/test").body(Body::empty()).unwrap();
5785 let resp = app.oneshot(req).await.unwrap();
5786
5787 let hsts = resp.headers().get("strict-transport-security").unwrap();
5788 assert!(
5789 hsts.to_str().unwrap().contains("max-age=63072000"),
5790 "HSTS must set 2-year max-age"
5791 );
5792 }
5793
5794 #[tokio::test]
5795 async fn default_csp_matches_guideline() {
5796 let app = security_router(false);
5797 let req = Request::builder().uri("/test").body(Body::empty()).unwrap();
5798 let resp = app.oneshot(req).await.unwrap();
5799 assert_eq!(
5800 resp.headers().get("content-security-policy").unwrap(),
5801 "default-src 'none'; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests"
5802 );
5803 }
5804
5805 #[tokio::test]
5806 async fn operator_csp_override_still_wins() {
5807 let cfg = SecurityHeadersConfig {
5808 content_security_policy: Some("default-src 'self'".into()),
5809 ..SecurityHeadersConfig::default()
5810 };
5811 let app = security_router_with(false, cfg);
5812 let req = Request::builder().uri("/test").body(Body::empty()).unwrap();
5813 let resp = app.oneshot(req).await.unwrap();
5814 assert_eq!(
5815 resp.headers().get("content-security-policy").unwrap(),
5816 "default-src 'self'"
5817 );
5818 }
5819
5820 fn check_with_security_headers(
5826 headers: SecurityHeadersConfig,
5827 ) -> Result<(), RmcpServerKitError> {
5828 let cfg =
5829 McpServerConfig::new("127.0.0.1:8080", "test", "0.0.0").with_security_headers(headers);
5830 cfg.check()
5831 }
5832
5833 #[test]
5834 fn security_headers_config_default_validates() {
5835 check_with_security_headers(SecurityHeadersConfig::default())
5836 .expect("default SecurityHeadersConfig must validate");
5837 }
5838
5839 #[test]
5840 fn security_headers_config_validate_accepts_empty_string() {
5841 let h = SecurityHeadersConfig {
5843 x_content_type_options: Some(String::new()),
5844 x_frame_options: Some(String::new()),
5845 cache_control: Some(String::new()),
5846 referrer_policy: Some(String::new()),
5847 cross_origin_opener_policy: Some(String::new()),
5848 cross_origin_resource_policy: Some(String::new()),
5849 cross_origin_embedder_policy: Some(String::new()),
5850 permissions_policy: Some(String::new()),
5851 x_permitted_cross_domain_policies: Some(String::new()),
5852 content_security_policy: Some(String::new()),
5853 x_dns_prefetch_control: Some(String::new()),
5854 strict_transport_security: Some(String::new()),
5855 };
5856 check_with_security_headers(h).expect("Some(\"\") on every field must validate (omit-all)");
5857 }
5858
5859 #[test]
5860 fn security_headers_config_validate_rejects_bad_value() {
5861 let h = SecurityHeadersConfig {
5863 referrer_policy: Some("\u{0007}".into()),
5864 ..SecurityHeadersConfig::default()
5865 };
5866 let err = check_with_security_headers(h)
5867 .expect_err("control char in referrer_policy must reject");
5868 let msg = err.to_string();
5869 assert!(
5870 msg.contains("referrer_policy"),
5871 "error must name the offending field, got: {msg}"
5872 );
5873 }
5874
5875 #[test]
5876 fn security_headers_config_validate_rejects_hsts_preload() {
5877 let h = SecurityHeadersConfig {
5878 strict_transport_security: Some("max-age=63072000; includeSubDomains; preload".into()),
5879 ..SecurityHeadersConfig::default()
5880 };
5881 let err = check_with_security_headers(h).expect_err("HSTS with preload must reject");
5882 let msg = err.to_string();
5883 assert!(
5884 msg.contains("strict_transport_security"),
5885 "error must name the field, got: {msg}"
5886 );
5887 assert!(
5888 msg.to_lowercase().contains("preload"),
5889 "error must mention `preload`, got: {msg}"
5890 );
5891 }
5892
5893 #[test]
5894 fn security_headers_config_validate_rejects_hsts_preload_uppercase() {
5895 let h = SecurityHeadersConfig {
5897 strict_transport_security: Some("max-age=600; PRELOAD".into()),
5898 ..SecurityHeadersConfig::default()
5899 };
5900 check_with_security_headers(h).expect_err("HSTS preload check must be case-insensitive");
5901 }
5902
5903 #[tokio::test]
5904 async fn security_headers_override_honored() {
5905 let h = SecurityHeadersConfig {
5907 x_frame_options: Some("SAMEORIGIN".into()),
5908 ..SecurityHeadersConfig::default()
5909 };
5910 let app = security_router_with(false, h);
5911 let req = Request::builder().uri("/test").body(Body::empty()).unwrap();
5912 let resp = app.oneshot(req).await.unwrap();
5913 assert_eq!(resp.status(), StatusCode::OK);
5914
5915 let xfo = resp.headers().get("x-frame-options").unwrap();
5916 assert_eq!(xfo, "SAMEORIGIN");
5917 }
5918
5919 #[tokio::test]
5920 async fn security_headers_empty_string_omits() {
5921 let h = SecurityHeadersConfig {
5923 referrer_policy: Some(String::new()),
5924 ..SecurityHeadersConfig::default()
5925 };
5926 let app = security_router_with(false, h);
5927 let req = Request::builder().uri("/test").body(Body::empty()).unwrap();
5928 let resp = app.oneshot(req).await.unwrap();
5929 assert_eq!(resp.status(), StatusCode::OK);
5930
5931 assert!(
5932 resp.headers().get("referrer-policy").is_none(),
5933 "Some(\"\") must omit the header"
5934 );
5935 assert_eq!(
5937 resp.headers().get("x-content-type-options").unwrap(),
5938 "nosniff"
5939 );
5940 }
5941
5942 #[tokio::test]
5943 async fn security_headers_hsts_only_when_tls() {
5944 let h = SecurityHeadersConfig {
5946 strict_transport_security: Some("max-age=600".into()),
5947 ..SecurityHeadersConfig::default()
5948 };
5949 let app = security_router_with(false, h);
5950 let req = Request::builder().uri("/test").body(Body::empty()).unwrap();
5951 let resp = app.oneshot(req).await.unwrap();
5952 assert!(
5953 resp.headers().get("strict-transport-security").is_none(),
5954 "HSTS must remain absent on plaintext deployments even with override"
5955 );
5956 }
5957
5958 #[cfg(feature = "oauth")]
5961 #[tokio::test]
5962 async fn oauth_token_cache_headers_set_pragma_and_vary() {
5963 let app = axum::Router::new()
5964 .route("/token", axum::routing::post(|| async { "{}" }))
5965 .layer(axum::middleware::from_fn(
5966 oauth_token_cache_headers_middleware,
5967 ));
5968 let req = Request::builder()
5969 .method("POST")
5970 .uri("/token")
5971 .body(Body::from("{}"))
5972 .unwrap();
5973 let resp = app.oneshot(req).await.unwrap();
5974 assert_eq!(resp.status(), StatusCode::OK);
5975
5976 let h = resp.headers();
5977 assert_eq!(
5978 h.get("pragma").unwrap(),
5979 "no-cache",
5980 "RFC 6749 §5.1: token responses must set Pragma: no-cache"
5981 );
5982 let vary_values: Vec<String> = h
5983 .get_all("vary")
5984 .iter()
5985 .filter_map(|v| v.to_str().ok().map(str::to_owned))
5986 .collect();
5987 assert!(
5988 vary_values
5989 .iter()
5990 .any(|v| v.eq_ignore_ascii_case("Authorization")),
5991 "RFC 6750 §5.4: Vary must include Authorization, got {vary_values:?}"
5992 );
5993 }
5994
5995 #[cfg(feature = "oauth")]
5996 #[tokio::test]
5997 async fn oauth_token_cache_headers_preserve_existing_vary() {
5998 let app = axum::Router::new()
6001 .route(
6002 "/token",
6003 axum::routing::post(|| async {
6004 axum::response::Response::builder()
6005 .header("vary", "Accept-Encoding")
6006 .body(Body::from("{}"))
6007 .unwrap()
6008 }),
6009 )
6010 .layer(axum::middleware::from_fn(
6011 oauth_token_cache_headers_middleware,
6012 ));
6013 let req = Request::builder()
6014 .method("POST")
6015 .uri("/token")
6016 .body(Body::empty())
6017 .unwrap();
6018 let resp = app.oneshot(req).await.unwrap();
6019
6020 let vary: Vec<String> = resp
6021 .headers()
6022 .get_all("vary")
6023 .iter()
6024 .filter_map(|v| v.to_str().ok().map(str::to_owned))
6025 .collect();
6026 assert!(
6027 vary.iter().any(|v| v.contains("Accept-Encoding")),
6028 "must preserve pre-existing Vary value, got {vary:?}"
6029 );
6030 assert!(
6031 vary.iter().any(|v| v.contains("Authorization")),
6032 "must append Authorization to Vary, got {vary:?}"
6033 );
6034 }
6035
6036 #[test]
6039 fn version_omits_build_fingerprint_by_default() {
6040 let v = version_payload("my-server", "1.2.3", false);
6041 assert_eq!(v["name"], "my-server");
6042 assert_eq!(v["version"], "1.2.3");
6043 assert!(v["rmcp_server_kit_version"].is_string());
6044 assert!(
6045 v.get("build_git_sha").is_none(),
6046 "build sha must be hidden by default"
6047 );
6048 assert!(v.get("build_timestamp").is_none());
6049 assert!(v.get("rust_version").is_none());
6050 }
6051
6052 #[test]
6053 fn version_exposes_all_when_enabled() {
6054 let v = version_payload("my-server", "1.2.3", true);
6055 assert!(v["build_git_sha"].is_string());
6056 assert!(v["build_timestamp"].is_string());
6057 assert!(v["rust_version"].is_string());
6058 assert!(v["rmcp_server_kit_version"].is_string());
6059 }
6060
6061 #[tokio::test]
6064 async fn concurrency_limit_layer_composes_and_serves() {
6065 let app = axum::Router::new()
6069 .route("/ok", axum::routing::get(|| async { "ok" }))
6070 .layer(
6071 tower::ServiceBuilder::new()
6072 .layer(axum::error_handling::HandleErrorLayer::new(
6073 |_err: tower::BoxError| async { StatusCode::SERVICE_UNAVAILABLE },
6074 ))
6075 .layer(tower::load_shed::LoadShedLayer::new())
6076 .layer(tower::limit::ConcurrencyLimitLayer::new(4)),
6077 );
6078 let resp = app
6079 .oneshot(Request::builder().uri("/ok").body(Body::empty()).unwrap())
6080 .await
6081 .unwrap();
6082 assert_eq!(resp.status(), StatusCode::OK);
6083 }
6084
6085 #[tokio::test]
6088 async fn compression_layer_gzip_encodes_response() {
6089 use tower_http::compression::Predicate as _;
6090
6091 let big_body = "a".repeat(4096);
6092 let app = axum::Router::new()
6093 .route(
6094 "/big",
6095 axum::routing::get(move || {
6096 let body = big_body.clone();
6097 async move { body }
6098 }),
6099 )
6100 .layer(
6101 tower_http::compression::CompressionLayer::new()
6102 .gzip(true)
6103 .br(true)
6104 .compress_when(
6105 tower_http::compression::DefaultPredicate::new()
6106 .and(tower_http::compression::predicate::SizeAbove::new(1024)),
6107 ),
6108 );
6109
6110 let req = Request::builder()
6111 .uri("/big")
6112 .header(header::ACCEPT_ENCODING, "gzip")
6113 .body(Body::empty())
6114 .unwrap();
6115 let resp = app.oneshot(req).await.unwrap();
6116 assert_eq!(resp.status(), StatusCode::OK);
6117 assert_eq!(
6118 resp.headers().get(header::CONTENT_ENCODING).unwrap(),
6119 "gzip"
6120 );
6121 }
6122
6123 #[tokio::test]
6126 async fn tls_handshake_timeout_reaps_idle_connections() {
6127 use tokio::io::AsyncReadExt as _;
6128
6129 let _ = rustls::crypto::ring::default_provider().install_default();
6130
6131 let key = rcgen::KeyPair::generate().expect("generate key");
6133 let cert = rcgen::CertificateParams::new(vec!["localhost".to_owned()])
6134 .expect("cert params")
6135 .self_signed(&key)
6136 .expect("self-signed cert");
6137 let dir = std::env::temp_dir().join(format!(
6138 "rmcp-server-kit-hs-timeout-{}",
6139 std::time::SystemTime::now()
6140 .duration_since(std::time::UNIX_EPOCH)
6141 .expect("clock after epoch")
6142 .as_nanos()
6143 ));
6144 tokio::fs::create_dir_all(&dir).await.expect("temp dir");
6145 let cert_path = dir.join("server.crt");
6146 let key_path = dir.join("server.key");
6147 tokio::fs::write(&cert_path, cert.pem())
6148 .await
6149 .expect("write cert");
6150 tokio::fs::write(&key_path, key.serialize_pem())
6151 .await
6152 .expect("write key");
6153
6154 let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
6155 let tls = TlsListener::new(
6156 listener,
6157 &cert_path,
6158 &key_path,
6159 None,
6160 None,
6161 Duration::from_millis(200),
6162 8, )
6164 .expect("tls listener");
6165 let addr = axum::serve::Listener::local_addr(&tls).expect("local addr");
6166
6167 let mut idle = tokio::net::TcpStream::connect(addr).await.expect("connect");
6171 let mut buf = [0_u8; 16];
6172 let read = tokio::time::timeout(Duration::from_secs(2), idle.read(&mut buf))
6173 .await
6174 .expect("server must reap the idle handshake within its timeout");
6175 match read {
6176 Ok(0) | Err(_) => {} Ok(n) => panic!("unexpected {n} bytes from server during reaped handshake"),
6178 }
6179
6180 drop(tls);
6181 }
6182
6183 fn assert_owasp_headers(resp: &axum::response::Response, ctx: &str) {
6186 let h = resp.headers();
6187 assert!(
6188 h.contains_key("x-content-type-options"),
6189 "{ctx}: missing X-Content-Type-Options"
6190 );
6191 assert!(
6192 h.contains_key("x-frame-options"),
6193 "{ctx}: missing X-Frame-Options"
6194 );
6195 assert!(
6196 h.contains_key("strict-transport-security"),
6197 "{ctx}: missing Strict-Transport-Security"
6198 );
6199 assert!(
6200 h.contains_key(header::CONTENT_SECURITY_POLICY),
6201 "{ctx}: missing Content-Security-Policy"
6202 );
6203 }
6204
6205 fn m5_router(configure: impl FnOnce(&mut McpServerConfig)) -> axum::Router {
6206 #[derive(Clone)]
6207 struct H;
6208 impl ServerHandler for H {}
6209 let mut config = McpServerConfig::new("127.0.0.1:8080", "test", "0.0.0")
6213 .with_allowed_origins(["http://good.example"])
6214 .with_tls("unused.crt", "unused.key");
6215 configure(&mut config);
6216 let (router, _params) = build_app_router(config, || H).expect("build_app_router");
6217 router
6218 }
6219
6220 #[test]
6225 #[should_panic(expected = "Overlapping method route")]
6226 fn extra_router_exact_overlap_with_framework_route_panics() {
6227 #[derive(Clone)]
6228 struct H;
6229 impl ServerHandler for H {}
6230 let config = McpServerConfig::new("127.0.0.1:8080", "test", "0.0.0").with_extra_router(
6231 axum::Router::new().route("/healthz", axum::routing::get(|| async { "mine" })),
6232 );
6233 let _ = build_app_router(config, || H);
6234 }
6235
6236 #[test]
6240 fn extra_router_non_overlapping_path_under_framework_prefix_is_accepted() {
6241 #[derive(Clone)]
6242 struct H;
6243 impl ServerHandler for H {}
6244 let config = McpServerConfig::new("127.0.0.1:8080", "test", "0.0.0").with_extra_router(
6245 axum::Router::new().route("/admin/custom", axum::routing::get(|| async { "mine" })),
6246 );
6247 assert!(
6248 build_app_router(config, || H).is_ok(),
6249 "non-overlapping path under a framework prefix must merge cleanly"
6250 );
6251 }
6252
6253 #[tokio::test]
6254 async fn headers_on_rejected_origin_403() {
6255 let app = m5_router(|_| {});
6256 let req = Request::builder()
6257 .uri("/healthz")
6258 .header(header::ORIGIN, "http://evil.example")
6259 .body(Body::empty())
6260 .unwrap();
6261 let resp = app.oneshot(req).await.unwrap();
6262 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
6263 assert_owasp_headers(&resp, "origin-403");
6264 }
6265
6266 #[tokio::test]
6267 async fn headers_on_cors_preflight() {
6268 let app = m5_router(|_| {});
6269 let req = Request::builder()
6270 .method(axum::http::Method::OPTIONS)
6271 .uri("/mcp")
6272 .header(header::ORIGIN, "http://good.example")
6273 .header(header::ACCESS_CONTROL_REQUEST_METHOD, "POST")
6274 .body(Body::empty())
6275 .unwrap();
6276 let resp = app.oneshot(req).await.unwrap();
6277 assert_owasp_headers(&resp, "cors-preflight");
6278 }
6279
6280 #[tokio::test]
6281 async fn headers_on_404_fallback() {
6282 let app = m5_router(|_| {});
6283 let req = Request::builder()
6284 .uri("/no-such-route")
6285 .body(Body::empty())
6286 .unwrap();
6287 let resp = app.oneshot(req).await.unwrap();
6288 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
6289 assert_owasp_headers(&resp, "404-fallback");
6290 }
6291
6292 #[tokio::test]
6293 async fn headers_on_overload_503() {
6294 let app = m5_router(|c| c.max_concurrent_requests = Some(0));
6297 let req = Request::builder()
6298 .uri("/healthz")
6299 .body(Body::empty())
6300 .unwrap();
6301 let resp = app.oneshot(req).await.unwrap();
6302 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
6303 assert_owasp_headers(&resp, "overload-503");
6304 }
6305
6306 #[cfg(feature = "oauth")]
6309 fn m6_auth_state() -> (Arc<AuthState>, String, String) {
6310 let (admin_token, admin_hash) = crate::auth::generate_api_key().unwrap();
6311 let (viewer_token, viewer_hash) = crate::auth::generate_api_key().unwrap();
6312 let state = Arc::new(AuthState {
6313 api_keys: ArcSwap::from_pointee(vec![
6314 crate::auth::ApiKeyEntry::new("admin-key", admin_hash, "admin"),
6315 crate::auth::ApiKeyEntry::new("viewer-key", viewer_hash, "viewer"),
6316 ]),
6317 rate_limiter: None,
6318 pre_auth_limiter: None,
6319 jwks_cache: None,
6320 seen_identities: crate::auth::SeenIdentitySet::new(),
6321 counters: crate::auth::AuthCounters::default(),
6322 resource_metadata_url: None,
6323 });
6324 (state, admin_token, viewer_token)
6325 }
6326
6327 #[cfg(feature = "oauth")]
6328 fn m6_admin_router(state: &Arc<AuthState>) -> axum::Router {
6329 let proxy = crate::oauth::OAuthProxyConfig::builder(
6330 "https://idp.example/authorize",
6331 "https://idp.example/token",
6332 "client",
6333 )
6334 .introspection_url("http://127.0.0.1:1/introspect")
6335 .revocation_url("http://127.0.0.1:1/revoke")
6336 .expose_admin_endpoints(true)
6337 .require_auth_on_admin_endpoints(true)
6338 .build();
6339 let http = crate::oauth::OauthHttpClient::new().expect("oauth http client");
6340 build_oauth_admin_router(&proxy, http, Some(state), "admin").expect("admin router")
6341 }
6342
6343 #[cfg(feature = "oauth")]
6344 fn m6_req(path: &str, token: &str) -> Request<Body> {
6345 Request::builder()
6346 .method(axum::http::Method::POST)
6347 .uri(path)
6348 .header(header::AUTHORIZATION, format!("Bearer {token}"))
6349 .body(Body::from("token=abc"))
6350 .unwrap()
6351 }
6352
6353 #[cfg(feature = "oauth")]
6354 #[tokio::test]
6355 async fn oauth_proxy_admin_requires_admin_role() {
6356 let (state, _admin, viewer) = m6_auth_state();
6357 for path in ["/introspect", "/revoke"] {
6358 let app = m6_admin_router(&state);
6359 let resp = app.oneshot(m6_req(path, &viewer)).await.unwrap();
6360 assert_eq!(
6361 resp.status(),
6362 StatusCode::FORBIDDEN,
6363 "an authenticated viewer must be rejected with 403 on {path}"
6364 );
6365 }
6366 }
6367
6368 #[cfg(feature = "oauth")]
6369 #[tokio::test]
6370 async fn oauth_proxy_admin_allows_admin_role() {
6371 let (state, admin, _viewer) = m6_auth_state();
6372 for path in ["/introspect", "/revoke"] {
6373 let app = m6_admin_router(&state);
6374 let resp = app.oneshot(m6_req(path, &admin)).await.unwrap();
6375 assert_ne!(
6379 resp.status(),
6380 StatusCode::FORBIDDEN,
6381 "an authenticated admin must pass the role gate on {path}"
6382 );
6383 assert_ne!(
6384 resp.status(),
6385 StatusCode::UNAUTHORIZED,
6386 "an authenticated admin must pass the auth gate on {path}"
6387 );
6388 }
6389 }
6390
6391 #[cfg(feature = "metrics")]
6399 mod metrics_labels_bounded {
6400 use super::*;
6401
6402 fn labels_for(method: &str, uri: &str) -> (&'static str, String) {
6403 let req = Request::builder()
6404 .method(method)
6405 .uri(uri)
6406 .body(Body::empty())
6407 .unwrap();
6408 metrics_labels(&req)
6409 }
6410
6411 #[test]
6412 fn many_unmatched_paths_collapse_to_one_label() {
6413 let mut seen = std::collections::HashSet::new();
6414 for i in 0..500 {
6415 let (_, path) = labels_for("GET", &format!("/nonexistent-{i}"));
6416 seen.insert(path);
6417 }
6418 assert_eq!(
6419 seen.len(),
6420 1,
6421 "unmatched paths must collapse to a single label, got {seen:?}"
6422 );
6423 assert!(seen.contains("<unmatched>"));
6424 }
6425
6426 #[test]
6427 fn nested_mcp_paths_collapse_to_the_mount_point() {
6428 let mut seen = std::collections::HashSet::new();
6429 for i in 0..200 {
6430 let (_, path) = labels_for("POST", &format!("/mcp/{i}"));
6431 seen.insert(path);
6432 }
6433 let (_, root) = labels_for("POST", "/mcp");
6434 seen.insert(root);
6435 assert_eq!(
6436 seen.len(),
6437 1,
6438 "nested /mcp paths must collapse to one label, got {seen:?}"
6439 );
6440 assert!(seen.contains("/mcp"));
6441 }
6442
6443 #[test]
6444 fn unusual_methods_collapse_to_one_bucket() {
6445 let mut seen = std::collections::HashSet::new();
6446 for verb in ["FROBNICATE", "WIBBLE", "QUUX", "M-SEARCH"] {
6447 let (method, _) = labels_for(verb, "/healthz");
6448 seen.insert(method);
6449 }
6450 assert_eq!(seen, std::collections::HashSet::from(["OTHER"]));
6451 }
6452
6453 #[test]
6454 fn known_methods_keep_their_identity() {
6455 for verb in ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] {
6456 let (method, _) = labels_for(verb, "/healthz");
6457 assert_eq!(method, verb);
6458 }
6459 }
6460
6461 #[test]
6462 fn raw_path_never_leaks_into_a_label() {
6463 let (_, path) = labels_for("GET", "/secret-token-abc123");
6464 assert!(
6465 !path.contains("secret-token"),
6466 "raw request path must never become a label value: {path}"
6467 );
6468 }
6469 }
6470}