use serde_json::Value;
use crate::client::ConfluenceClient;
use crate::formatters::format_page_detailed;
use crate::mcp::{CallToolResult, ToolDefinition};
use crate::types::{
AnyPage, ConfluencePage, ConfluencePageV1, CreatePageResponse, PageListResponseV1,
SearchResult, SpaceListResponse,
};
use super::schema;
pub fn definitions() -> Vec<ToolDefinition> {
vec![
ToolDefinition {
name: "get_page".to_string(),
description: "Fetch a Confluence page by its ID or title".to_string(),
input_schema: schema(&[
("pageId", "string", false, "The page ID (e.g., '123456')"),
("spaceKey", "string", false, "The space key (required if using title)"),
("title", "string", false, "The page title (requires spaceKey)"),
("includeBody", "boolean", false, "Whether to include the page body content (default: true)"),
]),
},
ToolDefinition {
name: "get_page_full".to_string(),
description: "Fetch complete Confluence page content in storage format (for automation, analysis, or export). Returns raw storage format without markdown conversion.".to_string(),
input_schema: schema(&[
("pageId", "string", false, "The page ID (e.g., '123456')"),
("spaceKey", "string", false, "The space key (required if using title)"),
("title", "string", false, "The page title (requires spaceKey)"),
]),
},
ToolDefinition {
name: "create_page".to_string(),
description: "Create a new Confluence page".to_string(),
input_schema: schema(&[
("spaceKey", "string", true, "The space key where the page will be created"),
("title", "string", true, "The title of the page"),
("content", "string", true, "The page content in HTML or storage format"),
("parentPageId", "string", false, "The ID of the parent page (for nested pages)"),
]),
},
ToolDefinition {
name: "update_page".to_string(),
description: "Update an existing Confluence page".to_string(),
input_schema: schema(&[
("pageId", "string", true, "The ID of the page to update"),
("title", "string", false, "New title for the page (optional)"),
("content", "string", true, "The new page content in HTML or storage format"),
("versionComment", "string", false, "A comment describing this update"),
]),
},
ToolDefinition {
name: "delete_page".to_string(),
description: "Delete a Confluence page".to_string(),
input_schema: schema(&[
("pageId", "string", true, "The ID of the page to delete"),
]),
},
]
}
pub async fn get_page(client: &ConfluenceClient, args: &Value) -> CallToolResult {
let page_id = args.get("pageId").and_then(|v| v.as_str());
let space_key = args.get("spaceKey").and_then(|v| v.as_str());
let title = args.get("title").and_then(|v| v.as_str());
let include_body = args.get("includeBody").and_then(|v| v.as_bool()).unwrap_or(true);
if page_id.is_none() && (space_key.is_none() || title.is_none()) {
return CallToolResult::text(
"Error: Please provide either pageId, or both spaceKey and title.",
);
}
let cfg = client.config();
let result: Result<AnyPage, String> = if cfg.is_cloud {
fetch_page_cloud(client, page_id, space_key, title, include_body).await
} else {
fetch_page_server(client, page_id, space_key, title, include_body).await
};
match result {
Ok(page) => CallToolResult::text(format_page_detailed(
&page,
&cfg.host,
cfg.is_cloud,
cfg.max_content_length,
)),
Err(e) => CallToolResult::text(format!("Error fetching page: {e}")),
}
}
pub async fn get_page_full(client: &ConfluenceClient, args: &Value) -> CallToolResult {
let page_id = args.get("pageId").and_then(|v| v.as_str());
let space_key = args.get("spaceKey").and_then(|v| v.as_str());
let title = args.get("title").and_then(|v| v.as_str());
if page_id.is_none() && (space_key.is_none() || title.is_none()) {
return CallToolResult::text(
"Error: Please provide either pageId, or both spaceKey and title.",
);
}
let cfg = client.config();
let result: Result<AnyPage, String> = if cfg.is_cloud {
fetch_page_cloud(client, page_id, space_key, title, true).await
} else {
fetch_page_server(client, page_id, space_key, title, true).await
};
match result {
Ok(page) => {
let storage = page.storage_value().to_string();
let size = storage.len();
let url = page.webui_link().map_or_else(
|| format!("{}/pages/{}", cfg.host, page.id()),
|w| {
if w.starts_with('/') {
let prefix = if cfg.is_cloud { "/wiki" } else { "" };
format!("{}{prefix}{w}", cfg.host)
} else {
w.to_string()
}
},
);
CallToolResult::text(format!(
"**Page:** {} (ID: {})\n**Version:** {}\n**Size:** {} bytes\n**URL:** {}\n\n---\n\n**Raw Storage Format:**\n\n```xml\n{}\n```",
page.title(),
page.id(),
page.version_number(),
size,
url,
storage,
))
}
Err(e) => CallToolResult::text(format!("Error fetching page: {e}")),
}
}
pub async fn create_page(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 title = match args.get("title").and_then(|v| v.as_str()) {
Some(t) => t,
None => return CallToolResult::text("Error: title is required."),
};
let content = match args.get("content").and_then(|v| v.as_str()) {
Some(c) => c,
None => return CallToolResult::text("Error: content is required."),
};
let parent_page_id = args.get("parentPageId").and_then(|v| v.as_str());
let cfg = client.config();
let result: Result<CreatePageResponse, String> = if cfg.is_cloud {
let spaces: SpaceListResponse = match client.get(&format!("/spaces?keys={space_key}")).await
{
Ok(s) => s,
Err(e) => return CallToolResult::text(format!("Error creating page: {e}")),
};
if spaces.results.is_empty() {
return CallToolResult::text(format!("Space with key \"{space_key}\" not found."));
}
let space_id = &spaces.results[0].id;
let mut body = serde_json::json!({
"spaceId": space_id,
"status": "current",
"title": title,
"body": {
"representation": "storage",
"value": content,
}
});
if let Some(pid) = parent_page_id {
body["parentId"] = Value::String(pid.to_string());
}
client.post("/pages", &body).await
} else {
let mut body = serde_json::json!({
"type": "page",
"title": title,
"space": { "key": space_key },
"body": {
"storage": {
"value": content,
"representation": "storage"
}
}
});
if let Some(pid) = parent_page_id {
body["ancestors"] = serde_json::json!([{ "id": pid }]);
}
client.post("/content", &body).await
};
match result {
Ok(resp) => {
let web_url = resp.links.as_ref().and_then(|l| l.webui.as_ref()).map_or_else(
|| format!("{}/wiki/spaces/{space_key}/pages/{}", cfg.host, resp.id),
|w| {
if w.starts_with('/') {
let prefix = if cfg.is_cloud { "/wiki" } else { "" };
format!("{}{prefix}{w}", cfg.host)
} else {
w.to_string()
}
},
);
CallToolResult::text(format!(
"✅ Page created successfully!\n\n**Title**: {}\n**ID**: {}\n**URL**: {}",
resp.title, resp.id, web_url
))
}
Err(e) => CallToolResult::text(format!("Error creating page: {e}")),
}
}
pub async fn update_page(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 title = args.get("title").and_then(|v| v.as_str());
let content = match args.get("content").and_then(|v| v.as_str()) {
Some(c) => c,
None => return CallToolResult::text("Error: content is required."),
};
let version_comment = args.get("versionComment").and_then(|v| v.as_str());
let cfg = client.config();
let result: Result<(), String> = async {
if cfg.is_cloud {
let current: ConfluencePage = client.get(&format!("/pages/{page_id}")).await?;
let current_version = current.version.as_ref().map_or(1, |v| v.number);
let current_title = current.title.clone();
let body = serde_json::json!({
"id": page_id,
"status": "current",
"title": title.unwrap_or(¤t_title),
"body": {
"representation": "storage",
"value": content,
},
"version": {
"number": current_version + 1,
"message": version_comment,
}
});
client.put::<Value>(&format!("/pages/{page_id}"), &body).await?;
Ok(())
} else {
let current: ConfluencePageV1 = client
.get(&format!("/content/{page_id}?expand=version"))
.await?;
let current_version = current.version.as_ref().map_or(1, |v| v.number);
let current_title = current.title.clone();
let body = serde_json::json!({
"type": "page",
"title": title.unwrap_or(¤t_title),
"body": {
"storage": {
"value": content,
"representation": "storage"
}
},
"version": {
"number": current_version + 1,
"message": version_comment,
}
});
client.put::<Value>(&format!("/content/{page_id}"), &body).await?;
Ok(())
}
}
.await;
match result {
Ok(()) => CallToolResult::text(format!(
"✅ Page updated successfully!\n\n**ID**: {page_id}"
)),
Err(e) => CallToolResult::text(format!("Error updating page: {e}")),
}
}
pub async fn delete_page(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 endpoint = if client.config().is_cloud {
format!("/pages/{page_id}")
} else {
format!("/content/{page_id}")
};
match client.delete(&endpoint).await {
Ok(()) => CallToolResult::text(format!("✅ Page {page_id} deleted successfully.")),
Err(e) => CallToolResult::text(format!("Error deleting page: {e}")),
}
}
async fn fetch_page_cloud(
client: &ConfluenceClient,
page_id: Option<&str>,
space_key: Option<&str>,
title: Option<&str>,
include_body: bool,
) -> Result<AnyPage, String> {
if let Some(pid) = page_id {
let body_format = if include_body { "&body-format=storage" } else { "" };
let page: ConfluencePage = client.get(&format!("/pages/{pid}?{body_format}")).await?;
Ok(AnyPage::V2(page))
} else {
let cql = format!(
"space=\"{}\" AND title=\"{}\"",
space_key.unwrap(),
title.unwrap()
);
let search: SearchResult = client
.get_v1(&format!(
"/search?cql={}&limit=1",
urlencoding(&cql)
))
.await?;
if search.results.is_empty() || search.results[0].content.is_none() {
return Err(format!(
"Page \"{}\" not found in space {}.",
title.unwrap(),
space_key.unwrap()
));
}
let found_id = &search.results[0].content.as_ref().unwrap().id;
let body_format = if include_body { "&body-format=storage" } else { "" };
let page: ConfluencePage =
client.get(&format!("/pages/{found_id}?{body_format}")).await?;
Ok(AnyPage::V2(page))
}
}
async fn fetch_page_server(
client: &ConfluenceClient,
page_id: Option<&str>,
space_key: Option<&str>,
title: Option<&str>,
include_body: bool,
) -> Result<AnyPage, String> {
let expand = if include_body {
"body.storage,version,ancestors,space"
} else {
"version,ancestors,space"
};
if let Some(pid) = page_id {
let page: ConfluencePageV1 =
client.get(&format!("/content/{pid}?expand={expand}")).await?;
Ok(AnyPage::V1(page))
} else {
let result: PageListResponseV1 = client
.get(&format!(
"/content?spaceKey={}&title={}&expand={expand}",
space_key.unwrap(),
urlencoding(title.unwrap())
))
.await?;
if result.results.is_empty() {
return Err(format!(
"Page \"{}\" not found in space {}.",
title.unwrap(),
space_key.unwrap()
));
}
Ok(AnyPage::V1(result.results.into_iter().next().unwrap()))
}
}
fn urlencoding(s: &str) -> String {
let mut out = String::new();
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(b as char)
}
_ => {
out.push('%');
out.push_str(&format!("{b:02X}"));
}
}
}
out
}