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::formatters::format_space;
use crate::mcp::{CallToolResult, ToolDefinition};
use crate::types::{AnyPage, PageListResponse, PageListResponseV1, SpaceListResponse};

use super::schema;

pub fn definitions() -> Vec<ToolDefinition> {
    vec![
        ToolDefinition {
            name: "list_spaces".to_string(),
            description: "List all accessible Confluence spaces".to_string(),
            input_schema: schema(&[
                ("limit", "number", false, "Maximum number of spaces to return (default: 25)"),
                ("type", "string", false, "Filter by space type: global, personal"),
            ]),
        },
        ToolDefinition {
            name: "get_space_pages".to_string(),
            description: "Get all pages in a Confluence space".to_string(),
            input_schema: schema(&[
                ("spaceKey", "string", true, "The space key"),
                ("limit", "number", false, "Maximum number of pages to return (default: 25)"),
                ("status", "string", false, "Page status: current, archived, draft (default: current)"),
            ]),
        },
    ]
}

pub async fn list_spaces(client: &ConfluenceClient, args: &Value) -> CallToolResult {
    let limit = args.get("limit").and_then(|v| v.as_i64()).unwrap_or(25);
    let space_type = args.get("type").and_then(|v| v.as_str());

    let endpoint = if client.config().is_cloud {
        let mut url = format!("/spaces?limit={limit}");
        if let Some(t) = space_type {
            url.push_str(&format!("&type={t}"));
        }
        url
    } else {
        let mut url = format!("/space?limit={limit}");
        if let Some(t) = space_type {
            url.push_str(&format!("&type={t}"));
        }
        url
    };

    match client.get::<SpaceListResponse>(&endpoint).await {
        Ok(result) => {
            if result.results.is_empty() {
                return CallToolResult::text("No spaces found.");
            }
            let formatted: Vec<String> = result.results.iter().map(|s| format_space(s)).collect();
            CallToolResult::text(format!(
                "Found {} space(s):\n\n{}",
                result.results.len(),
                formatted.join("\n\n---\n\n")
            ))
        }
        Err(e) => CallToolResult::text(format!("Error listing spaces: {e}")),
    }
}

pub async fn get_space_pages(client: &ConfluenceClient, args: &Value) -> CallToolResult {
    let space_key = match args.get("spaceKey").and_then(|v| v.as_str()) {
        Some(k) => k,
        None => return CallToolResult::text("Error: spaceKey is required."),
    };
    let limit = args.get("limit").and_then(|v| v.as_i64()).unwrap_or(25);
    let status = args
        .get("status")
        .and_then(|v| v.as_str())
        .unwrap_or("current");

    if client.config().is_cloud {
        // Get space ID first
        let spaces_result: Result<SpaceListResponse, _> =
            client.get(&format!("/spaces?keys={space_key}")).await;
        match spaces_result {
            Ok(sr) => {
                if sr.results.is_empty() {
                    return CallToolResult::text(format!(
                        "Space with key \"{space_key}\" not found."
                    ));
                }
                let space_id = &sr.results[0].id;
                match client
                    .get::<PageListResponse>(&format!(
                        "/spaces/{space_id}/pages?limit={limit}&status={status}"
                    ))
                    .await
                {
                    Ok(result) => format_page_list(&result.results, space_key),
                    Err(e) => CallToolResult::text(format!("Error getting space pages: {e}")),
                }
            }
            Err(e) => CallToolResult::text(format!("Error getting space pages: {e}")),
        }
    } else {
        match client
            .get::<PageListResponseV1>(&format!(
                "/content?spaceKey={space_key}&limit={limit}&status={status}&expand=version,space"
            ))
            .await
        {
            Ok(result) => format_page_list_v1(&result.results, space_key),
            Err(e) => CallToolResult::text(format!("Error getting space pages: {e}")),
        }
    }
}

fn format_page_list(pages: &[crate::types::ConfluencePage], space_key: &str) -> CallToolResult {
    if pages.is_empty() {
        return CallToolResult::text(format!("No pages found in space {space_key}."));
    }
    let formatted: Vec<String> = pages
        .iter()
        .map(|p| crate::formatters::format_page(&AnyPage::V2(clone_page_v2(p))))
        .collect();
    CallToolResult::text(format!(
        "Found {} page(s) in space {space_key}:\n\n{}",
        pages.len(),
        formatted.join("\n\n---\n\n")
    ))
}

fn format_page_list_v1(
    pages: &[crate::types::ConfluencePageV1],
    space_key: &str,
) -> CallToolResult {
    if pages.is_empty() {
        return CallToolResult::text(format!("No pages found in space {space_key}."));
    }
    let formatted: Vec<String> = pages
        .iter()
        .map(|p| crate::formatters::format_page(&AnyPage::V1(clone_page_v1(p))))
        .collect();
    CallToolResult::text(format!(
        "Found {} page(s) in space {space_key}:\n\n{}",
        pages.len(),
        formatted.join("\n\n---\n\n")
    ))
}

// Simple clone helpers since we take references to deserialized data.
fn clone_page_v2(p: &crate::types::ConfluencePage) -> crate::types::ConfluencePage {
    serde_json::from_value(serde_json::to_value(p).unwrap()).unwrap()
}

fn clone_page_v1(p: &crate::types::ConfluencePageV1) -> crate::types::ConfluencePageV1 {
    serde_json::from_value(serde_json::to_value(p).unwrap()).unwrap()
}