//! MCP tools for signing in (by email code or passkey) and the signed-in user.
use axum::extract::FromRef;
use axum::http::request::Parts;
use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::handler::server::tool::Extension;
use rmcp::handler::server::wrapper::{Json, Parameters};
use rmcp::{tool, tool_router};
use slave_framework::auth::{Challenge, normalize_email};
use crate::base::auth::bearer_token;
use crate::base::error::{AppError, AppResult};
use crate::base::mcp::McpServer;
use crate::modules::auth::AuthService;
use crate::modules::auth::dto::auth::{
ChallengeResponse, DoneResponse, EmailRequest, MeResponse, PasskeyRequest, SessionResponse,
VerifyRequest,
};
pub fn tools() -> ToolRouter<McpServer> {
McpServer::auth_tools()
}
#[tool_router(router = auth_tools, vis = "pub(crate)")]
impl McpServer {
/// The signed-in user and their roles. Sign in with `auth_email_start`
/// and `auth_email_verify` (or a passkey) and send the token as
/// `Authorization: Bearer <token>`.
#[tool(annotations(read_only_hint = true, open_world_hint = false))]
async fn auth_me(&self, Extension(parts): Extension<Parts>) -> AppResult<Json<MeResponse>> {
let user = self.authenticate_customer(&parts).await?;
Ok(Json(MeResponse {
id: user.id,
name: user.name,
email: user.email,
roles: user.roles,
}))
}
/// Email a 6-digit sign-in code to `email`. New addresses get an account
/// when they verify it; the first user ever becomes super admin.
#[tool(annotations(open_world_hint = true))]
async fn auth_email_start(
&self,
Parameters(input): Parameters<EmailRequest>,
) -> AppResult<Json<DoneResponse>> {
let email = email(&input.email)?;
self.state.auth.send_code(&email).await?;
Ok(Json(DoneResponse { ok: true }))
}
/// Sign in with the code from `auth_email_start`; returns a session token.
#[tool(annotations(open_world_hint = false))]
async fn auth_email_verify(
&self,
Parameters(input): Parameters<VerifyRequest>,
) -> AppResult<Json<SessionResponse>> {
let email = email(&input.email)?;
if !self.state.auth.verify_code(&email, &input.code).await? {
return Err(AppError::Validation(
"That code is wrong or has expired.".into(),
));
}
let user = AuthService::from_ref(&self.state).sign_in(&email).await?;
let token = self
.state
.auth
.create_session(user.id, input.customer)
.await?;
Ok(Json(SessionResponse { token }))
}
/// Sign out: ends the caller's session.
#[tool(annotations(destructive_hint = true, open_world_hint = false))]
async fn auth_logout(
&self,
Extension(parts): Extension<Parts>,
) -> AppResult<Json<DoneResponse>> {
let token = bearer_token(&parts.headers).ok_or(AppError::Unauthorized)?;
self.state.auth.end_session(token).await?;
Ok(Json(DoneResponse { ok: true }))
}
/// Start adding a passkey for the signed-in user.
#[tool(annotations(open_world_hint = false))]
async fn auth_passkey_register_start(
&self,
Extension(parts): Extension<Parts>,
) -> AppResult<Json<ChallengeResponse>> {
let user = self.authenticate_customer(&parts).await?;
let email = user.email.clone().unwrap_or_else(|| user.name.clone());
let challenge = self
.state
.auth
.passkey_register_start(user.id, &email)
.await?;
Ok(Json(challenge.into()))
}
/// Finish adding a passkey.
#[tool(annotations(open_world_hint = false))]
async fn auth_passkey_register_finish(
&self,
Extension(parts): Extension<Parts>,
Parameters(input): Parameters<PasskeyRequest>,
) -> AppResult<Json<DoneResponse>> {
let user = self.authenticate_customer(&parts).await?;
self.state
.auth
.passkey_register_finish(user.id, &input.id, input.credential)
.await?;
Ok(Json(DoneResponse { ok: true }))
}
/// Start signing in with a passkey.
#[tool(annotations(open_world_hint = false))]
async fn auth_passkey_login_start(&self) -> AppResult<Json<ChallengeResponse>> {
Ok(Json(self.state.auth.passkey_login_start().await?.into()))
}
/// Finish signing in with a passkey; returns a session token.
#[tool(annotations(open_world_hint = false))]
async fn auth_passkey_login_finish(
&self,
Parameters(input): Parameters<PasskeyRequest>,
) -> AppResult<Json<SessionResponse>> {
let user_id = self
.state
.auth
.passkey_login_finish(&input.id, input.credential)
.await?;
AuthService::from_ref(&self.state).load(user_id).await?;
let token = self
.state
.auth
.create_session(user_id, input.customer)
.await?;
Ok(Json(SessionResponse { token }))
}
}
fn email(email: &str) -> AppResult<String> {
normalize_email(email)
.ok_or_else(|| AppError::Validation("That email address is not valid.".into()))
}
impl From<Challenge> for ChallengeResponse {
fn from(challenge: Challenge) -> Self {
Self {
id: challenge.id,
options: challenge.options,
}
}
}