use anyhow::{Context, Result};
use rmcp::{
handler::server::wrapper::Parameters,
model::{CallToolResult, Content},
schemars, tool, tool_router, ErrorData as McpError,
};
use serde::Deserialize;
use crate::atlassian::adf::AdfDocument;
use crate::atlassian::convert::{adf_to_markdown_with_options, markdown_to_adf, RenderOptions};
use super::error::tool_error;
use super::server::OmniDevServer;
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct AtlassianConvertParams {
pub content: String,
pub direction: String,
#[serde(default)]
pub compact: Option<bool>,
#[serde(default)]
pub strip_local_ids: Option<bool>,
}
#[allow(missing_docs)] #[tool_router(router = atlassian_tool_router, vis = "pub")]
impl OmniDevServer {
#[tool(description = "Convert between JFM markdown and ADF JSON. \
Mirrors `omni-dev atlassian convert to-adf` / `from-adf`. \
`direction` must be either \"to-adf\" or \"from-adf\".")]
pub async fn atlassian_convert(
&self,
Parameters(params): Parameters<AtlassianConvertParams>,
) -> Result<CallToolResult, McpError> {
let output = run_convert(¶ms).map_err(tool_error)?;
Ok(CallToolResult::success(vec![Content::text(output)]))
}
}
fn run_convert(params: &AtlassianConvertParams) -> Result<String> {
match params.direction.as_str() {
"to-adf" => {
let doc = markdown_to_adf(¶ms.content)?;
let compact = params.compact.unwrap_or(false);
if compact {
serde_json::to_string(&doc).context("Failed to serialize ADF JSON")
} else {
serde_json::to_string_pretty(&doc).context("Failed to serialize ADF JSON")
}
}
"from-adf" => {
let doc = AdfDocument::from_json_str(¶ms.content)?;
let opts = RenderOptions {
strip_local_ids: params.strip_local_ids.unwrap_or(false),
};
adf_to_markdown_with_options(&doc, &opts)
}
other => anyhow::bail!("Invalid direction \"{other}\": must be \"to-adf\" or \"from-adf\""),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn to_adf_pretty_returns_multi_line_json() {
let params = AtlassianConvertParams {
content: "# Title\n\nBody text.".to_string(),
direction: "to-adf".to_string(),
compact: None,
strip_local_ids: None,
};
let out = run_convert(¶ms).unwrap();
assert!(out.contains("\"type\""));
assert!(out.contains('\n'));
}
#[test]
fn to_adf_compact_has_no_newlines() {
let params = AtlassianConvertParams {
content: "Plain body".to_string(),
direction: "to-adf".to_string(),
compact: Some(true),
strip_local_ids: None,
};
let out = run_convert(¶ms).unwrap();
assert!(out.contains("\"type\""));
assert!(!out.contains('\n'), "compact JSON must not have newlines");
}
#[test]
fn from_adf_returns_markdown() {
let adf = r#"{"version":1,"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Hello"}]}]}"#;
let params = AtlassianConvertParams {
content: adf.to_string(),
direction: "from-adf".to_string(),
compact: None,
strip_local_ids: None,
};
let out = run_convert(¶ms).unwrap();
assert!(out.contains("Hello"));
}
#[test]
fn from_adf_strip_local_ids_flag_plumbed_through() {
let adf = r#"{
"version": 1, "type": "doc",
"content": [{
"type": "paragraph",
"attrs": {"localId": "abc-123"},
"content": [{"type": "text", "text": "Body"}]
}]
}"#;
let params = AtlassianConvertParams {
content: adf.to_string(),
direction: "from-adf".to_string(),
compact: None,
strip_local_ids: Some(true),
};
let out = run_convert(¶ms).unwrap();
assert!(out.contains("Body"));
}
#[test]
fn from_adf_invalid_json_errors() {
let params = AtlassianConvertParams {
content: "not json".to_string(),
direction: "from-adf".to_string(),
compact: None,
strip_local_ids: None,
};
assert!(run_convert(¶ms).is_err());
}
#[test]
fn unknown_direction_errors() {
let params = AtlassianConvertParams {
content: "x".to_string(),
direction: "sideways".to_string(),
compact: None,
strip_local_ids: None,
};
let err = run_convert(¶ms).unwrap_err();
assert!(err.to_string().contains("direction"));
}
#[test]
fn tool_router_registers_atlassian_convert() {
let router = OmniDevServer::atlassian_tool_router();
assert!(router.has_route("atlassian_convert"));
}
use rmcp::handler::server::wrapper::Parameters;
#[tokio::test(flavor = "current_thread")]
async fn atlassian_convert_handler_to_adf_success() {
let server = OmniDevServer::new();
let result = server
.atlassian_convert(Parameters(AtlassianConvertParams {
content: "# Title\n\nBody.".to_string(),
direction: "to-adf".to_string(),
compact: None,
strip_local_ids: None,
}))
.await
.unwrap();
assert!(!result.is_error.unwrap_or(false));
}
#[tokio::test(flavor = "current_thread")]
async fn atlassian_convert_handler_invalid_direction_returns_tool_error() {
let server = OmniDevServer::new();
let result = server
.atlassian_convert(Parameters(AtlassianConvertParams {
content: "x".to_string(),
direction: "sideways".to_string(),
compact: None,
strip_local_ids: None,
}))
.await;
let err = result.unwrap_err();
assert!(err.message.contains("direction"));
}
}