use serde_json::Value;
use crate::client::ConfluenceClient;
use crate::formatters::format_bytes;
use crate::mcp::{CallToolResult, ToolDefinition};
use crate::types::{AttachmentListResponse, ConfluenceAttachment};
use super::schema;
pub fn definitions() -> Vec<ToolDefinition> {
vec![
ToolDefinition {
name: "list_page_attachments".to_string(),
description:
"List all attachments (images, files) on a Confluence page with metadata"
.to_string(),
input_schema: schema(&[
(
"pageId",
"string",
true,
"The page ID to list attachments for",
),
(
"mediaType",
"string",
false,
"Filter by media type (e.g., 'image/png', 'image/', 'application/pdf')",
),
]),
},
ToolDefinition {
name: "get_attachment_url".to_string(),
description: "Get the direct download URL for a Confluence attachment".to_string(),
input_schema: schema(&[
(
"pageId",
"string",
true,
"The page ID containing the attachment",
),
("filename", "string", true, "The filename of the attachment"),
]),
},
ToolDefinition {
name: "get_attachment_base64".to_string(),
description: "Fetch an attachment and return it as base64-encoded data. Useful for images with vision-capable models.".to_string(),
input_schema: schema(&[
("pageId", "string", true, "The page ID containing the attachment"),
("filename", "string", true, "The filename of the attachment"),
("maxSizeKB", "number", false, "Maximum file size in KB to fetch (default: 500KB)"),
]),
},
]
}
async fn fetch_attachments(
client: &ConfluenceClient,
page_id: &str,
) -> Result<(Vec<ConfluenceAttachment>, String), String> {
let base_url = client.config().host.clone();
if client.config().is_cloud {
let result: AttachmentListResponse = client
.get(&format!("/pages/{page_id}/attachments"))
.await?;
Ok((result.results, base_url))
} else {
let result: AttachmentListResponse = client
.get(&format!(
"/content/{page_id}/child/attachment?expand=metadata,extensions"
))
.await?;
let base = result
.links
.as_ref()
.and_then(|l| l.base.as_deref())
.unwrap_or(&base_url)
.to_string();
Ok((result.results, base))
}
}
fn attachment_media_type(att: &ConfluenceAttachment) -> String {
att.media_type
.clone()
.or_else(|| att.extensions.as_ref().and_then(|e| e.media_type.clone()))
.or_else(|| att.metadata.as_ref().and_then(|m| m.media_type.clone()))
.unwrap_or_else(|| "unknown".to_string())
}
fn attachment_file_size(att: &ConfluenceAttachment) -> u64 {
att.file_size
.or_else(|| att.extensions.as_ref().and_then(|e| e.file_size))
.unwrap_or(0)
}
fn attachment_download_path(att: &ConfluenceAttachment) -> String {
att.download_link
.clone()
.or_else(|| att.links.as_ref().and_then(|l| l.download.clone()))
.unwrap_or_default()
}
pub async fn list_page_attachments(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 media_type_filter = args.get("mediaType").and_then(|v| v.as_str());
match fetch_attachments(client, page_id).await {
Ok((mut attachments, base_url)) => {
if attachments.is_empty() {
return CallToolResult::text("No attachments found on this page.");
}
if let Some(filter) = media_type_filter {
attachments.retain(|a| {
attachment_media_type(a)
.to_lowercase()
.contains(&filter.to_lowercase())
});
if attachments.is_empty() {
return CallToolResult::text(format!(
"No attachments found matching media type \"{filter}\"."
));
}
}
let formatted: Vec<String> = attachments
.iter()
.enumerate()
.map(|(i, att)| {
let mtype = attachment_media_type(att);
let size = attachment_file_size(att);
let dl = attachment_download_path(att);
let is_image = mtype.starts_with("image/");
let emoji = if is_image { " 🖼️" } else { "" };
let mut lines = vec![
format!("### {}. {}", i + 1, att.title),
format!("- **ID**: {}", att.id),
format!("- **Type**: {mtype}{emoji}"),
format!("- **Size**: {}", format_bytes(size)),
];
if !dl.is_empty() {
let full = if dl.starts_with('/') {
format!("{base_url}{dl}")
} else {
dl
};
lines.push(format!("- **URL**: {full}"));
}
lines.join("\n")
})
.collect();
let image_count = attachments
.iter()
.filter(|a| attachment_media_type(a).starts_with("image/"))
.count();
let img_note = if image_count > 0 {
format!(" ({image_count} images)")
} else {
String::new()
};
CallToolResult::text(format!(
"# Attachments for Page {page_id}\n\n**Total**: {} attachment(s){img_note}\n\n---\n\n{}\n\n---\n💡 Use `get_attachment_url` to get a direct URL, or `get_attachment_base64` to fetch image data for vision models.",
attachments.len(),
formatted.join("\n\n")
))
}
Err(e) => CallToolResult::text(format!("Error listing attachments: {e}")),
}
}
pub async fn get_attachment_url(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 filename = match args.get("filename").and_then(|v| v.as_str()) {
Some(f) => f,
None => return CallToolResult::text("Error: filename is required."),
};
match fetch_attachments(client, page_id).await {
Ok((attachments, base_url)) => {
let attachment = attachments
.iter()
.find(|a| a.title.to_lowercase() == filename.to_lowercase());
match attachment {
Some(att) => {
let dl = attachment_download_path(att);
let full_url = if dl.starts_with('/') {
format!("{base_url}{dl}")
} else {
dl
};
let mtype = attachment_media_type(att);
let size = attachment_file_size(att);
CallToolResult::text(format!(
"# Attachment: {}\n\n| Property | Value |\n|----------|-------|\n| **ID** | {} |\n| **Type** | {mtype} |\n| **Size** | {} |\n| **URL** | {full_url} |\n\n---\n📎 Direct link: {full_url}",
att.title,
att.id,
format_bytes(size),
))
}
None => {
let available: Vec<&str> =
attachments.iter().map(|a| a.title.as_str()).collect();
CallToolResult::text(format!(
"Attachment \"{filename}\" not found.\n\nAvailable attachments: {}",
if available.is_empty() {
"none".to_string()
} else {
available.join(", ")
}
))
}
}
}
Err(e) => CallToolResult::text(format!("Error getting attachment URL: {e}")),
}
}
pub async fn get_attachment_base64(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 filename = match args.get("filename").and_then(|v| v.as_str()) {
Some(f) => f,
None => return CallToolResult::text("Error: filename is required."),
};
let max_size_kb = args
.get("maxSizeKB")
.and_then(|v| v.as_u64())
.unwrap_or(500);
match fetch_attachments(client, page_id).await {
Ok((attachments, base_url)) => {
let attachment = attachments
.iter()
.find(|a| a.title.to_lowercase() == filename.to_lowercase());
match attachment {
Some(att) => {
let file_size = attachment_file_size(att);
let max_bytes = max_size_kb * 1024;
if file_size > max_bytes {
return CallToolResult::text(format!(
"Attachment \"{}\" ({}) exceeds the maximum size limit of {}KB.\n\nUse `get_attachment_url` to get the direct URL instead.",
filename,
format_bytes(file_size),
max_size_kb,
));
}
let dl = attachment_download_path(att);
if dl.is_empty() {
return CallToolResult::text(format!(
"No download URL found for attachment \"{filename}\"."
));
}
let full_url = if dl.starts_with('/') {
format!("{base_url}{dl}")
} else {
dl
};
match client.fetch_binary(&full_url).await {
Ok((data, mime, size)) => {
CallToolResult::text(format!(
"# Attachment: {}\n\n| Property | Value |\n|----------|-------|\n| **Type** | {mime} |\n| **Size** | {} |\n| **Base64 Length** | {} characters |\n\n## Base64 Data\n\n```\ndata:{mime};base64,{data}\n```\n\n---\n💡 The data URI above can be used directly in image tags or passed to vision-capable models.",
att.title,
format_bytes(size as u64),
data.len(),
))
}
Err(e) => CallToolResult::text(format!("Error fetching attachment: {e}")),
}
}
None => {
let available: Vec<&str> =
attachments.iter().map(|a| a.title.as_str()).collect();
CallToolResult::text(format!(
"Attachment \"{filename}\" not found.\n\nAvailable attachments: {}",
if available.is_empty() {
"none".to_string()
} else {
available.join(", ")
}
))
}
}
}
Err(e) => CallToolResult::text(format!("Error fetching attachment: {e}")),
}
}