slave 0.13.1

CLI for slave, a Laravel-inspired Rust framework for MCP-first apps: scaffold an MCP backend with auth, roles, teams and SurrealDB migrations, add server-rendered web apps, and route them by domain or path
//! 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)
}