use std::net::SocketAddr;
use rmcp::handler::server::ServerHandler;
use rmcp::transport::StreamableHttpServerConfig;
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
use rmcp::transport::streamable_http_server::tower::StreamableHttpService;
use tokio_util::sync::CancellationToken;
use crate::credentials::credential_rejection_middleware;
use crate::headers::{ForwardConfig, forward_headers_middleware};
const LEGACY_SESSION_MODE: bool = false;
pub struct ServeOptions {
pub bind: SocketAddr,
pub forward: ForwardConfig,
pub allowed_hosts: Vec<String>,
pub allowed_origins: Vec<String>,
}
impl ServeOptions {
pub fn new(bind: SocketAddr) -> Self {
Self {
bind,
forward: ForwardConfig::authorization_only(),
allowed_hosts: Vec::new(),
allowed_origins: Vec::new(),
}
}
pub fn with_forward(mut self, forward: ForwardConfig) -> Self {
self.forward = forward;
self
}
pub fn with_allowed_hosts<I, S>(mut self, hosts: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.allowed_hosts = hosts.into_iter().map(Into::into).collect();
self
}
pub fn with_allowed_origins<I, S>(mut self, origins: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.allowed_origins = origins.into_iter().map(Into::into).collect();
self
}
}
pub async fn serve<H, F>(options: ServeOptions, make_handler: F) -> anyhow::Result<()>
where
H: ServerHandler + Send + 'static,
F: Fn() -> H + Send + Sync + 'static,
{
let ct = CancellationToken::new();
let mut config = StreamableHttpServerConfig::default()
.with_legacy_session_mode(LEGACY_SESSION_MODE)
.with_json_response(!LEGACY_SESSION_MODE)
.with_cancellation_token(ct.child_token());
config = if options.allowed_hosts.is_empty() {
config.disable_allowed_hosts()
} else {
config.with_allowed_hosts(options.allowed_hosts.clone())
};
config = if options.allowed_origins.is_empty() {
config.disable_allowed_origins()
} else {
config.with_allowed_origins(options.allowed_origins.clone())
};
let service = StreamableHttpService::new(
move || Ok(make_handler()),
LocalSessionManager::default().into(),
config,
);
let app = axum::Router::new()
.route("/health", axum::routing::get(health))
.nest_service("/mcp", service)
.layer(axum::middleware::from_fn(credential_rejection_middleware))
.layer(axum::middleware::from_fn_with_state(
options.forward.clone(),
forward_headers_middleware,
));
let listener = tokio::net::TcpListener::bind(options.bind).await?;
tracing::info!(bind = %options.bind, "MCP streamable-HTTP server listening on /mcp");
let shutdown = {
let ct = ct.clone();
async move {
let _ = tokio::signal::ctrl_c().await;
tracing::info!("shutdown signal received");
ct.cancel();
}
};
axum::serve(listener, app)
.with_graceful_shutdown(shutdown)
.await?;
Ok(())
}
async fn health() -> &'static str {
"OK"
}