Skip to main content

wowlab_sentinel/mcp/tools/
mod.rs

1mod docs;
2mod effects;
3mod labels;
4mod loadout;
5mod parse_simc;
6mod query;
7mod rotations;
8
9use super::schema;
10use crate::mcp::mcp_tool;
11
12#[derive(Debug, thiserror::Error)]
13#[error("{kind}")]
14pub(super) struct ToolError {
15    kind: ToolErrorKind,
16}
17
18#[derive(Debug, thiserror::Error)]
19enum ToolErrorKind {
20    #[error("{message}")]
21    Message { message: String },
22}
23
24impl From<String> for ToolError {
25    fn from(message: String) -> Self {
26        Self {
27            kind: ToolErrorKind::Message { message },
28        }
29    }
30}
31
32impl From<&str> for ToolError {
33    fn from(message: &str) -> Self {
34        message.to_owned().into()
35    }
36}
37
38mcp_tool!(
39    get_docs,
40    1,
41    "Read project documentation. Returns the index by default, pass `full=true` for full content.",
42    crate::mcp::GetDocsInput,
43    tips = ["Use `get_docs` first to understand the project before querying game data."],
44    |srv, params| async { docs::handle(&srv.mcp, params).await }
45);
46
47mcp_tool!(
48    list_tables,
49    2,
50    "List all tables with category, description, and column count.",
51    crate::mcp::EmptyInput,
52    tips = ["Call `list_tables` before `get_schema` to avoid large responses."],
53    |srv, _params| async {
54        Ok(crate::mcp::json_result(
55            srv.mcp.schema.list_tables_compact(),
56        ))
57    }
58);
59
60mcp_tool!(
61    get_schema,
62    3,
63    "Get column details for a table. Pass a `table` name to see columns and types, omit for all.",
64    schema::GetSchemaInput,
65    tips = [],
66    |srv, params| async {
67        srv.mcp
68            .schema
69            .get_schema(params.table.as_deref())
70            .map(crate::mcp::json_result)
71            .map_err(|e| crate::mcp::mcp_error(crate::telemetry::McpTool::GetSchema, &e))
72    }
73);
74
75mcp_tool!(
76    query,
77    4,
78    "Fetch rows from a game data table with filters, sorting, and pagination.",
79    query::TableQuery,
80    tips = [
81        "`op=contains` for fuzzy name search, `op=in` for ID lists.",
82        "All filters are AND-ed. Use `query_batch` for multiple tables.",
83        "Rows include every column. For tables with many `json` columns (see `get_schema`), run `query_count` first or narrow the `limit` — large payloads can be rejected by some MCP clients.",
84    ],
85    |srv, params| async { query::handle_query(&srv.db, &srv.mcp.schema, params).await }
86);
87
88mcp_tool!(
89    query_batch,
90    5,
91    "Execute multiple queries in one request (max 50). Each runs independently.",
92    query::TableBatchQuery,
93    tips = [],
94    |srv, params| async { query::handle_query_batch(&srv.db, &srv.mcp.schema, params).await }
95);
96
97mcp_tool!(
98    query_count,
99    6,
100    "Count rows matching filters without returning row data. Same filters as `query`.",
101    query::CountQuery,
102    tips = ["Use `query_count` to check result size before fetching rows."],
103    |srv, params| async { query::handle_query_count(&srv.db, &srv.mcp.schema, params).await }
104);
105
106mcp_tool!(
107    resolve_effects,
108    7,
109    "Resolve spell and item effects with trigger spell chains (max 50 IDs).",
110    effects::ResolveEffectsInput,
111    tips = ["Use `resolve_effects` to understand what a spell or item does mechanically."],
112    |srv, params| async { effects::handle(&srv.db, params).await }
113);
114
115mcp_tool!(
116    decode_loadout,
117    8,
118    "Decode a base64 talent loadout string into structured talent data with spell names.",
119    loadout::DecodeLoadoutInput,
120    tips = ["Talent strings come from SimC `talents=` lines or game client export."],
121    |srv, params| async { loadout::handle(&srv.db, params).await }
122);
123
124mcp_tool!(
125    get_rotation,
126    9,
127    "Fetch a rotation script by ID, optionally at a specific version.",
128    rotations::GetRotationInput,
129    tips = ["Use `list_rotations` to find rotation IDs, then `get_rotation` to fetch the script."],
130    |srv, params| async { rotations::handle_get(&srv.db, params).await }
131);
132
133mcp_tool!(
134    list_spell_labels,
135    10,
136    "List all known spell label IDs with their names. Use these IDs to filter `game.spells` by the `labels` jsonb column (e.g. `op=jsonb_array_contains, value=20` for Rogue spells).",
137    labels::ListSpellLabelsInput,
138    tips = [
139        "Filter spells by label: query `game.spells` with `column=labels, op=jsonb_array_contains, value=<label_id>`."
140    ],
141    |_srv, params| async { labels::handle(params).await }
142);
143
144mcp_tool!(
145    list_rotations,
146    11,
147    "List accessible rotation scripts with optional filters by spec and name.",
148    rotations::ListRotationsInput,
149    tips = [
150        "Use `list_rotations` to find available rotations, then `get_rotation` to fetch the script."
151    ],
152    |srv, params| async { rotations::handle_list(&srv.db, params).await }
153);
154
155mcp_tool!(
156    parse_simc,
157    12,
158    "Parse a SimulationCraft profile string into structured character, equipment, talents, and metadata.",
159    parse_simc::ParseSimcInput,
160    tips = [
161        "Use `parse_simc` to ingest a user-pasted `/simc` export before querying or simulating it."
162    ],
163    |_srv, params| async { parse_simc::handle(params).await }
164);