mcp-confluence 1.0.0

MCP server for Confluence integration - create, update, search, and manage Confluence pages
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::io::{self, BufRead, Write};

use crate::client::ConfluenceClient;
use crate::tools;

/// JSON-RPC request.
#[derive(Debug, Deserialize)]
pub struct JsonRpcRequest {
    #[allow(dead_code)]
    pub jsonrpc: String,
    pub id: Option<Value>,
    pub method: String,
    #[serde(default)]
    pub params: Option<Value>,
}

/// JSON-RPC success response.
#[derive(Debug, Serialize)]
pub struct JsonRpcResponse {
    pub jsonrpc: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<Value>,
    pub result: Value,
}

/// JSON-RPC error response.
#[derive(Debug, Serialize)]
pub struct JsonRpcError {
    pub jsonrpc: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<Value>,
    pub error: RpcError,
}

#[derive(Debug, Serialize)]
pub struct RpcError {
    pub code: i64,
    pub message: String,
}

/// MCP tool definition (for tools/list response).
#[derive(Debug, Serialize)]
pub struct ToolDefinition {
    pub name: String,
    pub description: String,
    #[serde(rename = "inputSchema")]
    pub input_schema: Value,
}

/// MCP text content for tool results.
#[derive(Debug, Serialize)]
pub struct TextContent {
    #[serde(rename = "type")]
    pub content_type: String,
    pub text: String,
}

/// Result payload for tools/call.
#[derive(Debug, Serialize)]
pub struct CallToolResult {
    pub content: Vec<TextContent>,
}

impl CallToolResult {
    pub fn text(msg: impl Into<String>) -> Self {
        CallToolResult {
            content: vec![TextContent {
                content_type: "text".to_string(),
                text: msg.into(),
            }],
        }
    }
}

fn send_response(stdout: &mut impl Write, id: Option<Value>, result: Value) -> io::Result<()> {
    let resp = JsonRpcResponse {
        jsonrpc: "2.0".to_string(),
        id,
        result,
    };
    let line = serde_json::to_string(&resp).unwrap();
    writeln!(stdout, "{line}")?;
    stdout.flush()
}

fn send_error(
    stdout: &mut impl Write,
    id: Option<Value>,
    code: i64,
    message: &str,
) -> io::Result<()> {
    let resp = JsonRpcError {
        jsonrpc: "2.0".to_string(),
        id,
        error: RpcError {
            code,
            message: message.to_string(),
        },
    };
    let line = serde_json::to_string(&resp).unwrap();
    writeln!(stdout, "{line}")?;
    stdout.flush()
}

/// Run the MCP server loop, reading JSON-RPC messages from stdin and writing responses to stdout.
pub async fn run(client: ConfluenceClient) -> io::Result<()> {
    let stdin = io::stdin();
    let mut stdout = io::stdout().lock();
    let reader = stdin.lock();

    for line_result in reader.lines() {
        let line = match line_result {
            Ok(l) => l,
            Err(_) => break,
        };

        let line = line.trim();
        if line.is_empty() {
            continue;
        }

        let req: JsonRpcRequest = match serde_json::from_str(line) {
            Ok(r) => r,
            Err(e) => {
                eprintln!("[MCP] Invalid JSON-RPC: {e}");
                send_error(&mut stdout, None, -32700, &format!("Parse error: {e}"))?;
                continue;
            }
        };

        let id = req.id.clone();

        match req.method.as_str() {
            "initialize" => {
                let result = serde_json::json!({
                    "protocolVersion": "2024-11-05",
                    "capabilities": {
                        "tools": {}
                    },
                    "serverInfo": {
                        "name": "confluence-mcp-server",
                        "version": "1.0.0"
                    }
                });
                send_response(&mut stdout, id, result)?;
            }

            "notifications/initialized" => {
                // No response needed for notifications
            }

            "tools/list" => {
                let tool_defs = tools::list_tools();
                let result = serde_json::json!({
                    "tools": tool_defs
                });
                send_response(&mut stdout, id, result)?;
            }

            "tools/call" => {
                let params = req.params.unwrap_or(Value::Null);
                let tool_name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
                let arguments = params
                    .get("arguments")
                    .cloned()
                    .unwrap_or(Value::Object(serde_json::Map::new()));

                let call_result = tools::call_tool(&client, tool_name, &arguments).await;
                let result = serde_json::to_value(call_result).unwrap();
                send_response(&mut stdout, id, result)?;
            }

            _ => {
                // Unknown method – respond with error for requests with IDs
                if id.is_some() {
                    send_error(
                        &mut stdout,
                        id,
                        -32601,
                        &format!("Method not found: {}", req.method),
                    )?;
                }
            }
        }
    }

    Ok(())
}