codebase-graph 1.1.5

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use super::{options::McpServeOptions, tools::mcp_call_tool_result};
use crate::cli::{
    constants::{LATEST_PROTOCOL_VERSION, MCP_TOOL_SPECS_JSON},
    format::metadata_payload,
};
use serde_json::json;

pub(in crate::cli) fn handle_mcp_message(
    message: serde_json::Value,
    session: &mut McpSession,
    options: &McpServeOptions,
) -> Option<serde_json::Value> {
    let request_id = message
        .get("id")
        .cloned()
        .unwrap_or(serde_json::Value::Null);
    let method = message
        .get("method")
        .and_then(serde_json::Value::as_str)
        .unwrap_or("");
    if method == "notifications/initialized" {
        session.initialized = true;
        return None;
    }
    if method.starts_with("notifications/") {
        return None;
    }
    if matches!(method, "tools/list" | "tools/call") && session.protocol_version.is_none() {
        return Some(rpc_error(
            request_id,
            -32002,
            "MCP session is not initialized",
        ));
    }
    let result = match method {
        "initialize" => {
            let requested = message
                .get("params")
                .and_then(|params| params.get("protocolVersion"))
                .and_then(serde_json::Value::as_str)
                .unwrap_or("");
            let protocol_version = negotiate_protocol_version(requested);
            session.protocol_version = Some(protocol_version.to_string());
            Ok(json!({
                "protocolVersion": protocol_version,
                "capabilities": {"tools": {"listChanged": false}},
                "serverInfo": {"name": "codebase_graph", "version": env!("CARGO_PKG_VERSION")},
            }))
        }
        "ping" => Ok(json!({})),
        "tools/list" => metadata_payload(MCP_TOOL_SPECS_JSON),
        "tools/call" => {
            let params = message.get("params").cloned().unwrap_or_else(|| json!({}));
            let tool_name = params
                .get("name")
                .and_then(serde_json::Value::as_str)
                .unwrap_or("");
            let arguments = params
                .get("arguments")
                .cloned()
                .unwrap_or_else(|| json!({}));
            mcp_call_tool_result(tool_name, &arguments, options)
        }
        _ => {
            return Some(rpc_error(
                request_id,
                -32601,
                &format!("Unsupported MCP method: {method}"),
            ));
        }
    };
    match result {
        Ok(result) => Some(json!({"jsonrpc": "2.0", "id": request_id, "result": result})),
        Err(error) => Some(rpc_error(request_id, -32602, &error)),
    }
}

pub(in crate::cli) fn parse_mcp_payload(data: &[u8]) -> Result<serde_json::Value, String> {
    let payload: serde_json::Value =
        serde_json::from_slice(data).map_err(|error| error.to_string())?;
    if !payload.is_object() {
        return Err("JSON-RPC payload must be an object".to_string());
    }
    Ok(payload)
}

pub(in crate::cli) fn negotiate_protocol_version(requested: &str) -> String {
    match requested {
        "2025-11-25" | "2025-06-18" | "2025-03-26" | "2024-11-05" => requested.to_string(),
        _ => LATEST_PROTOCOL_VERSION.to_string(),
    }
}

pub(in crate::cli) fn rpc_error(
    request_id: serde_json::Value,
    code: i64,
    message: &str,
) -> serde_json::Value {
    json!({
        "jsonrpc": "2.0",
        "id": request_id,
        "error": {
            "code": code,
            "message": message,
        }
    })
}

pub(in crate::cli) fn is_supported_protocol_version(version: &str) -> bool {
    matches!(
        version,
        "2025-11-25" | "2025-06-18" | "2025-03-26" | "2024-11-05"
    )
}

#[derive(Debug, Default)]
pub(in crate::cli) struct McpSession {
    pub(in crate::cli) protocol_version: Option<String>,
    pub(in crate::cli) initialized: bool,
}