use super::{
error::{TransportServerError, TransportServerResult},
routes::mcp_routes,
};
use crate::AxumRuntime;
use axum::Router;
#[cfg(feature = "ssl")]
use axum_server::tls_rustls::RustlsConfig;
use axum_server::Handle;
use rust_mcp_sdk::auth::AuthProvider;
use rust_mcp_sdk::mcp_http::middleware::AuthMiddleware;
use rust_mcp_sdk::mcp_http::{
Middleware, DEFAULT_MESSAGES_ENDPOINT, DEFAULT_SSE_ENDPOINT, DEFAULT_STREAMABLE_HTTP_ENDPOINT,
};
use rust_mcp_sdk::schema::schema_utils::{ClientMessage, ServerMessage};
use rust_mcp_sdk::{
error::SdkResult,
id_generator::{FastIdGenerator, UuidGenerator},
mcp_http::{
resolve_dns_middleware, DnsRebindingOptions, HealthHandler, McpAppState, McpHttpHandler,
},
IdGenerator, McpObserver, McpServerHandler, ServerDetails,
};
use rust_mcp_sdk::{SessionId, TransportOptions};
use std::{
net::{SocketAddr, ToSocketAddrs},
path::Path,
sync::Arc,
time::Duration,
};
use tokio::signal;
const DEFAULT_CLIENT_PING_INTERVAL: Duration = Duration::from_secs(12);
const GRACEFUL_SHUTDOWN_TIMEOUT_SECS: u64 = 5;
pub use rust_mcp_sdk::mcp_http::McpMountOptions;
pub struct AxumServerOptions {
pub host: String,
pub port: u16,
pub session_id_generator: Option<Arc<dyn IdGenerator<SessionId>>>,
pub custom_streamable_http_endpoint: Option<String>,
pub transport_options: Arc<TransportOptions>,
pub enable_json_response: Option<bool>,
pub ping_interval: Duration,
pub max_request_body_size: Option<usize>,
pub enable_ssl: bool,
pub ssl_cert_path: Option<String>,
pub ssl_key_path: Option<String>,
pub dns_rebinding: DnsRebindingOptions,
pub sse_support: bool,
pub custom_sse_endpoint: Option<String>,
pub custom_messages_endpoint: Option<String>,
pub auth: Option<Arc<dyn AuthProvider>>,
pub health_endpoint: Option<String>,
pub health_handler: Option<Arc<dyn HealthHandler>>,
pub message_observer: Option<Arc<dyn McpObserver<ClientMessage, ServerMessage>>>,
pub max_listen_streams: usize,
}
impl AxumServerOptions {
pub fn validate(&self) -> TransportServerResult<()> {
if self.enable_ssl {
if self.ssl_cert_path.is_none() || self.ssl_key_path.is_none() {
return Err(TransportServerError::InvalidServerOptions(
"Both 'ssl_cert_path' and 'ssl_key_path' must be provided when SSL is enabled."
.into(),
));
}
if !Path::new(self.ssl_cert_path.as_deref().unwrap_or("")).is_file() {
return Err(TransportServerError::InvalidServerOptions(
"'ssl_cert_path' does not point to a valid or existing file.".into(),
));
}
if !Path::new(self.ssl_key_path.as_deref().unwrap_or("")).is_file() {
return Err(TransportServerError::InvalidServerOptions(
"'ssl_key_path' does not point to a valid or existing file.".into(),
));
}
}
Ok(())
}
pub(crate) async fn resolve_server_address(&self) -> TransportServerResult<SocketAddr> {
self.validate()?;
let mut host = self.host.to_string();
if let Some(stripped) = self.host.strip_prefix("http://") {
if self.enable_ssl {
tracing::warn!("Warning: Ignoring http:// scheme for SSL; using hostname only");
}
host = stripped.to_string();
} else if let Some(stripped) = host.strip_prefix("https://") {
host = stripped.to_string();
}
let addr = {
let mut iter = (host, self.port)
.to_socket_addrs()
.map_err(|err| TransportServerError::ServerStartError(err.to_string()))?;
match iter.next() {
Some(addr) => addr,
None => format!("{}:{}", self.host, self.port).parse().map_err(
|err: std::net::AddrParseError| {
TransportServerError::ServerStartError(err.to_string())
},
)?,
}
};
Ok(addr)
}
pub fn base_url(&self) -> String {
format!(
"{}://{}:{}",
if self.enable_ssl { "https" } else { "http" },
self.host,
self.port
)
}
pub fn streamable_http_url(&self) -> String {
format!("{}{}", self.base_url(), self.streamable_http_endpoint())
}
pub fn sse_url(&self) -> String {
format!("{}{}", self.base_url(), self.sse_endpoint())
}
pub fn sse_message_url(&self) -> String {
format!("{}{}", self.base_url(), self.sse_messages_endpoint())
}
pub fn sse_endpoint(&self) -> &str {
self.custom_sse_endpoint
.as_deref()
.unwrap_or(DEFAULT_SSE_ENDPOINT)
}
pub fn sse_messages_endpoint(&self) -> &str {
self.custom_messages_endpoint
.as_deref()
.unwrap_or(DEFAULT_MESSAGES_ENDPOINT)
}
pub fn streamable_http_endpoint(&self) -> &str {
self.custom_streamable_http_endpoint
.as_deref()
.unwrap_or(DEFAULT_STREAMABLE_HTTP_ENDPOINT)
}
pub fn max_request_body_size(&self) -> usize {
self.max_request_body_size
.unwrap_or(rust_mcp_sdk::mcp_http::DEFAULT_MAX_REQUEST_BODY_SIZE)
}
pub fn resolve_mount_options(&self) -> McpMountOptions {
McpMountOptions {
streamable_http_endpoint: self.streamable_http_endpoint().to_string(),
sse_endpoint: self.sse_endpoint().to_string(),
sse_messages_endpoint: self.sse_messages_endpoint().to_string(),
health_endpoint: self.health_endpoint.clone(),
max_request_body_size: self.max_request_body_size(),
}
}
}
impl Default for AxumServerOptions {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 8080,
custom_sse_endpoint: None,
custom_streamable_http_endpoint: None,
custom_messages_endpoint: None,
ping_interval: DEFAULT_CLIENT_PING_INTERVAL,
max_request_body_size: None,
transport_options: Default::default(),
enable_ssl: false,
ssl_cert_path: None,
ssl_key_path: None,
session_id_generator: None,
enable_json_response: None,
sse_support: true,
dns_rebinding: DnsRebindingOptions::default(),
auth: None,
health_endpoint: None,
health_handler: None,
message_observer: None,
max_listen_streams: rust_mcp_sdk::mcp_http::DEFAULT_MAX_LISTEN_STREAMS,
}
}
}
pub struct AxumServer {
app: Router,
state: Arc<McpAppState>,
pub(crate) options: AxumServerOptions,
handle: Handle<SocketAddr>,
}
impl AxumServer {
pub fn new(
server_details: ServerDetails,
handler: Arc<dyn McpServerHandler + 'static>,
mut server_options: AxumServerOptions,
) -> Self {
let state: Arc<McpAppState> = Arc::new(McpAppState {
id_generator: server_options
.session_id_generator
.take()
.map_or(Arc::new(UuidGenerator {}), |g| Arc::clone(&g)),
stream_id_gen: Arc::new(FastIdGenerator::new(Some("s_"))),
server_details: Arc::new(server_details),
handler,
ping_interval: server_options.ping_interval,
transport_options: Arc::clone(&server_options.transport_options),
enable_json_response: server_options.enable_json_response.unwrap_or(false),
message_observer: server_options.message_observer.take(),
extensions: Arc::new(tokio::sync::RwLock::new(None)),
active_listen_streams: Arc::new(tokio::sync::Mutex::new(Vec::new())),
max_listen_streams: server_options.max_listen_streams,
});
let mut middlewares: Vec<Arc<dyn Middleware>> = vec![];
if let Some(dns) = resolve_dns_middleware(
&mut server_options.dns_rebinding,
&server_options.host,
server_options.port,
) {
middlewares.push(Arc::new(dns));
}
let http_handler = {
let auth_provider = server_options.auth.take();
if let Some(auth_provider) = auth_provider.as_ref() {
middlewares.push(Arc::new(AuthMiddleware::new(auth_provider.clone())))
}
McpHttpHandler::new(
auth_provider,
middlewares,
server_options.health_handler.clone(),
)
};
let mount_options = server_options.resolve_mount_options();
let app = mcp_routes(Arc::clone(&state), &mount_options, http_handler);
Self {
app,
state,
options: server_options,
handle: Handle::new(),
}
}
pub fn state(&self) -> Arc<McpAppState> {
Arc::clone(&self.state)
}
pub fn with_route(mut self, path: &'static str, route: axum::routing::MethodRouter) -> Self {
self.app = self.app.route(path, route);
self
}
pub async fn server_info(&self, addr: Option<SocketAddr>) -> TransportServerResult<String> {
let addr = addr.unwrap_or(self.options.resolve_server_address().await?);
let server_type = if self.options.enable_ssl {
"SSL server"
} else {
"Server"
};
let protocol = if self.options.enable_ssl {
"https"
} else {
"http"
};
let mut server_url = format!(
"\n• Streamable HTTP {} is available at {}://{}{}",
server_type,
protocol,
addr,
self.options.streamable_http_endpoint()
);
if self.options.sse_support {
let sse_url = format!(
"\n• SSE {} is available at {}://{}{}",
server_type,
protocol,
addr,
self.options.sse_endpoint()
);
server_url.push_str(&sse_url);
};
Ok(server_url)
}
pub fn options(&self) -> &AxumServerOptions {
&self.options
}
#[cfg(feature = "ssl")]
pub(crate) async fn start_ssl(self, addr: SocketAddr) -> TransportServerResult<()> {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let config = RustlsConfig::from_pem_file(
self.options.ssl_cert_path.as_deref().unwrap_or_default(),
self.options.ssl_key_path.as_deref().unwrap_or_default(),
)
.await
.map_err(|err| TransportServerError::SslCertError(err.to_string()))?;
tracing::info!("{}", self.server_info(Some(addr)).await?);
let handle_clone = self.handle.clone();
let state_clone = self.state().clone();
tokio::spawn(async move {
shutdown_signal(handle_clone, state_clone).await;
});
let handle_clone = self.handle.clone();
axum_server::bind_rustls(addr, config)
.handle(handle_clone)
.serve(self.app.into_make_service())
.await
.map_err(|err| TransportServerError::ServerStartError(err.to_string()))
}
pub fn server_handle(&self) -> Handle<SocketAddr> {
self.handle.clone()
}
pub(crate) async fn start_http(self, addr: SocketAddr) -> TransportServerResult<()> {
tracing::info!("{}", self.server_info(Some(addr)).await?);
let handle_clone = self.handle.clone();
tokio::spawn(async move {
shutdown_signal(handle_clone, self.state.clone()).await;
});
let handle_clone = self.handle.clone();
axum_server::bind(addr)
.handle(handle_clone)
.serve(self.app.into_make_service())
.await
.map_err(|err| TransportServerError::ServerStartError(err.to_string()))
}
pub async fn start(self) -> SdkResult<()> {
let runtime = AxumRuntime::create(self).await?;
runtime.await_server().await
}
pub async fn start_runtime(self) -> SdkResult<AxumRuntime> {
AxumRuntime::create(self).await
}
}
async fn shutdown_signal(handle: Handle<SocketAddr>, state: Arc<McpAppState>) {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("Failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("Failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
tracing::info!("Signal received, starting graceful shutdown");
state.shutdown_all_listen_streams().await;
handle.graceful_shutdown(Some(Duration::from_secs(GRACEFUL_SHUTDOWN_TIMEOUT_SECS)));
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_server_options_base_url_custom() {
let options = AxumServerOptions {
host: String::from("127.0.0.1"),
port: 8081,
enable_ssl: true,
..Default::default()
};
assert_eq!(options.base_url(), "https://127.0.0.1:8081");
}
#[test]
fn test_server_options_streamable_http_custom() {
let options = AxumServerOptions {
custom_streamable_http_endpoint: Some(String::from("/abcd/mcp")),
host: String::from("127.0.0.1"),
port: 8081,
enable_ssl: true,
..Default::default()
};
assert_eq!(
options.streamable_http_url(),
"https://127.0.0.1:8081/abcd/mcp"
);
assert_eq!(options.streamable_http_endpoint(), "/abcd/mcp");
}
#[test]
fn test_server_options_sse_custom() {
let options = AxumServerOptions {
custom_sse_endpoint: Some(String::from("/abcd/sse")),
host: String::from("127.0.0.1"),
port: 8081,
enable_ssl: true,
..Default::default()
};
assert_eq!(options.sse_url(), "https://127.0.0.1:8081/abcd/sse");
assert_eq!(options.sse_endpoint(), "/abcd/sse");
}
#[test]
fn test_server_options_sse_messages_custom() {
let options = AxumServerOptions {
custom_messages_endpoint: Some(String::from("/abcd/messages")),
..Default::default()
};
assert_eq!(
options.sse_message_url(),
"http://127.0.0.1:8080/abcd/messages"
);
assert_eq!(options.sse_messages_endpoint(), "/abcd/messages");
}
#[test]
fn test_server_options_validate() {
let options = AxumServerOptions::default();
assert!(options.validate().is_ok());
let options = AxumServerOptions {
enable_ssl: true,
..Default::default()
};
assert!(options.validate().is_err());
let options = AxumServerOptions {
enable_ssl: true,
ssl_cert_path: Some(String::from("/invalid/path/to/cert.pem")),
ssl_key_path: Some(String::from("/invalid/path/to/key.pem")),
..Default::default()
};
assert!(options.validate().is_err());
let cert_file =
NamedTempFile::with_suffix(".pem").expect("Expected to create test cert file");
let ssl_cert_path = cert_file
.path()
.to_str()
.expect("Expected to get cert path")
.to_string();
let key_file =
NamedTempFile::with_suffix(".pem").expect("Expected to create test key file");
let ssl_key_path = key_file
.path()
.to_str()
.expect("Expected to get key path")
.to_string();
let options = AxumServerOptions {
enable_ssl: true,
ssl_cert_path: Some(ssl_cert_path),
ssl_key_path: Some(ssl_key_path),
..Default::default()
};
assert!(options.validate().is_ok());
}
#[tokio::test]
async fn test_server_options_resolve_server_address() {
let options = AxumServerOptions::default();
assert!(options.resolve_server_address().await.is_ok());
let options = AxumServerOptions {
host: String::from("8.6.7.5"),
port: 309,
..Default::default()
};
assert!(options.resolve_server_address().await.is_ok());
let options = AxumServerOptions {
host: String::from("http://8.6.7.5"),
port: 309,
..Default::default()
};
assert!(options.resolve_server_address().await.is_ok());
let options = AxumServerOptions {
host: String::from("invalid-host"),
port: 309,
..Default::default()
};
assert!(options.resolve_server_address().await.is_err());
}
#[cfg(feature = "ssl")]
#[test]
fn install_crypto_provider_idempotent() {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
}
}