Skip to main content

wowlab_sentinel/mcp/tools/
rotations.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use sqlx::PgPool;
4
5const DEFAULT_LIMIT: i64 = 20;
6const MAX_LIMIT: i64 = 50;
7
8/// Input for fetching a single rotation by ID.
9#[derive(Debug, Default, Deserialize, JsonSchema)]
10#[schemars(description = "Fetch a rotation script by ID, optionally at a specific version")]
11pub(super) struct GetRotationInput {
12    #[schemars(description = "Rotation UUID (from `list_rotations`).")]
13    pub id: String,
14
15    #[serde(default)]
16    #[schemars(description = "Version number to fetch. Omit for the current/latest version.")]
17    pub version: Option<i32>,
18}
19
20/// Input for listing/searching public rotations.
21#[derive(Debug, Default, Deserialize, JsonSchema)]
22#[schemars(description = "List accessible rotation scripts with optional filters")]
23pub(super) struct ListRotationsInput {
24    #[serde(default)]
25    #[schemars(description = "Filter by spec ID (e.g. 262 for Enhancement Shaman).")]
26    pub spec_id: Option<i32>,
27
28    #[serde(default)]
29    #[schemars(description = "Search rotation names (case-insensitive substring match).")]
30    pub name: Option<String>,
31
32    #[serde(default)]
33    #[schemars(description = "Max results to return. Default: 20, max: 50.")]
34    pub limit: Option<i64>,
35
36    #[serde(default)]
37    #[schemars(description = "Offset for pagination.")]
38    pub offset: Option<i64>,
39}
40
41#[derive(Debug, Serialize, sqlx::FromRow)]
42struct RotationRow {
43    id: uuid::Uuid,
44    slug: String,
45    name: String,
46    script: serde_json::Value,
47    description: Option<String>,
48    spec_id: i32,
49    current_version: i32,
50    forked_from_id: Option<uuid::Uuid>,
51    created_at: chrono::DateTime<chrono::Utc>,
52    updated_at: chrono::DateTime<chrono::Utc>,
53}
54
55#[derive(Debug, Serialize, sqlx::FromRow)]
56struct RotationListRow {
57    id: uuid::Uuid,
58    slug: String,
59    name: String,
60    description: Option<String>,
61    spec_id: i32,
62    spec_name: Option<String>,
63    class_name: Option<String>,
64    current_version: i32,
65    created_at: chrono::DateTime<chrono::Utc>,
66    updated_at: chrono::DateTime<chrono::Utc>,
67}
68
69#[derive(Debug, Serialize, sqlx::FromRow)]
70struct RotationVersionRow {
71    id: uuid::Uuid,
72    slug: String,
73    name: String,
74    script: serde_json::Value,
75    description: Option<String>,
76    spec_id: i32,
77    version: i32,
78    message: Option<String>,
79    created_at: chrono::DateTime<chrono::Utc>,
80}
81
82pub(super) async fn handle_get(
83    db: &PgPool,
84    params: GetRotationInput,
85) -> Result<rmcp::model::CallToolResult, rmcp::ErrorData> {
86    let result = execute_get(db, params)
87        .await
88        .map_err(|e| crate::mcp::mcp_error(crate::telemetry::McpTool::GetRotation, &e))?;
89
90    Ok(crate::mcp::json_result(result))
91}
92
93pub(super) async fn handle_list(
94    db: &PgPool,
95    params: ListRotationsInput,
96) -> Result<rmcp::model::CallToolResult, rmcp::ErrorData> {
97    let result = execute_list(db, params)
98        .await
99        .map_err(|e| crate::mcp::mcp_error(crate::telemetry::McpTool::ListRotations, &e))?;
100
101    Ok(crate::mcp::json_result(result))
102}
103
104async fn execute_get(
105    db: &PgPool,
106    input: GetRotationInput,
107) -> Result<serde_json::Value, super::ToolError> {
108    let id = input
109        .id
110        .parse::<uuid::Uuid>()
111        .map_err(|e| format!("Invalid UUID for `id`: {e}"))?;
112    let user_id = crate::mcp::current_user_id();
113
114    if let Some(version) = input.version {
115        let row = sqlx::query_file_as!(
116            RotationVersionRow,
117            "queries/mcp_get_rotation_version_by_id.sql",
118            id,
119            version,
120            user_id
121        )
122        .fetch_optional(db)
123        .await
124        .map_err(|e| format!("Database error: {e}"))?;
125
126        match row {
127            Some(r) => serde_json::to_value(r).map_err(|e| format!("Serialize error: {e}").into()),
128            None => Err("Rotation not found".into()),
129        }
130    } else {
131        let row = sqlx::query_file_as!(
132            RotationRow,
133            "queries/mcp_get_rotation_by_id.sql",
134            id,
135            user_id
136        )
137        .fetch_optional(db)
138        .await
139        .map_err(|e| format!("Database error: {e}"))?;
140
141        match row {
142            Some(r) => serde_json::to_value(r).map_err(|e| format!("Serialize error: {e}").into()),
143            None => Err("Rotation not found".into()),
144        }
145    }
146}
147
148async fn execute_list(
149    db: &PgPool,
150    input: ListRotationsInput,
151) -> Result<Vec<RotationListRow>, super::ToolError> {
152    let user_id = crate::mcp::current_user_id();
153    let limit = input.limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
154    let offset = input.offset.unwrap_or(0).max(0);
155
156    let rows = sqlx::query_file_as!(
157        RotationListRow,
158        "queries/mcp_list_rotations.sql",
159        input.spec_id,
160        input.name.as_deref(),
161        limit,
162        offset,
163        user_id
164    )
165    .fetch_all(db)
166    .await
167    .map_err(|e| format!("Database error: {e}"))?;
168
169    Ok(rows)
170}