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::{ImportConcern, ImportNotice, ImportReport, 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 serve_http_with_options(db, opts, config, shutdown, crate::ServerOptions::default()).await
88}
89
90pub async fn serve_http_with_options(
96 db: Database,
97 opts: HttpOptions,
98 config: McpConfig,
99 shutdown: Option<tokio_util::sync::CancellationToken>,
100 options: crate::ServerOptions,
101) -> Result<(), Box<dyn std::error::Error>> {
102 use rmcp::transport::streamable_http_server::{
103 StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager,
104 };
105
106 let ct = shutdown.unwrap_or_default();
107 let db = Arc::new(db);
108
109 let mut allowed_hosts: Vec<String> = vec!["localhost".into(), "127.0.0.1".into(), "::1".into()];
112 let mut allowed_origins: Vec<String> =
113 vec!["http://localhost".into(), "http://127.0.0.1".into()];
114 for host in &opts.extra_allowed_hosts {
115 allowed_hosts.push(host.clone());
116 allowed_origins.push(format!("http://{}", bracket_ipv6(host)));
117 }
118
119 #[allow(clippy::field_reassign_with_default)]
122 let http_config = {
123 let mut c = StreamableHttpServerConfig::default();
124 c.stateful_mode = false;
125 c.json_response = true;
126 c.sse_keep_alive = None;
127 c.cancellation_token = ct.clone();
128 c.allowed_hosts = allowed_hosts;
134 c.allowed_origins = allowed_origins;
135 c
136 };
137
138 let service: StreamableHttpService<McpServer, LocalSessionManager> = StreamableHttpService::new(
139 {
140 let db = db.clone();
141 let config = config.clone();
142 move || Ok(McpServer::with_config(db.clone(), config.clone()))
143 },
144 Default::default(),
145 http_config,
146 );
147
148 let router = axum::Router::new().nest_service("/mcp", service).layer(
151 axum::middleware::from_fn_with_state(opts.auth.clone(), auth::enforce),
152 );
153 let addr = format_bind_addr(&opts.host, opts.port);
154 let std_listener = crate::net::bind_exclusive_to(&addr).map_err(std::io::Error::other)?;
158 let listener = tokio::net::TcpListener::from_std(std_listener)?;
159 let local_addr = listener.local_addr()?;
160
161 if !options.suppresses_informational_messages() {
162 eprintln!("MCP HTTP server listening on http://{local_addr}/mcp");
163 }
164
165 use std::future::IntoFuture;
173 let serve_fut = axum::serve(listener, router).into_future();
174 tokio::select! {
175 res = serve_fut => res?,
176 _ = ct.cancelled_owned() => {}
177 }
178
179 Ok(())
180}