//! 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()))
}
}