use rmcp::schemars::JsonSchema;
use serde::Deserialize;
use serde_json::Value;
use tailscale_rest::models::user::{USER_ROLE_FILTERS, USER_ROLES, USER_TYPE_FILTERS};
use crate::context::ToolContext;
use crate::error::ToolResult;
use crate::tools::common::{Done, answered_or, one_of, path_segment};
crate::tools! {
tailnet_user_list => UserListParams, user_list,
toolset: TailnetUsers, tier: Read, idempotent: true;
tailnet_user_get => UserParams, user_get,
toolset: TailnetUsers, tier: Read, idempotent: true;
tailnet_user_role_set => UserRoleParams, user_role_set,
toolset: TailnetUsers, tier: Write, idempotent: true;
tailnet_user_approve => UserParams, user_approve,
toolset: TailnetUsers, tier: Write, idempotent: true;
tailnet_user_suspend => UserParams, user_suspend,
toolset: TailnetUsers, tier: Write, idempotent: true;
tailnet_user_restore => UserParams, user_restore,
toolset: TailnetUsers, tier: Write, idempotent: true;
tailnet_user_delete => UserParams, user_delete,
toolset: TailnetUsers, tier: Destructive, idempotent: true;
}
fn user_path(id: &str, rest: &str) -> ToolResult<String> {
let id = path_segment("user_id", id)?;
Ok(format!("/api/v2/users/{id}{rest}"))
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct UserListParams {
#[serde(default)]
pub user_type: Option<String>,
#[serde(default)]
pub role: Option<String>,
}
async fn user_list(ctx: &ToolContext, params: UserListParams) -> ToolResult<Value> {
let client = ctx.tailnet()?;
let user_type = params
.user_type
.as_deref()
.map(|value| one_of("user_type", value, USER_TYPE_FILTERS))
.transpose()?;
let role = params
.role
.as_deref()
.map(|value| one_of("role", value, USER_ROLE_FILTERS))
.transpose()?;
Ok(client
.get(client.tailnet_path(None, "/users"))
.maybe_query("type", user_type)
.maybe_query("role", role)
.send_as::<Value>()
.await?)
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct UserParams {
pub user_id: String,
}
async fn user_get(ctx: &ToolContext, params: UserParams) -> ToolResult<Value> {
let client = ctx.tailnet()?;
Ok(client
.get(user_path(¶ms.user_id, "")?)
.send_as::<Value>()
.await?)
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct UserRoleParams {
pub user_id: String,
pub role: String,
}
async fn user_role_set(ctx: &ToolContext, params: UserRoleParams) -> ToolResult<Value> {
let client = ctx.tailnet()?;
let path = user_path(¶ms.user_id, "/role")?;
let mut body = tailscale_rest::models::user::UserRole::default();
body.role = Some(one_of("role", ¶ms.role, USER_ROLES)?);
let answer = client.post(path).json(&body).send().await?;
answered_or(
answer,
Done::new("role changed")
.about("user_id", params.user_id)
.about("role", params.role),
)
}
async fn standing(
ctx: &ToolContext,
user_id: String,
action: &'static str,
done: &'static str,
) -> ToolResult<Value> {
let client = ctx.tailnet()?;
let path = user_path(&user_id, &format!("/{action}"))?;
let answer = client.post(path).send().await?;
answered_or(answer, Done::new(done).about("user_id", user_id))
}
async fn user_approve(ctx: &ToolContext, params: UserParams) -> ToolResult<Value> {
standing(ctx, params.user_id, "approve", "user approved").await
}
async fn user_suspend(ctx: &ToolContext, params: UserParams) -> ToolResult<Value> {
standing(ctx, params.user_id, "suspend", "user suspended").await
}
async fn user_restore(ctx: &ToolContext, params: UserParams) -> ToolResult<Value> {
standing(ctx, params.user_id, "restore", "user restored").await
}
async fn user_delete(ctx: &ToolContext, params: UserParams) -> ToolResult<Value> {
standing(ctx, params.user_id, "delete", "user deleted").await
}