use std::{net::SocketAddr, sync::Arc};
use axum::{
Router,
body::Body,
extract::Request,
http::{HeaderName, HeaderValue, Method, StatusCode, header},
middleware,
response::IntoResponse,
routing::{any, get},
};
use rmcp::transport::streamable_http_server::{
session::local::LocalSessionManager,
tower::{StreamableHttpServerConfig, StreamableHttpService},
};
use rust_embed::Embed;
use tower_http::compression::CompressionLayer;
use tower_http::cors::{Any, CorsLayer};
use tracing::{info, warn};
use api_keys_simplified::ApiKeyManagerV0;
use crate::config::{self, Config};
use crate::{
actor, api, auth, backup, db, links, mcp, oauth, ratelimit, realtime, resolve_caller, storage,
};
#[derive(Embed)]
#[folder = "web/dist/"]
#[allow(dead_code)]
struct WebAssets;
async fn serve_frontend(uri: axum::http::Uri) -> impl IntoResponse {
let path = uri.path().trim_start_matches('/');
if let Some(file) = WebAssets::get(path) {
let mime = mime_guess::from_path(path)
.first_or_octet_stream()
.to_string();
let cache_control = if path.starts_with("assets/") {
"public, max-age=31536000, immutable"
} else {
"no-cache"
};
return (
StatusCode::OK,
[
(header::CONTENT_TYPE, mime),
(header::CACHE_CONTROL, cache_control.to_string()),
],
file.data.to_vec(),
)
.into_response();
}
match WebAssets::get("index.html") {
Some(file) => (
StatusCode::OK,
[
(header::CONTENT_TYPE, "text/html".to_string()),
(header::CACHE_CONTROL, "no-cache".to_string()),
],
file.data.to_vec(),
)
.into_response(),
None => (
StatusCode::NOT_FOUND,
"Frontend not built. Run: cd web && bun run build",
)
.into_response(),
}
}
#[derive(Debug, Clone)]
pub struct Reachability {
pub host: String,
pub public_url: Option<String>,
}
impl Reachability {
pub fn from_config(cfg: &Config) -> Self {
Self {
host: cfg.server.host.clone(),
public_url: cfg.server.public_url.clone(),
}
}
pub fn public_exposure(&self) -> Option<String> {
if !config::is_localhost_host(&self.host) {
return Some(format!("[server] host ({}) is not loopback", self.host));
}
let url = self
.public_url
.as_deref()
.map(str::trim)
.filter(|u| !u.is_empty())?;
match public_url_host(url) {
Some(host) if is_private_host(&host) => None,
Some(host) => Some(format!(
"[server] public_url ({url}) points at {host}, which is not a \
loopback or private-network address"
)),
None => Some(format!(
"[server] public_url ({url}) has no host that can be read as \
loopback or private"
)),
}
}
}
fn public_url_host(url: &str) -> Option<String> {
let uri = url.parse::<axum::http::Uri>().ok()?;
uri.authority().map(|a| a.host().to_string())
}
fn is_private_host(host: &str) -> bool {
let host = host.trim().trim_start_matches('[').trim_end_matches(']');
if config::is_localhost_host(host) {
return true;
}
if let Ok(ip) = host.parse::<std::net::IpAddr>() {
return is_private_ip(ip);
}
let lower = host.to_ascii_lowercase();
[".localhost", ".local", ".internal", ".home.arpa", ".lan"]
.iter()
.any(|suffix| lower.ends_with(suffix))
}
fn is_private_ip(ip: std::net::IpAddr) -> bool {
match ip {
std::net::IpAddr::V4(v4) => v4.is_loopback() || v4.is_private() || v4.is_link_local(),
std::net::IpAddr::V6(v6) => {
let first = v6.segments()[0];
v6.is_loopback()
|| (first & 0xfe00) == 0xfc00
|| (first & 0xffc0) == 0xfe80
}
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn build_app(
cfg: &Config,
pool: db::DbPool,
manager: ApiKeyManagerV0,
realtime: realtime::RealtimeHub,
trusted_proxies: Arc<[ratelimit::IpNetwork]>,
) -> Router {
build_app_with_store(
cfg,
pool,
manager,
realtime,
trusted_proxies,
storage::AttachmentStore::from_db_path(&cfg.database.path),
)
}
fn build_app_with_store(
cfg: &Config,
pool: db::DbPool,
manager: ApiKeyManagerV0,
realtime: realtime::RealtimeHub,
trusted_proxies: Arc<[ratelimit::IpNetwork]>,
attachment_store: storage::AttachmentStore,
) -> Router {
let issuer = cfg.server.public_url.clone().unwrap_or_else(|| {
let host = match cfg.server.host.as_str() {
"0.0.0.0" | "::" | "[::]" => "127.0.0.1",
h => h,
};
format!("http://{}:{}", crate::display_host(host), cfg.server.port)
});
let manager_ext = Arc::new(manager.clone());
let auth_state = auth::AuthState {
db: pool.clone(),
manager,
public_url: issuer.clone(),
required: cfg.auth.required,
};
let db_for_mcp = pool.clone();
let realtime_for_mcp = realtime.clone();
let mut mcp_allowed_hosts: Vec<String> =
vec!["localhost".into(), "127.0.0.1".into(), "::1".into()];
if let Some(ref url) = cfg.server.public_url
&& let Ok(parsed) = url.parse::<axum::http::Uri>()
&& let Some(authority) = parsed.authority()
{
let host: String = authority.host().to_string();
mcp_allowed_hosts.push(host);
}
let mcp_config = StreamableHttpServerConfig::default()
.with_stateful_mode(false)
.with_json_response(true)
.with_allowed_hosts(mcp_allowed_hosts.clone());
let mcp_service = StreamableHttpService::new(
move || {
Ok(mcp::LificMcp::with_realtime(
db_for_mcp.clone(),
realtime_for_mcp.clone(),
))
},
Arc::new(LocalSessionManager::default()),
mcp_config,
);
let login_limiter = Arc::new(ratelimit::RateLimiter::new(
5,
std::time::Duration::from_secs(15 * 60),
));
let attachment_config = api::AttachmentConfig::default();
let attachment_upload_limiter = Arc::new(api::AttachmentUploadLimiter(
ratelimit::RateLimiter::new(30, std::time::Duration::from_secs(10 * 60)),
));
let mcp_public_url = cfg.server.public_url.clone();
let mcp_allowed_hosts_for_links = mcp_allowed_hosts.clone();
let authed_routes = api::router(pool.clone(), &cfg.server.cors_origins)
.route(
"/mcp",
any(move |request: Request<Body>| async move {
let auth_user = request
.extensions()
.get::<Option<db::models::AuthUser>>()
.cloned()
.flatten();
let issue_links = links::IssueLinkContext::for_http_request(
mcp_public_url.as_deref(),
request
.headers()
.get(header::HOST)
.and_then(|value| value.to_str().ok()),
&mcp_allowed_hosts_for_links,
);
mcp::with_request_context(auth_user, issue_links, || async {
mcp_service.handle(request).await.into_response()
})
.await
}),
)
.layer(axum::Extension(realtime.clone()))
.layer(axum::Extension(login_limiter))
.layer(axum::Extension(trusted_proxies.clone()))
.layer(axum::Extension(attachment_store))
.layer(axum::Extension(attachment_config))
.layer(axum::Extension(attachment_upload_limiter))
.layer(axum::Extension(crate::config::AuthConfig::from_server(
&cfg.auth,
cfg.server.public_url.as_deref(),
)))
.layer(axum::Extension(Reachability::from_config(cfg)))
.layer(axum::Extension(manager_ext))
.layer(middleware::from_fn_with_state(
auth_state,
auth_middleware_wrapper,
));
let oauth_register_limiter = Arc::new(ratelimit::RateLimiter::new(
10,
std::time::Duration::from_secs(60 * 60),
));
let oauth_state = oauth::OAuthState {
db: pool.clone(),
issuer,
issuer_is_explicit: cfg.server.public_url.is_some(),
allowed_hosts: mcp_allowed_hosts.clone().into(),
register_limiter: oauth_register_limiter,
trusted_proxies,
};
let authless_mcp_router: Option<Router> = cfg
.server
.mcp_path_token
.clone()
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.map(|token| {
let authless_user: Option<db::models::AuthUser> = {
match pool.read() {
Ok(conn) => match cfg.server.mcp_path_user.as_deref() {
Some(uname) => db::queries::users::get_user_by_username(&conn, uname)
.ok()
.map(|u| db::models::AuthUser {
id: u.id,
username: u.username,
display_name: u.display_name,
is_admin: u.is_admin,
}),
None => {
resolve_caller::resolve_caller_conn(&conn, None, actor::Transport::Mcp)
.ok()
.flatten()
.map(|i| i.user)
}
},
Err(_) => None,
}
};
info!(
acting_as = authless_user
.as_ref()
.map_or("<anonymous>", |u| u.username.as_str()),
"authless MCP endpoint enabled at /mcp/<token>"
);
build_authless_mcp_router(
pool.clone(),
&token,
authless_user,
mcp_allowed_hosts.clone(),
cfg.server.public_url.clone(),
realtime.clone(),
)
});
let app = authed_routes.merge(oauth::router(oauth_state));
let app = match authless_mcp_router {
Some(r) => app.merge(r),
None => app,
};
app.fallback(get(serve_frontend))
.layer(build_global_cors(&cfg.server.cors_origins))
.layer(axum::extract::DefaultBodyLimit::max(2 * 1024 * 1024)) .layer(CompressionLayer::new())
}
pub async fn run(cfg: &Config) -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| format!("lific={}", cfg.log.level).into()),
)
.init();
let trusted_proxies = Arc::<[ratelimit::IpNetwork]>::from(
cfg.server
.trusted_proxy_ranges()
.map_err(|error| format!("invalid server.trusted_proxies: {error}"))?,
);
let reachability = Reachability::from_config(cfg);
if !cfg.auth.required {
if let Some(exposure) = reachability.public_exposure() {
return Err(format!(
"refusing to start: [auth] required = false (login-free mode) while \
{exposure}. Anyone who can reach this instance can administer it. \
Re-enable auth, bind to 127.0.0.1, or remove the public URL."
)
.into());
}
warn!(
host = %cfg.server.host,
"{} ([auth] required = false)",
config::login_free_caution()
);
}
let pool = db::open(&cfg.database.path)?;
info!(path = %cfg.database.path.display(), "database ready");
{
let conn = pool.write()?;
db::queries::settings::ensure(&conn, cfg.auth.allow_signup)?;
let auto_login = db::queries::settings::get(&conn)?.web_auto_login;
drop(conn);
if auto_login && let Some(exposure) = reachability.public_exposure() {
return Err(format!(
"refusing to start: single-user web auto-login is enabled while \
{exposure}. Anyone who can reach this instance gets an admin \
session without a password. Disable web auto-login in the \
instance settings, bind to 127.0.0.1, or remove the public URL."
)
.into());
}
if auto_login
&& cfg
.server
.public_url
.as_deref()
.is_some_and(|u| u.trim().to_ascii_lowercase().starts_with("https://"))
{
warn!(
"web_auto_login is ENABLED while public_url is https — anyone who can \
reach this instance gets an admin session without a password. Only \
enable single-user mode on a private/local instance."
);
}
}
let manager =
auth::create_key_manager().map_err(|e| format!("key manager init failed: {e}"))?;
if auth::should_mint_initial_key(&pool) {
let key = auth::create_api_key(&pool, &manager, "default", None)?;
info!("no API keys found, auto-generated initial key");
print_initial_key(&key);
} else if !auth::has_any_keys(&pool) {
info!(
"human operator present — passwordless mode; mint keys on demand with `lific key create`"
);
} else {
let count = auth::list_api_keys(&pool)?
.iter()
.filter(|k| !k.revoked)
.count();
info!(active_keys = count, "API key auth enabled");
}
if cfg.backup.enabled {
let pool_arc = Arc::new(pool.clone());
backup::start_backup_task(pool_arc, cfg.database.path.clone(), cfg.backup.clone());
info!(
dir = %cfg.backup_dir().display(),
interval = %format!("{}m", cfg.backup.interval_minutes),
retain = cfg.backup.retain,
"automatic backups enabled"
);
}
crate::retention::start_trash_purge_task(pool.clone(), cfg.retention.trash_days);
let attachment_store = storage::AttachmentStore::from_db_path(&cfg.database.path);
let gc_attachment_store = attachment_store.clone();
storage::start_gc_task(pool.clone(), gc_attachment_store);
let app = build_app_with_store(
cfg,
pool.clone(),
manager,
realtime::RealtimeHub::new(),
trusted_proxies,
attachment_store,
);
let addr = format!("{}:{}", cfg.server.host, cfg.server.port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
info!(addr = %addr, "lific server started (REST + MCP + OAuth at /mcp)");
let shutdown_pool = pool.clone();
let server = axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(shutdown_signal(shutdown_pool));
server.await?;
Ok(())
}
fn print_initial_key(key: &str) {
println!();
println!(" Initial API key — save it now, it will not be shown again:");
println!();
println!(" {key}");
println!();
println!(" Use it as: Authorization: Bearer <key>");
println!();
}
fn build_global_cors(cors_origins: &[String]) -> CorsLayer {
let layer = CorsLayer::new()
.allow_methods([
Method::GET,
Method::POST,
Method::PATCH,
Method::PUT,
Method::DELETE,
Method::OPTIONS,
])
.allow_headers([
header::AUTHORIZATION,
header::CONTENT_TYPE,
header::ACCEPT,
HeaderName::from_static("mcp-protocol-version"),
HeaderName::from_static("mcp-session-id"),
HeaderName::from_static("last-event-id"),
])
.expose_headers([
header::WWW_AUTHENTICATE,
HeaderName::from_static("mcp-session-id"),
])
.max_age(std::time::Duration::from_secs(86400));
if cors_origins.is_empty() {
layer.allow_origin(Any)
} else {
let origins: Vec<HeaderValue> =
cors_origins.iter().filter_map(|o| o.parse().ok()).collect();
layer.allow_origin(origins)
}
}
fn build_authless_mcp_router(
pool: db::DbPool,
token: &str,
user: Option<db::models::AuthUser>,
allowed_hosts: Vec<String>,
public_url: Option<String>,
realtime: realtime::RealtimeHub,
) -> Router {
let allowed_hosts_for_links = allowed_hosts.clone();
let config = StreamableHttpServerConfig::default()
.with_stateful_mode(false)
.with_json_response(true)
.with_allowed_hosts(allowed_hosts);
let service = StreamableHttpService::new(
move || Ok(mcp::LificMcp::with_realtime(pool.clone(), realtime.clone())),
Arc::new(LocalSessionManager::default()),
config,
);
Router::new().route(
&format!("/mcp/{token}"),
any(move |request: Request<Body>| async move {
let issue_links = links::IssueLinkContext::for_http_request(
public_url.as_deref(),
request
.headers()
.get(header::HOST)
.and_then(|value| value.to_str().ok()),
&allowed_hosts_for_links,
);
mcp::with_request_context(user, issue_links, || async {
service.handle(request).await.into_response()
})
.await
}),
)
}
async fn auth_middleware_wrapper(
state: axum::extract::State<auth::AuthState>,
request: Request<Body>,
next: middleware::Next,
) -> axum::response::Response {
if skips_auth_middleware(request.uri().path()) {
return next.run(request).await;
}
auth::require_api_key(state, request, next).await
}
fn skips_auth_middleware(path: &str) -> bool {
matches!(
path,
"/api/health"
| "/api/instance"
| "/api/auth/signup"
| "/api/auth/login"
| "/api/auth/auto-login"
| "/api/events/ws"
| "/register"
| "/authorize"
| "/token"
| "/revoke"
) || path.starts_with("/.well-known/")
|| path.starts_with("/oauth/")
}
async fn shutdown_signal(pool: db::DbPool) {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
info!("shutdown signal received, checkpointing WAL...");
backup::checkpoint_wal(&pool);
info!("shutdown complete");
}
#[cfg(test)]
mod reachability_tests {
use super::*;
fn reach(host: &str, public_url: Option<&str>) -> Reachability {
Reachability {
host: host.to_string(),
public_url: public_url.map(str::to_string),
}
}
#[test]
fn a_local_instance_is_not_exposed() {
for (host, url) in [
("127.0.0.1", None),
("localhost", None),
("::1", None),
("127.0.0.1", Some("http://127.0.0.1:3456")),
("127.0.0.1", Some("http://localhost:3456")),
("127.0.0.1", Some("http://[::1]:3456")),
("127.0.0.1", Some("https://10.0.0.5")),
("127.0.0.1", Some("https://192.168.1.20:3456")),
("127.0.0.1", Some("https://nas.local")),
("127.0.0.1", Some("https://lific.home.arpa")),
("127.0.0.1", Some(" ")),
] {
assert_eq!(
reach(host, url).public_exposure(),
None,
"host={host} public_url={url:?} should count as local"
);
}
}
#[test]
fn a_non_loopback_bind_is_exposed() {
for host in ["0.0.0.0", "::", "192.168.1.10", "not-an-address"] {
let exposure = reach(host, None).public_exposure().unwrap_or_default();
assert!(
exposure.contains("[server] host"),
"host={host} should be refused on the bind: {exposure:?}"
);
}
}
#[test]
fn a_loopback_bind_behind_a_public_url_is_exposed() {
for url in [
"https://magi.tailb93ac8.ts.net",
"https://lific.example.com/lific",
"http://203.0.113.10:3456",
"https://[2606:4700:4700::1111]",
"lific.example.com",
] {
let exposure = reach("127.0.0.1", Some(url))
.public_exposure()
.unwrap_or_default();
assert!(
exposure.contains("public_url"),
"public_url={url} should be refused: {exposure:?}"
);
}
}
#[test]
fn from_config_reads_both_halves() {
let mut cfg = Config::default();
cfg.server.host = "127.0.0.1".into();
cfg.server.public_url = Some("https://lific.example.com".into());
let r = Reachability::from_config(&cfg);
assert_eq!(r.host, "127.0.0.1");
assert_eq!(r.public_url.as_deref(), Some("https://lific.example.com"));
assert!(r.public_exposure().is_some());
}
}
#[cfg(test)]
mod cors_tests {
use super::*;
use axum::routing::post;
use http_body_util::BodyExt;
use tower::ServiceExt;
#[test]
fn websocket_path_skips_header_auth_middleware() {
assert!(skips_auth_middleware("/api/events/ws"));
assert!(!skips_auth_middleware("/api/issues"));
}
fn app_with_cors(origins: &[String]) -> Router {
let inner = Router::new().route(
"/mcp",
post(|headers: axum::http::HeaderMap| async move {
if headers.get(header::AUTHORIZATION).is_none() {
return (StatusCode::UNAUTHORIZED, "missing auth").into_response();
}
(StatusCode::OK, "ok").into_response()
}),
);
inner.layer(build_global_cors(origins))
}
#[tokio::test]
async fn cors_preflight_to_mcp_bypasses_auth() {
let app = app_with_cors(&[]);
let req = Request::builder()
.method(Method::OPTIONS)
.uri("/mcp")
.header("origin", "https://claude.ai")
.header("access-control-request-method", "POST")
.header(
"access-control-request-headers",
"authorization,content-type",
)
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert!(
res.status().is_success(),
"preflight should succeed without auth, got {}",
res.status()
);
let headers = res.headers();
assert_eq!(
headers
.get("access-control-allow-origin")
.and_then(|v| v.to_str().ok()),
Some("*"),
"empty cors_origins should allow any origin"
);
let allow_methods = headers
.get("access-control-allow-methods")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
assert!(
allow_methods.contains("POST"),
"POST must be in allowed methods, got: {allow_methods}"
);
assert!(
allow_methods.contains("PATCH"),
"PATCH must be in allowed methods, got: {allow_methods}"
);
let allow_headers = headers
.get("access-control-allow-headers")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_ascii_lowercase();
assert!(
allow_headers.contains("authorization"),
"authorization must be allowed, got: {allow_headers}"
);
assert!(
allow_headers.contains("mcp-session-id"),
"mcp-session-id must be allowed, got: {allow_headers}"
);
}
#[tokio::test]
async fn cors_does_not_bypass_auth_on_real_request() {
let app = app_with_cors(&[]);
let req = Request::builder()
.method(Method::POST)
.uri("/mcp")
.header("origin", "https://claude.ai")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn explicit_origins_are_allowlisted() {
let app = app_with_cors(&["https://claude.ai".to_string()]);
let req = Request::builder()
.method(Method::OPTIONS)
.uri("/mcp")
.header("origin", "https://claude.ai")
.header("access-control-request-method", "POST")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
assert!(res.status().is_success());
assert_eq!(
res.headers()
.get("access-control-allow-origin")
.and_then(|v| v.to_str().ok()),
Some("https://claude.ai")
);
}
#[tokio::test]
async fn mcp_session_id_is_exposed() {
let app = app_with_cors(&[]);
let req = Request::builder()
.method(Method::OPTIONS)
.uri("/mcp")
.header("origin", "https://claude.ai")
.header("access-control-request-method", "POST")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
let _ = res.into_body().collect().await;
let app = app_with_cors(&[]);
let req = Request::builder()
.method(Method::POST)
.uri("/mcp")
.header("origin", "https://claude.ai")
.body(Body::empty())
.unwrap();
let res = app.oneshot(req).await.unwrap();
let expose = res
.headers()
.get("access-control-expose-headers")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_ascii_lowercase();
assert!(
expose.contains("mcp-session-id"),
"mcp-session-id must be exposed, got: {expose}"
);
assert!(
expose.contains("www-authenticate"),
"www-authenticate must be exposed, got: {expose}"
);
}
}
#[cfg(test)]
mod authless_mcp_tests {
use super::*;
use http_body_util::BodyExt;
use tower::ServiceExt;
fn initialize_body() -> Body {
let body = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {"name": "test", "version": "1"}
}
});
Body::from(serde_json::to_vec(&body).unwrap())
}
#[tokio::test]
async fn authless_path_serves_mcp_without_auth() {
let pool = db::open_memory().unwrap();
let token = "s3cret-authless-token-abcdef";
let router = build_authless_mcp_router(
pool,
token,
None,
vec!["localhost".into()],
None,
realtime::RealtimeHub::new(),
);
let req = Request::builder()
.method(Method::POST)
.uri(format!("/mcp/{token}"))
.header("host", "localhost")
.header("content-type", "application/json")
.header("accept", "application/json, text/event-stream")
.body(initialize_body())
.unwrap();
let res = router.oneshot(req).await.unwrap();
assert_eq!(
res.status(),
StatusCode::OK,
"authless MCP initialize must succeed without any auth header"
);
let bytes = res.into_body().collect().await.unwrap().to_bytes();
let val: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert!(
val["result"]["serverInfo"].is_object(),
"expected an initialize result, got: {val}"
);
}
#[tokio::test]
async fn wrong_path_token_does_not_match() {
let pool = db::open_memory().unwrap();
let router = build_authless_mcp_router(
pool,
"the-right-token",
None,
vec!["localhost".into()],
None,
realtime::RealtimeHub::new(),
);
let req = Request::builder()
.method(Method::POST)
.uri("/mcp/the-wrong-token")
.header("host", "localhost")
.header("content-type", "application/json")
.header("accept", "application/json, text/event-stream")
.body(initialize_body())
.unwrap();
let res = router.oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
}