ccd-cli 1.0.0-alpha.9

Bootstrap and validate Continuous Context Development repositories
#[cfg(feature = "daemon")]
pub(crate) mod daemon;
mod dispatch;
pub(crate) mod protocol;
pub(crate) mod tools;

use std::io::{self, BufRead, BufReader, BufWriter, Write};

use anyhow::Result;
use serde_json::Value;

use protocol::{
    InitializeResult, JsonRpcRequest, JsonRpcResponse, ServerCapabilities, ServerInfo,
    ToolCallParams, ToolResult, ToolsCapability, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND,
    PARSE_ERROR,
};

const PROTOCOL_VERSION: &str = "2024-11-05";

/// Run the MCP stdio server. Reads JSON-RPC requests line by line, responds on stdout.
pub fn serve() -> Result<()> {
    let tools = tools::build_tools();

    let stdin = io::stdin().lock();
    let stdout = io::stdout().lock();
    let mut reader = BufReader::new(stdin);
    let mut writer = BufWriter::new(stdout);

    loop {
        let mut line = String::new();
        if reader.read_line(&mut line)? == 0 {
            break; // EOF
        }
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }

        // Phase 1: Parse raw JSON text.  Failure here is PARSE_ERROR (-32700).
        let value: Value = match serde_json::from_str(trimmed) {
            Ok(v) => v,
            Err(e) => {
                write_response(
                    &mut writer,
                    &JsonRpcResponse::error(Value::Null, PARSE_ERROR, format!("parse error: {e}")),
                )?;
                continue;
            }
        };

        // Extract id from raw Value — distinguishes absent (None) from null (Some(Null)).
        // Serde's Option<Value> collapses both to None, so we must check before deserializing.
        let raw_id = value.get("id").cloned();

        // Phase 2: Validate request structure.  Failure is INVALID_REQUEST (-32600).
        // Always respond (use extracted id, or null if absent/undetectable).
        let request: JsonRpcRequest = match serde_json::from_value(value) {
            Ok(r) => r,
            Err(e) => {
                write_response(
                    &mut writer,
                    &JsonRpcResponse::error(
                        raw_id.unwrap_or(Value::Null),
                        INVALID_REQUEST,
                        format!("invalid request: {e}"),
                    ),
                )?;
                continue;
            }
        };

        // Phase 3: Validate JSON-RPC version.
        if request.jsonrpc != "2.0" {
            // Notifications (absent id) with wrong version are silently ignored.
            if raw_id.is_some() {
                write_response(
                    &mut writer,
                    &JsonRpcResponse::error(
                        raw_id.unwrap_or(Value::Null),
                        INVALID_REQUEST,
                        format!("unsupported jsonrpc version: {:?}", request.jsonrpc),
                    ),
                )?;
            }
            continue;
        }

        // Phase 4: Validate id type — must be string, number, or null (spec §4.2).
        if let Some(ref id) = raw_id {
            match id {
                Value::String(_) | Value::Number(_) | Value::Null => {}
                _ => {
                    write_response(
                        &mut writer,
                        &JsonRpcResponse::error(
                            Value::Null, // invalid id cannot be echoed
                            INVALID_REQUEST,
                            "id must be a string, number, or null",
                        ),
                    )?;
                    continue;
                }
            }
        }

        // Phase 5: Validate params type — must be object, array, or absent (spec §4.2).
        match &request.params {
            Value::Object(_) | Value::Array(_) | Value::Null => {}
            _ => {
                // Notifications with bad params are silently ignored.
                if raw_id.is_some() {
                    write_response(
                        &mut writer,
                        &JsonRpcResponse::error(
                            raw_id.unwrap_or(Value::Null),
                            INVALID_REQUEST,
                            "params must be a structured value (object or array)",
                        ),
                    )?;
                }
                continue;
            }
        }

        // Notification detection: absent id = notification, present id (incl. null) = request.
        let id = match raw_id {
            Some(id) => id,
            None => continue,
        };

        let response = handle_request(&request.method, &request.params, id, &tools);
        write_response(&mut writer, &response)?;
    }

    Ok(())
}

pub(crate) fn handle_request(
    method: &str,
    params: &Value,
    id: Value,
    tools: &[protocol::Tool],
) -> JsonRpcResponse {
    match method {
        "initialize" => {
            let result = InitializeResult {
                protocol_version: PROTOCOL_VERSION,
                capabilities: ServerCapabilities {
                    tools: ToolsCapability {},
                },
                server_info: ServerInfo {
                    name: "ccd",
                    version: env!("CARGO_PKG_VERSION"),
                },
            };
            JsonRpcResponse::success(id, serde_json::to_value(result).unwrap())
        }

        "ping" => JsonRpcResponse::success(id, serde_json::json!({})),

        "tools/list" => {
            let list = serde_json::json!({ "tools": tools });
            JsonRpcResponse::success(id, list)
        }

        "tools/call" => handle_tool_call(params, id),

        _ => JsonRpcResponse::error(id, METHOD_NOT_FOUND, format!("method not found: {method}")),
    }
}

fn handle_tool_call(params: &Value, id: Value) -> JsonRpcResponse {
    let call: ToolCallParams = match serde_json::from_value(params.clone()) {
        Ok(c) => c,
        Err(e) => {
            return JsonRpcResponse::error(
                id,
                INVALID_PARAMS,
                format!("invalid tool call params: {e}"),
            );
        }
    };

    match dispatch::dispatch(&call.name, &call.arguments) {
        Ok(report_value) => {
            // Minified JSON — agents parse it, humans don't read it on the wire.
            let text = serde_json::to_string(&report_value).unwrap_or_default();
            let result = ToolResult::text(text);
            JsonRpcResponse::success(id, serde_json::to_value(result).unwrap())
        }
        Err(e) => {
            let result = ToolResult::error(format!("{e:#}"));
            JsonRpcResponse::success(id, serde_json::to_value(result).unwrap())
        }
    }
}

fn write_response(
    writer: &mut BufWriter<io::StdoutLock<'_>>,
    response: &JsonRpcResponse,
) -> Result<()> {
    serde_json::to_writer(&mut *writer, response)?;
    writer.write_all(b"\n")?;
    writer.flush()?;
    Ok(())
}