1mod auth;
7mod errors;
8mod server;
9
10pub use auth::{
11 AuthError, AuthMode, ResolvedAuth, first_run_message, is_loopback_host, resolve_auth,
12};
13pub use server::{McpConfig, McpServer};
14
15use crate::Database;
16use std::sync::Arc;
17
18pub async fn serve_stdio(
23 db: Database,
24 config: McpConfig,
25) -> Result<(), Box<dyn std::error::Error>> {
26 use rmcp::{ServiceExt, transport::stdio};
27
28 let server = McpServer::with_config(Arc::new(db), config)
29 .serve(stdio())
30 .await
31 .map_err(|e| format!("MCP server error: {e}"))?;
32
33 server.waiting().await?;
34 Ok(())
35}
36
37#[derive(Clone, Debug)]
44pub struct HttpOptions {
45 pub host: String,
46 pub port: u16,
47 pub auth: AuthMode,
48 pub extra_allowed_hosts: Vec<String>,
51}
52
53fn bracket_ipv6(host: &str) -> String {
56 if host.contains(':') && !host.starts_with('[') {
57 format!("[{host}]")
58 } else {
59 host.to_string()
60 }
61}
62
63fn format_bind_addr(host: &str, port: u16) -> String {
65 format!("{}:{}", bracket_ipv6(host), port)
66}
67
68pub async fn serve_http(
73 db: Database,
74 opts: HttpOptions,
75 config: McpConfig,
76) -> Result<(), Box<dyn std::error::Error>> {
77 serve_http_with_shutdown(db, opts, config, None).await
78}
79
80pub async fn serve_http_with_shutdown(
82 db: Database,
83 opts: HttpOptions,
84 config: McpConfig,
85 shutdown: Option<tokio_util::sync::CancellationToken>,
86) -> Result<(), Box<dyn std::error::Error>> {
87 use rmcp::transport::streamable_http_server::{
88 StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager,
89 };
90
91 let ct = shutdown.unwrap_or_default();
92 let db = Arc::new(db);
93
94 let mut allowed_hosts: Vec<String> = vec!["localhost".into(), "127.0.0.1".into(), "::1".into()];
97 let mut allowed_origins: Vec<String> =
98 vec!["http://localhost".into(), "http://127.0.0.1".into()];
99 for host in &opts.extra_allowed_hosts {
100 allowed_hosts.push(host.clone());
101 allowed_origins.push(format!("http://{}", bracket_ipv6(host)));
102 }
103
104 #[allow(clippy::field_reassign_with_default)]
107 let http_config = {
108 let mut c = StreamableHttpServerConfig::default();
109 c.stateful_mode = false;
110 c.json_response = true;
111 c.sse_keep_alive = None;
112 c.cancellation_token = ct.clone();
113 c.allowed_hosts = allowed_hosts;
119 c.allowed_origins = allowed_origins;
120 c
121 };
122
123 let service: StreamableHttpService<McpServer, LocalSessionManager> = StreamableHttpService::new(
124 {
125 let db = db.clone();
126 let config = config.clone();
127 move || Ok(McpServer::with_config(db.clone(), config.clone()))
128 },
129 Default::default(),
130 http_config,
131 );
132
133 let router = axum::Router::new().nest_service("/mcp", service).layer(
136 axum::middleware::from_fn_with_state(opts.auth.clone(), auth::enforce),
137 );
138 let addr = format_bind_addr(&opts.host, opts.port);
139 let listener = tokio::net::TcpListener::bind(&addr).await?;
140 let local_addr = listener.local_addr()?;
141
142 eprintln!("MCP HTTP server listening on http://{local_addr}/mcp");
143
144 use std::future::IntoFuture;
152 let serve_fut = axum::serve(listener, router).into_future();
153 tokio::select! {
154 res = serve_fut => res?,
155 _ = ct.cancelled_owned() => {}
156 }
157
158 Ok(())
159}