wowlab_sentinel/mcp/tools/
rotations.rs1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use sqlx::PgPool;
4
5const DEFAULT_LIMIT: i64 = 20;
6const MAX_LIMIT: i64 = 50;
7
8#[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#[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}