mod auth;
mod errors;
mod server;
pub use auth::{
AuthError, AuthMode, ResolvedAuth, first_run_message, is_loopback_host, resolve_auth,
};
pub use server::{McpConfig, McpServer};
use crate::Database;
use std::sync::Arc;
pub async fn serve_stdio(
db: Database,
config: McpConfig,
) -> Result<(), Box<dyn std::error::Error>> {
use rmcp::{ServiceExt, transport::stdio};
let server = McpServer::with_config(Arc::new(db), config)
.serve(stdio())
.await
.map_err(|e| format!("MCP server error: {e}"))?;
server.waiting().await?;
Ok(())
}
#[derive(Clone, Debug)]
pub struct HttpOptions {
pub host: String,
pub port: u16,
pub auth: AuthMode,
pub extra_allowed_hosts: Vec<String>,
}
fn bracket_ipv6(host: &str) -> String {
if host.contains(':') && !host.starts_with('[') {
format!("[{host}]")
} else {
host.to_string()
}
}
fn format_bind_addr(host: &str, port: u16) -> String {
format!("{}:{}", bracket_ipv6(host), port)
}
pub async fn serve_http(
db: Database,
opts: HttpOptions,
config: McpConfig,
) -> Result<(), Box<dyn std::error::Error>> {
serve_http_with_shutdown(db, opts, config, None).await
}
pub async fn serve_http_with_shutdown(
db: Database,
opts: HttpOptions,
config: McpConfig,
shutdown: Option<tokio_util::sync::CancellationToken>,
) -> Result<(), Box<dyn std::error::Error>> {
use rmcp::transport::streamable_http_server::{
StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager,
};
let ct = shutdown.unwrap_or_default();
let db = Arc::new(db);
let mut allowed_hosts: Vec<String> = vec!["localhost".into(), "127.0.0.1".into(), "::1".into()];
let mut allowed_origins: Vec<String> =
vec!["http://localhost".into(), "http://127.0.0.1".into()];
for host in &opts.extra_allowed_hosts {
allowed_hosts.push(host.clone());
allowed_origins.push(format!("http://{}", bracket_ipv6(host)));
}
#[allow(clippy::field_reassign_with_default)]
let http_config = {
let mut c = StreamableHttpServerConfig::default();
c.stateful_mode = false;
c.json_response = true;
c.sse_keep_alive = None;
c.cancellation_token = ct.clone();
c.allowed_hosts = allowed_hosts;
c.allowed_origins = allowed_origins;
c
};
let service: StreamableHttpService<McpServer, LocalSessionManager> = StreamableHttpService::new(
{
let db = db.clone();
let config = config.clone();
move || Ok(McpServer::with_config(db.clone(), config.clone()))
},
Default::default(),
http_config,
);
let router = axum::Router::new().nest_service("/mcp", service).layer(
axum::middleware::from_fn_with_state(opts.auth.clone(), auth::enforce),
);
let addr = format_bind_addr(&opts.host, opts.port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
let local_addr = listener.local_addr()?;
eprintln!("MCP HTTP server listening on http://{local_addr}/mcp");
use std::future::IntoFuture;
let serve_fut = axum::serve(listener, router).into_future();
tokio::select! {
res = serve_fut => res?,
_ = ct.cancelled_owned() => {}
}
Ok(())
}