wowlab_sentinel/mcp/tools/
parse_simc.rs1use schemars::JsonSchema;
2use serde::Deserialize;
3use wowlab_parsers::parse_simc;
4
5#[derive(Debug, Default, Deserialize, JsonSchema)]
7#[schemars(
8 description = "Parse a SimulationCraft profile string into structured character, equipment, talents, and metadata."
9)]
10pub(crate) struct ParseSimcInput {
11 #[schemars(
12 description = "Raw SimC profile text, e.g. the output of `/simc` in-game or a saved .simc file."
13 )]
14 profile: String,
15}
16
17#[expect(
18 clippy::unused_async,
19 reason = "MCP route handlers share one asynchronous interface"
20)]
21pub(super) async fn handle(
22 params: ParseSimcInput,
23) -> Result<rmcp::model::CallToolResult, rmcp::ErrorData> {
24 let profile = parse_simc(¶ms.profile).map_err(|error| {
25 super::ToolError::invalid_params(format!(
26 "Failed to parse SimC profile: {error}. Pass the raw `/simc` export in the `profile` string; do not pass a JSON object or JSON-encode the profile text."
27 ))
28 });
29
30 Ok(crate::mcp::tool_result(
31 crate::telemetry::McpTool::ParseSimc,
32 profile,
33 ))
34}
35
36#[cfg(test)]
37mod tests {
38 use googletest::prelude::*;
39 use rmcp::model::ContentBlock;
40
41 use super::*;
42
43 #[gtest]
44 #[tokio::test]
45 async fn invalid_profile_error_explains_the_input_contract() -> Result<()> {
46 let result = handle(ParseSimcInput {
47 profile: r#"{"profile":"not raw SimC"}"#.to_owned(),
48 })
49 .await?;
50 let text = result
51 .content
52 .first()
53 .and_then(ContentBlock::as_text)
54 .or_fail()?;
55
56 verify_that!(result.is_error, some(eq(true)))?;
57 verify_that!(
58 text.text.as_str(),
59 contains_substring("Invalid token at byte 0")
60 )?;
61 verify_that!(
62 text.text.as_str(),
63 contains_substring("raw `/simc` export in the `profile` string")
64 )?;
65
66 verify_that!(
67 text.text.as_str(),
68 contains_substring("do not pass a JSON object")
69 )
70 }
71}