//! The MCP server: the application's only business interface.
//!
//! Every business operation is an MCP tool, served over Streamable HTTP at
//! `/mcp`. Domain modules contribute tools from their `tools.rs`; they are
//! combined once at startup by `crate::modules::tools()`.
//!
//! The endpoint runs statelessly (no MCP sessions) and answers plain
//! request/response calls with `application/json`, so any instance can serve
//! any request.
use std::sync::Arc;
use axum::http::request::Parts;
use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::model::{Implementation, ServerCapabilities, ServerConfig};
use rmcp::transport::streamable_http_server::session::never::NeverSessionManager;
use rmcp::transport::streamable_http_server::{StreamableHttpServerConfig, StreamableHttpService};
use rmcp::{ServerHandler, tool_handler};
use super::auth::AuthUser;
use super::error::{AppError, AppResult};
use super::state::AppState;
use crate::modules;
/// Path the MCP endpoint is mounted at.
pub const PATH: &str = "/mcp";
/// Handles MCP requests. A cheap clone is made for every request.
#[derive(Clone)]
pub struct McpServer {
pub state: AppState,
tools: Arc<ToolRouter<McpServer>>,
}
impl McpServer {
/// Resolves the caller from the request's `Authorization: Bearer` header.
/// Tokens of customer-facing apps are refused (`Forbidden`), so every tool
/// is for the platform's apps unless it opts in with
/// [`Self::authenticate_customer`].
pub async fn authenticate(&self, parts: &Parts) -> AppResult<AuthUser> {
let user = self.authenticate_customer(parts).await?;
if user.customer {
return Err(AppError::Forbidden);
}
Ok(user)
}
/// Like [`Self::authenticate`], but also accepts tokens of customer-facing
/// apps (storefronts). Use it only in tools meant for customers.
pub async fn authenticate_customer(&self, parts: &Parts) -> AppResult<AuthUser> {
AuthUser::authenticate(&self.state, &parts.headers).await
}
/// Like [`Self::authenticate`], but also requires the `super_admin` role.
pub async fn authenticate_super_admin(&self, parts: &Parts) -> AppResult<AuthUser> {
let user = self.authenticate(parts).await?;
user.require_super_admin()?;
Ok(user)
}
}
#[tool_handler(router = self.tools)]
impl ServerHandler for McpServer {
fn get_info(&self) -> ServerConfig {
ServerConfig::new(ServerCapabilities::builder().enable_tools().build())
.with_server_info(Implementation::new(
self.state.config.name.clone(),
env!("CARGO_PKG_VERSION"),
))
.with_instructions(
"Sign in with `auth_email_start` and `auth_email_verify` (a code sent by \
email) and send the session token as `Authorization: Bearer <token>` on \
every request. The first user to sign in becomes super admin.",
)
}
}
/// The Streamable HTTP service to mount at [`PATH`].
pub fn service(state: AppState) -> StreamableHttpService<McpServer, NeverSessionManager> {
let config = StreamableHttpServerConfig::default()
.with_legacy_session_mode(false)
.with_json_response(true)
.with_sse_keep_alive(None)
.with_allowed_hosts(state.config.mcp_allowed_hosts.clone());
let server = McpServer {
state,
tools: Arc::new(modules::tools()),
};
StreamableHttpService::new(move || Ok(server.clone()), Default::default(), config)
}