pub mod spaces;
pub mod pages;
pub mod search;
pub mod labels;
pub mod reading;
pub mod attachments;
pub mod storage;
pub mod tables;
use serde_json::Value;
use crate::client::ConfluenceClient;
use crate::mcp::{CallToolResult, ToolDefinition};
pub fn schema(
properties: &[(&str, &str, bool, &str)],
) -> Value {
let mut props = serde_json::Map::new();
let mut required = Vec::new();
for &(name, ty, req, desc) in properties {
let mut prop = serde_json::Map::new();
prop.insert("type".to_string(), Value::String(ty.to_string()));
prop.insert("description".to_string(), Value::String(desc.to_string()));
props.insert(name.to_string(), Value::Object(prop));
if req {
required.push(Value::String(name.to_string()));
}
}
let mut obj = serde_json::Map::new();
obj.insert("type".to_string(), Value::String("object".to_string()));
obj.insert("properties".to_string(), Value::Object(props));
if !required.is_empty() {
obj.insert("required".to_string(), Value::Array(required));
}
Value::Object(obj)
}
pub fn schema_with_array(
properties: &[(&str, &str, bool, &str)],
array_props: &[(&str, &str, bool, &str)],
) -> Value {
let mut val = schema(properties);
let obj = val.as_object_mut().unwrap();
let mut extra_required: Vec<Value> = Vec::new();
let mut extra_props: Vec<(String, Value)> = Vec::new();
for &(name, item_type, req, desc) in array_props {
let mut prop = serde_json::Map::new();
prop.insert("type".to_string(), Value::String("array".to_string()));
prop.insert("description".to_string(), Value::String(desc.to_string()));
let mut items = serde_json::Map::new();
items.insert("type".to_string(), Value::String(item_type.to_string()));
prop.insert("items".to_string(), Value::Object(items));
extra_props.push((name.to_string(), Value::Object(prop)));
if req {
extra_required.push(Value::String(name.to_string()));
}
}
let props = obj.get_mut("properties").unwrap().as_object_mut().unwrap();
for (k, v) in extra_props {
props.insert(k, v);
}
let required = obj
.entry("required")
.or_insert_with(|| Value::Array(Vec::new()))
.as_array_mut()
.unwrap();
required.extend(extra_required);
val
}
pub fn list_tools() -> Vec<ToolDefinition> {
let mut tools = Vec::new();
tools.extend(spaces::definitions());
tools.extend(pages::definitions());
tools.extend(search::definitions());
tools.extend(labels::definitions());
tools.extend(reading::definitions());
tools.extend(attachments::definitions());
tools.extend(storage::definitions());
tools.extend(tables::definitions());
tools
}
pub async fn call_tool(
client: &ConfluenceClient,
name: &str,
args: &Value,
) -> CallToolResult {
match name {
"list_spaces" => spaces::list_spaces(client, args).await,
"get_space_pages" => spaces::get_space_pages(client, args).await,
"get_page" => pages::get_page(client, args).await,
"get_page_full" => pages::get_page_full(client, args).await,
"create_page" => pages::create_page(client, args).await,
"update_page" => pages::update_page(client, args).await,
"delete_page" => pages::delete_page(client, args).await,
"search_pages" => search::search_pages(client, args).await,
"get_page_children" => search::get_page_children(client, args).await,
"add_labels" => labels::add_labels(client, args).await,
"get_labels" => labels::get_labels(client, args).await,
"read_page_outline" => reading::read_page_outline(client, args).await,
"read_page_section" => reading::read_page_section(client, args).await,
"read_page_offset" => reading::read_page_offset(client, args).await,
"list_page_attachments" => attachments::list_page_attachments(client, args).await,
"get_attachment_url" => attachments::get_attachment_url(client, args).await,
"get_attachment_base64" => attachments::get_attachment_base64(client, args).await,
"get_page_storage_format" => storage::get_page_storage_format(client, args).await,
"update_release_table_cell" => tables::update_release_table_cell(client, args).await,
"find_replace_in_page" => tables::find_replace_in_page(client, args).await,
"insert_table_row" => tables::insert_table_row(client, args).await,
_ => CallToolResult::text(format!("Unknown tool: {name}")),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn schema_basic() {
let s = schema(&[
("name", "string", true, "The name"),
("count", "integer", false, "A count"),
]);
let obj = s.as_object().unwrap();
assert_eq!(obj["type"], "object");
let props = obj["properties"].as_object().unwrap();
assert!(props.contains_key("name"));
assert!(props.contains_key("count"));
assert_eq!(props["name"]["type"], "string");
assert_eq!(props["name"]["description"], "The name");
assert_eq!(props["count"]["type"], "integer");
let required = obj["required"].as_array().unwrap();
assert_eq!(required.len(), 1);
assert_eq!(required[0], "name");
}
#[test]
fn schema_no_required() {
let s = schema(&[("opt", "string", false, "Optional field")]);
let obj = s.as_object().unwrap();
assert!(!obj.contains_key("required"));
}
#[test]
fn schema_empty() {
let s = schema(&[]);
let obj = s.as_object().unwrap();
assert_eq!(obj["type"], "object");
assert!(obj["properties"].as_object().unwrap().is_empty());
}
#[test]
fn schema_with_array_basic() {
let s = schema_with_array(
&[("pageId", "string", true, "Page ID")],
&[("labels", "string", true, "Labels to add")],
);
let obj = s.as_object().unwrap();
let props = obj["properties"].as_object().unwrap();
assert!(props.contains_key("pageId"));
assert!(props.contains_key("labels"));
assert_eq!(props["labels"]["type"], "array");
assert_eq!(props["labels"]["items"]["type"], "string");
let required = obj["required"].as_array().unwrap();
assert!(required.contains(&Value::String("pageId".to_string())));
assert!(required.contains(&Value::String("labels".to_string())));
}
#[test]
fn schema_with_array_optional() {
let s = schema_with_array(
&[],
&[("tags", "string", false, "Optional tags")],
);
let obj = s.as_object().unwrap();
let props = obj["properties"].as_object().unwrap();
assert!(props.contains_key("tags"));
assert!(!obj.contains_key("required") || obj["required"].as_array().unwrap().is_empty());
}
#[test]
fn list_tools_returns_all() {
let tools = list_tools();
assert!(!tools.is_empty());
let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
assert!(names.contains(&"list_spaces"));
assert!(names.contains(&"get_page"));
assert!(names.contains(&"create_page"));
assert!(names.contains(&"update_page"));
assert!(names.contains(&"delete_page"));
assert!(names.contains(&"search_pages"));
assert!(names.contains(&"add_labels"));
assert!(names.contains(&"get_labels"));
assert!(names.contains(&"read_page_outline"));
assert!(names.contains(&"read_page_section"));
assert!(names.contains(&"read_page_offset"));
assert!(names.contains(&"list_page_attachments"));
assert!(names.contains(&"get_attachment_url"));
assert!(names.contains(&"get_attachment_base64"));
assert!(names.contains(&"get_page_storage_format"));
assert!(names.contains(&"update_release_table_cell"));
assert!(names.contains(&"find_replace_in_page"));
assert!(names.contains(&"insert_table_row"));
}
#[test]
fn tool_definitions_have_schemas() {
let tools = list_tools();
for tool in &tools {
assert!(!tool.name.is_empty(), "Tool name should not be empty");
assert!(!tool.description.is_empty(), "Tool {} should have a description", tool.name);
let schema = tool.input_schema.as_object().unwrap();
assert_eq!(schema["type"], "object", "Tool {} schema should be an object", tool.name);
}
}
#[tokio::test]
async fn call_tool_unknown() {
let config = crate::config::Config {
host: "https://example.com".to_string(),
email: None,
api_token: "tok".to_string(),
api_version: "1".to_string(),
is_cloud: false,
use_bearer: true,
max_content_length: 30000,
};
let client = crate::client::ConfluenceClient::new(config);
let result = call_tool(&client, "nonexistent_tool", &Value::Null).await;
assert_eq!(result.content.len(), 1);
assert!(result.content[0].text.contains("Unknown tool"));
}
}