use serde_json::Value;
use crate::client::ConfluenceClient;
use crate::formatters::{format_page, strip_html};
use crate::mcp::{CallToolResult, ToolDefinition};
use crate::types::{
AnyPage, PageListResponse, PageListResponseV1, SearchResult,
};
use super::schema;
pub fn definitions() -> Vec<ToolDefinition> {
vec![
ToolDefinition {
name: "search_pages".to_string(),
description: "Search for Confluence pages using CQL (Confluence Query Language)"
.to_string(),
input_schema: schema(&[
(
"cql",
"string",
true,
"CQL query string (e.g., 'space = DEV AND title ~ \"API\"')",
),
(
"limit",
"number",
false,
"Maximum number of results to return (default: 10)",
),
]),
},
ToolDefinition {
name: "get_page_children".to_string(),
description: "Get child pages of a Confluence page".to_string(),
input_schema: schema(&[
("pageId", "string", true, "The ID of the parent page"),
(
"limit",
"number",
false,
"Maximum number of children to return (default: 25)",
),
]),
},
]
}
pub async fn search_pages(client: &ConfluenceClient, args: &Value) -> CallToolResult {
let cql = match args.get("cql").and_then(|v| v.as_str()) {
Some(c) => c,
None => return CallToolResult::text("Error: cql is required."),
};
let limit = args.get("limit").and_then(|v| v.as_i64()).unwrap_or(10);
let encoded_cql = urlencoding(cql);
let result: Result<SearchResult, String> = client
.get_v1(&format!(
"/search?cql={encoded_cql}&limit={limit}"
))
.await;
match result {
Ok(sr) => {
if sr.results.is_empty() {
return CallToolResult::text("No pages found matching your query.");
}
let formatted: Vec<String> = sr
.results
.iter()
.map(|r| {
let mut lines = vec![format!("**{}**", r.title)];
if let Some(content) = &r.content {
lines.push(format!("- **ID**: {}", content.id));
lines.push(format!("- **Status**: {}", content.status));
if let Some(space) = &content.space {
lines.push(format!("- **Space**: {} ({})", space.name, space.key));
}
}
if let Some(excerpt) = &r.excerpt {
let clean = strip_html(excerpt);
let truncated = if clean.len() > 200 {
format!("{}...", &clean[..200])
} else {
clean
};
lines.push(format!("- **Excerpt**: {truncated}"));
}
lines.join("\n")
})
.collect();
let total = sr.total_size.unwrap_or(sr.results.len() as i64);
CallToolResult::text(format!(
"Found {} result(s) (showing {}):\n\n{}",
total,
sr.results.len(),
formatted.join("\n\n---\n\n")
))
}
Err(e) => CallToolResult::text(format!("Error searching pages: {e}")),
}
}
pub async fn get_page_children(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 limit = args.get("limit").and_then(|v| v.as_i64()).unwrap_or(25);
if client.config().is_cloud {
match client
.get::<PageListResponse>(&format!("/pages/{page_id}/children?limit={limit}"))
.await
{
Ok(result) => {
if result.results.is_empty() {
return CallToolResult::text("No child pages found.");
}
let formatted: Vec<String> = result
.results
.iter()
.map(|p| {
let ap = AnyPage::V2(
serde_json::from_value(serde_json::to_value(p).unwrap()).unwrap(),
);
format_page(&ap)
})
.collect();
CallToolResult::text(format!(
"Found {} child page(s):\n\n{}",
result.results.len(),
formatted.join("\n\n---\n\n")
))
}
Err(e) => CallToolResult::text(format!("Error getting child pages: {e}")),
}
} else {
match client
.get::<PageListResponseV1>(&format!(
"/content/{page_id}/child/page?limit={limit}&expand=version"
))
.await
{
Ok(result) => {
if result.results.is_empty() {
return CallToolResult::text("No child pages found.");
}
let formatted: Vec<String> = result
.results
.iter()
.map(|p| {
let ap = AnyPage::V1(
serde_json::from_value(serde_json::to_value(p).unwrap()).unwrap(),
);
format_page(&ap)
})
.collect();
CallToolResult::text(format!(
"Found {} child page(s):\n\n{}",
result.results.len(),
formatted.join("\n\n---\n\n")
))
}
Err(e) => CallToolResult::text(format!("Error getting child pages: {e}")),
}
}
}
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
}