Skip to main content

wowlab_sentinel/mcp/tools/
loadout.rs

1use 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/// Input for the talent loadout decoder MCP tool.
8#[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
19/// Decode a talent loadout string and resolve talent names from the database.
20pub(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}