wowlab_sentinel/mcp/tools/
loadout.rs1use schemars::JsonSchema;
2use serde::Deserialize;
3use sqlx::PgPool;
4use wowlab_loadout::{EnrichedLoadout, apply_decoded_traits, decode_trait_loadout, enrich_loadout};
5use wowlab_types::data::TraitTreeFlat;
6
7#[derive(Debug, Default, Deserialize, JsonSchema)]
9#[schemars(
10 description = "Decode a WoW talent loadout string into selected talents with spell IDs, names, and ranks"
11)]
12pub(crate) struct DecodeLoadoutInput {
13 #[schemars(
14 description = "Base64-encoded talent loadout string (from SimC `talents=` or the game client export)"
15 )]
16 loadout: String,
17}
18
19pub(super) async fn execute(
21 db: &PgPool,
22 input: DecodeLoadoutInput,
23) -> Result<EnrichedLoadout, super::ToolError> {
24 let decoded = decode_trait_loadout(&input.loadout).map_err(|error| {
25 super::ToolError::invalid_params(format!("Failed to decode loadout string: {error}"))
26 })?;
27
28 let spec_id = i32::from(decoded.spec_id);
29
30 let row: Option<(serde_json::Value,)> =
31 sqlx::query_as("SELECT row_to_json(t) FROM game.specs_traits t WHERE spec_id = $1 LIMIT 1")
32 .bind(spec_id)
33 .fetch_optional(db)
34 .await
35 .map_err(|error| {
36 super::ToolError::database(
37 format!("Failed to fetch trait tree for spec_id {spec_id}"),
38 error,
39 )
40 })?;
41 let row = row.ok_or_else(|| unknown_spec_error(spec_id))?;
42
43 let tree: TraitTreeFlat = serde_json::from_value(row.0)
44 .map_err(|error| super::ToolError::internal("Failed to parse trait tree", error))?;
45
46 let with_selections = apply_decoded_traits(tree, &decoded);
47
48 Ok(enrich_loadout(&with_selections))
49}
50
51fn unknown_spec_error(spec_id: i32) -> super::ToolError {
52 super::ToolError::invalid_params(format!(
53 "Unknown loadout spec_id {spec_id}: game.specs_traits has no matching row"
54 ))
55}
56
57pub(super) async fn handle(
58 db: &PgPool,
59 params: DecodeLoadoutInput,
60) -> Result<rmcp::model::CallToolResult, rmcp::ErrorData> {
61 Ok(crate::mcp::tool_result(
62 crate::telemetry::McpTool::DecodeLoadout,
63 execute(db, params).await,
64 ))
65}
66
67#[cfg(test)]
68mod tests {
69 use googletest::prelude::*;
70
71 use super::*;
72 use crate::{mcp::McpFailure, telemetry::McpErrorType};
73
74 #[gtest]
75 fn unknown_spec_is_an_actionable_invalid_parameter_error() {
76 let error = unknown_spec_error(33_028);
77
78 assert_eq!(error.error_type(), McpErrorType::InvalidParams);
79 assert_eq!(
80 error.to_string(),
81 "Unknown loadout spec_id 33028: game.specs_traits has no matching row"
82 );
83 }
84}