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
//! MCP tools for teams and their members. Business rules live in
//! `TeamService` and `access.rs`.

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 crate::base::error::AppResult;
use crate::base::mcp::McpServer;
use crate::modules::team::dto::team::{
    CreateTeamRequest, CustomerJoinRequest, CustomerListResponse, CustomerResponse,
    InvitationListResponse, InvitationRequest, InvitationResponse, InviteRequest,
    MemberListResponse, ModulesResponse, RemoveMemberRequest, RevokeInviteRequest, StoreResponse,
    TeamListResponse, TeamRequest, TeamResponse, UpdateMemberRequest,
};
use crate::modules::team::services::team_service::TeamService;
use crate::modules::team::{ADMIN, MODULES, OWNER, STAFF};

pub fn tools() -> ToolRouter<McpServer> {
    McpServer::team_tools()
}

#[tool_router(router = team_tools, vis = "pub(crate)")]
impl McpServer {
    /// The teams the caller works on, with their role in each (every team
    /// for super admins).
    #[tool(annotations(read_only_hint = true, open_world_hint = false))]
    async fn team_list(
        &self,
        Extension(parts): Extension<Parts>,
    ) -> AppResult<Json<TeamListResponse>> {
        let user = self.authenticate(&parts).await?;
        let teams = TeamService::from_ref(&self.state).list(&user).await?;
        Ok(Json(TeamListResponse {
            teams: teams.into_iter().map(TeamResponse::from).collect(),
        }))
    }

    /// Create a team; the caller becomes its owner.
    #[tool(annotations(open_world_hint = false))]
    async fn team_create(
        &self,
        Extension(parts): Extension<Parts>,
        Parameters(input): Parameters<CreateTeamRequest>,
    ) -> AppResult<Json<TeamResponse>> {
        let user = self.authenticate(&parts).await?;
        let created = TeamService::from_ref(&self.state)
            .create(&user, &input.name, &input.slug)
            .await?;
        Ok(Json(created.into()))
    }

    /// A team and what the caller may do there (members and super admins).
    #[tool(annotations(read_only_hint = true, open_world_hint = false))]
    async fn team_get(
        &self,
        Extension(parts): Extension<Parts>,
        Parameters(input): Parameters<TeamRequest>,
    ) -> AppResult<Json<TeamResponse>> {
        let user = self.authenticate(&parts).await?;
        let team = TeamService::from_ref(&self.state)
            .get(&user, input.team_id)
            .await?;
        Ok(Json(team.into()))
    }

    /// The team roles and the modules staff can be given.
    #[tool(annotations(read_only_hint = true, open_world_hint = false))]
    async fn team_modules(&self) -> AppResult<Json<ModulesResponse>> {
        Ok(Json(ModulesResponse {
            roles: [OWNER, ADMIN, STAFF].map(String::from).to_vec(),
            modules: MODULES.iter().map(|m| (*m).to_owned()).collect(),
        }))
    }

    /// Become a customer of a store (a team, by slug); repeat calls are
    /// harmless. For customer-facing apps: call it after sign-in.
    #[tool(annotations(idempotent_hint = true, open_world_hint = false))]
    async fn customer_join(
        &self,
        Extension(parts): Extension<Parts>,
        Parameters(input): Parameters<CustomerJoinRequest>,
    ) -> AppResult<Json<StoreResponse>> {
        let user = self.authenticate_customer(&parts).await?;
        let team = TeamService::from_ref(&self.state)
            .join_as_customer(&user, &input.slug)
            .await?;
        Ok(Json(StoreResponse {
            id: team.id,
            name: team.name,
            slug: team.slug,
        }))
    }

    /// A team's customers (owners, admins and staff with `customers`).
    #[tool(annotations(read_only_hint = true, open_world_hint = false))]
    async fn customer_list(
        &self,
        Extension(parts): Extension<Parts>,
        Parameters(input): Parameters<TeamRequest>,
    ) -> AppResult<Json<CustomerListResponse>> {
        let user = self.authenticate(&parts).await?;
        let users = TeamService::from_ref(&self.state)
            .customers(&user, input.team_id)
            .await?;
        Ok(Json(CustomerListResponse {
            customers: users
                .into_iter()
                .map(|user| CustomerResponse {
                    user_id: user.id,
                    name: user.name,
                    email: user.email,
                })
                .collect(),
        }))
    }

    /// The members of a team (any member may see them).
    #[tool(annotations(read_only_hint = true, open_world_hint = false))]
    async fn member_list(
        &self,
        Extension(parts): Extension<Parts>,
        Parameters(input): Parameters<TeamRequest>,
    ) -> AppResult<Json<MemberListResponse>> {
        let user = self.authenticate(&parts).await?;
        let members = TeamService::from_ref(&self.state)
            .members(&user, input.team_id)
            .await?;
        Ok(Json(members.into()))
    }

    /// Invite someone to a team by email (owners and admins; only owners
    /// invite owners). They join by accepting after signing in with that
    /// email. Returns the team's pending invitations.
    #[tool(annotations(open_world_hint = false))]
    async fn invite_create(
        &self,
        Extension(parts): Extension<Parts>,
        Parameters(input): Parameters<InviteRequest>,
    ) -> AppResult<Json<InvitationListResponse>> {
        let user = self.authenticate(&parts).await?;
        let invitations = TeamService::from_ref(&self.state)
            .invite(
                &user,
                input.team_id,
                &input.email,
                input.role.trim(),
                input.modules,
            )
            .await?;
        Ok(Json(invitations.into()))
    }

    /// A team's pending invitations (owners and admins).
    #[tool(annotations(read_only_hint = true, open_world_hint = false))]
    async fn invite_list(
        &self,
        Extension(parts): Extension<Parts>,
        Parameters(input): Parameters<TeamRequest>,
    ) -> AppResult<Json<InvitationListResponse>> {
        let user = self.authenticate(&parts).await?;
        let invitations = TeamService::from_ref(&self.state)
            .invitations(&user, input.team_id)
            .await?;
        Ok(Json(invitations.into()))
    }

    /// Withdraw a pending invitation (owners and admins).
    #[tool(annotations(destructive_hint = true, open_world_hint = false))]
    async fn invite_revoke(
        &self,
        Extension(parts): Extension<Parts>,
        Parameters(input): Parameters<RevokeInviteRequest>,
    ) -> AppResult<Json<InvitationListResponse>> {
        let user = self.authenticate(&parts).await?;
        let invitations = TeamService::from_ref(&self.state)
            .revoke_invitation(&user, input.team_id, input.invitation_id)
            .await?;
        Ok(Json(invitations.into()))
    }

    /// Invitations for the caller's (verified) email.
    #[tool(annotations(read_only_hint = true, open_world_hint = false))]
    async fn invite_mine(
        &self,
        Extension(parts): Extension<Parts>,
    ) -> AppResult<Json<InvitationListResponse>> {
        let user = self.authenticate(&parts).await?;
        let mine = TeamService::from_ref(&self.state)
            .my_invitations(&user)
            .await?;
        Ok(Json(InvitationListResponse {
            invitations: mine
                .into_iter()
                .map(|(invitation, team)| InvitationResponse::new(invitation, Some(team)))
                .collect(),
        }))
    }

    /// Accept an invitation sent to the caller's verified email and join the team.
    #[tool(annotations(open_world_hint = false))]
    async fn invite_accept(
        &self,
        Extension(parts): Extension<Parts>,
        Parameters(input): Parameters<InvitationRequest>,
    ) -> AppResult<Json<TeamResponse>> {
        let user = self.authenticate(&parts).await?;
        let joined = TeamService::from_ref(&self.state)
            .accept(&user, input.invitation_id)
            .await?;
        Ok(Json(joined.into()))
    }

    /// Decline an invitation sent to the caller's verified email.
    #[tool(annotations(destructive_hint = true, open_world_hint = false))]
    async fn invite_decline(
        &self,
        Extension(parts): Extension<Parts>,
        Parameters(input): Parameters<InvitationRequest>,
    ) -> AppResult<Json<InvitationListResponse>> {
        let user = self.authenticate(&parts).await?;
        let service = TeamService::from_ref(&self.state);
        service.decline(&user, input.invitation_id).await?;
        let mine = service.my_invitations(&user).await?;
        Ok(Json(InvitationListResponse {
            invitations: mine
                .into_iter()
                .map(|(invitation, team)| InvitationResponse::new(invitation, Some(team)))
                .collect(),
        }))
    }

    /// Change a member's role and, for staff, their modules (owners and
    /// admins; only owners touch owners).
    #[tool(annotations(idempotent_hint = true, open_world_hint = false))]
    async fn member_update(
        &self,
        Extension(parts): Extension<Parts>,
        Parameters(input): Parameters<UpdateMemberRequest>,
    ) -> AppResult<Json<MemberListResponse>> {
        let user = self.authenticate(&parts).await?;
        let members = TeamService::from_ref(&self.state)
            .update_member(
                &user,
                input.team_id,
                input.user_id,
                input.role.trim(),
                input.modules,
            )
            .await?;
        Ok(Json(members.into()))
    }

    /// Remove a member from a team. A team keeps at least one owner.
    #[tool(annotations(destructive_hint = true, open_world_hint = false))]
    async fn member_remove(
        &self,
        Extension(parts): Extension<Parts>,
        Parameters(input): Parameters<RemoveMemberRequest>,
    ) -> AppResult<Json<MemberListResponse>> {
        let user = self.authenticate(&parts).await?;
        let members = TeamService::from_ref(&self.state)
            .remove_member(&user, input.team_id, input.user_id)
            .await?;
        Ok(Json(members.into()))
    }
}