use std::sync::Arc;
use axum::extract::FromRequestParts;
use axum::routing::{get, post};
use axum::Router;
use crate::extractors::Tenant;
use crate::sql::sqlx::Database;
use crate::tenancy::jwt_lifecycle::JwtLifecycle;
use crate::tenancy::DefaultTenantDb;
use super::auth::{agent_token, default_jwt, post_authed};
use super::transport::{post_handler, sse_handler};
#[derive(Clone)]
pub(crate) struct McpState {
#[allow(dead_code)]
pub(crate) pool: Option<crate::sql::Pool>,
pub(crate) page_size: Option<usize>,
}
impl McpState {
fn new(pool: Option<crate::sql::Pool>) -> Self {
Self {
pool,
page_size: None,
}
}
}
pub(crate) const DEFAULT_PREFIX: &str = "/mcp";
const TOKEN_PATH: &str = "/token";
const OAUTH_TOKEN_PATH: &str = "/oauth/token";
pub(crate) fn post_endpoints(prefix: &str) -> Option<[String; 3]> {
let base = prefix.trim_end_matches('/');
if base.is_empty() {
return None;
}
Some([
base.to_owned(),
format!("{base}{TOKEN_PATH}"),
format!("{base}{OAUTH_TOKEN_PATH}"),
])
}
#[derive(Clone)]
pub struct AuthedMcpState {
pub(crate) mcp: McpState,
pub(crate) jwt: Arc<JwtLifecycle>,
}
fn routes(state: McpState) -> Router {
Router::new()
.route("/", post(post_handler))
.with_state(state)
}
#[must_use]
pub fn router(pool: crate::sql::Pool) -> Router {
routes(McpState::new(Some(pool)))
}
#[must_use]
pub fn tenant_router() -> Router {
routes(McpState::new(None))
}
#[must_use]
pub fn secure_tenant_router() -> Router {
tenant_router_authed(default_jwt())
}
#[must_use]
pub fn secure_tenant_router_for<DB>() -> Router
where
DB: Database,
Tenant<DB>: FromRequestParts<AuthedMcpState> + Send,
{
tenant_router_authed_for::<DB>(default_jwt())
}
#[cfg(feature = "config")]
#[must_use]
pub fn secure_tenant_router_from_settings(settings: &crate::config::McpSettings) -> Router {
secure_tenant_router_from_settings_for::<DefaultTenantDb>(settings)
}
#[cfg(feature = "config")]
#[must_use]
pub fn secure_tenant_router_from_settings_for<DB>(settings: &crate::config::McpSettings) -> Router
where
DB: Database,
Tenant<DB>: FromRequestParts<AuthedMcpState> + Send,
{
let jwt = Arc::new(
JwtLifecycle::lazy(super::auth::jwt_secret).with_access_ttl(settings.token_ttl_secs()),
);
let state = AuthedMcpState {
mcp: McpState {
page_size: settings.max_tools_listed,
..McpState::new(None)
},
jwt,
};
let mut router = authed_routes::<DB>(state, settings.sse_enabled());
if !settings.allowed_origins.is_empty() {
use crate::cors::{CorsLayer, CorsRouterExt};
router = router.cors(CorsLayer::new().allow_origins(settings.allowed_origins.clone()));
}
if let Some(rpm) = settings.rate_limit() {
use crate::rate_limit::{RateLimitLayer, RateLimitRouterExt};
router = router.rate_limit(RateLimitLayer::per_ip(
rpm.get(),
std::time::Duration::from_secs(60),
));
}
{
use crate::body_limit::{BodyLimitLayer, BodyLimitRouterExt};
router = router.body_limit(BodyLimitLayer::new(settings.max_body_bytes()));
}
router
}
#[must_use]
pub fn tenant_router_authed(jwt: Arc<JwtLifecycle>) -> Router {
tenant_router_authed_for::<DefaultTenantDb>(jwt)
}
#[must_use]
pub fn tenant_router_authed_for<DB>(jwt: Arc<JwtLifecycle>) -> Router
where
DB: Database,
Tenant<DB>: FromRequestParts<AuthedMcpState> + Send,
{
let state = AuthedMcpState {
mcp: McpState::new(None),
jwt,
};
authed_routes::<DB>(state, true)
}
fn authed_routes<DB>(state: AuthedMcpState, enable_sse: bool) -> Router
where
DB: Database,
Tenant<DB>: FromRequestParts<AuthedMcpState> + Send,
{
use super::oauth;
use axum::middleware::map_response;
let root = if enable_sse {
post(post_authed::<DB>).get(sse_handler::<DB>)
} else {
post(post_authed::<DB>)
};
Router::new()
.route("/", root)
.route(
TOKEN_PATH,
post(agent_token::<DB>).layer(map_response(crate::vary::token_reply)),
)
.route(
OAUTH_TOKEN_PATH,
post(oauth::oauth_token::<DB>).layer(map_response(crate::vary::token_reply)),
)
.route(
"/.well-known/oauth-protected-resource",
get(oauth::well_known_protected_resource),
)
.route(
"/.well-known/oauth-authorization-server",
get(oauth::well_known_authorization_server),
)
.with_state(state)
}