mcp-confluence 1.0.0

MCP server for Confluence integration - create, update, search, and manage Confluence pages
use serde_json::Value;

use crate::client::ConfluenceClient;
use crate::mcp::{CallToolResult, ToolDefinition};
use crate::types::{ConfluencePage, ConfluencePageV1};

use super::schema;

pub fn definitions() -> Vec<ToolDefinition> {
    vec![ToolDefinition {
        name: "get_page_storage_format".to_string(),
        description: "Get the raw Confluence storage format (XML/HTML) of a page. Essential for making precise edits to complex pages with tables, macros, etc.".to_string(),
        input_schema: schema(&[
            ("pageId", "string", true, "The page ID to get storage format for"),
            ("section", "string", false, "Optional: Extract only a section containing this text (useful for large pages)"),
        ]),
    }]
}

pub async fn get_page_storage_format(client: &ConfluenceClient, args: &Value) -> CallToolResult {
    let page_id = match args.get("pageId").and_then(|v| v.as_str()) {
        Some(id) => id,
        None => return CallToolResult::text("Error: pageId is required."),
    };
    let section = args.get("section").and_then(|v| v.as_str());

    let fetch_result: Result<(String, String, i64), String> = async {
        if client.config().is_cloud {
            let page: ConfluencePage = client
                .get(&format!("/pages/{page_id}?body-format=storage"))
                .await?;
            let body = page
                .body
                .and_then(|b| b.storage)
                .map_or(String::new(), |s| s.value);
            let ver = page.version.map_or(1, |v| v.number);
            Ok((page.title, body, ver))
        } else {
            let page: ConfluencePageV1 = client
                .get(&format!(
                    "/content/{page_id}?expand=body.storage,version"
                ))
                .await?;
            let body = page
                .body
                .and_then(|b| b.storage)
                .map_or(String::new(), |s| s.value);
            let ver = page.version.map_or(1, |v| v.number);
            Ok((page.title, body, ver))
        }
    }
    .await;

    match fetch_result {
        Ok((title, body, version)) => {
            if body.is_empty() {
                return CallToolResult::text(format!("Page \"{title}\" has no content."));
            }

            let output_content = if let Some(sec) = section {
                let lower_body = body.to_lowercase();
                let lower_sec = sec.to_lowercase();
                if let Some(idx) = lower_body.find(&lower_sec) {
                    let before = &body[..idx];
                    let after = &body[idx..];

                    let tr_start = before.rfind("<tr").unwrap_or(0);
                    let table_start = before.rfind("<table").unwrap_or(0);
                    let start = tr_start.max(table_start).max(idx.saturating_sub(2000));

                    let tr_end = after.find("</tr>").map(|p| idx + p + 5);
                    let end = tr_end.unwrap_or_else(|| (idx + 5000).min(body.len()));

                    body[start..end].to_string()
                } else {
                    body.clone()
                }
            } else {
                body.clone()
            };

            let section_note = section
                .map(|s| format!("| **Extracted Section** | Around \"{s}\" |\n"))
                .unwrap_or_default();

            CallToolResult::text(format!(
                "# Storage Format: {title}\n\n| Property | Value |\n|----------|-------|\n| **Page ID** | {page_id} |\n| **Current Version** | {version} |\n| **Content Length** | {} characters |\n{section_note}\n## Raw Storage Format\n\n```xml\n{output_content}\n```\n\n---\n💡 Use `update_page` with this storage format to make edits. Remember to increment the version.",
                body.len(),
            ))
        }
        Err(e) => CallToolResult::text(format!("Error getting storage format: {e}")),
    }
}