crtx-mcp 0.1.2

MCP stdio JSON-RPC 2.0 server for Cortex — tool dispatch, ToolHandler trait, gate wiring (ADR 0045).
Documentation
//! `cortex_doctor` MCP tool handler.
//!
//! Runs store precondition checks — the same schema-version gates as
//! `cortex doctor --strict` (`crates/cortex-cli/src/cmd/doctor.rs`) — and
//! returns a structured result. Write-nothing, read-only.
//!
//! Supervised tier tool (council recommendation): logs prominently on every
//! call so operators can audit MCP-initiated doctor invocations.
//!
//! Gate: [`GateId::HealthRead`].

use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use cortex_ledger::{audit::verify_schema_migration_v1_to_v2_boundary, JsonlLog};
use serde_json::{json, Value};

use crate::{GateId, ToolError, ToolHandler};

/// MCP tool: `cortex_doctor`.
///
/// Schema:
/// ```text
/// cortex_doctor() → { ok: bool, issues: [string] }
/// ```
///
/// If all precondition checks pass, `ok: true, issues: []`.
/// If any check fails, `ok: false, issues: ["<description>", ...]`.
#[derive(Debug)]
pub struct CortexDoctorTool {
    pool: Arc<Mutex<cortex_store::Pool>>,
    event_log: PathBuf,
}

impl CortexDoctorTool {
    /// Construct the tool over a shared store connection and event log path.
    #[must_use]
    pub fn new(pool: Arc<Mutex<cortex_store::Pool>>, event_log: PathBuf) -> Self {
        Self { pool, event_log }
    }
}

impl ToolHandler for CortexDoctorTool {
    fn name(&self) -> &'static str {
        "cortex_doctor"
    }

    fn gate_set(&self) -> &'static [GateId] {
        &[GateId::HealthRead]
    }

    fn call(&self, _params: Value) -> Result<Value, ToolError> {
        tracing::info!("cortex_doctor called via MCP");

        let pool = self
            .pool
            .lock()
            .map_err(|err| ToolError::Internal(format!("failed to acquire store lock: {err}")))?;

        let mut issues: Vec<String> = Vec::new();

        // Schema version check — same gate as `cortex doctor --strict`.
        let schema_report =
            cortex_store::verify::verify_schema_version(&pool, cortex_core::SCHEMA_VERSION)
                .map_err(|err| {
                    ToolError::Internal(format!("failed to verify schema version: {err}"))
                })?;

        if !schema_report.is_ok() {
            for failure in &schema_report.failures {
                issues.push(format!("{}: {}", failure.invariant(), failure.detail()));
            }
        } else {
            // Schema v2 boundary check (mirrors `doctor --strict` logic):
            // only required when the event log contains pre-cutover v1 rows.
            let needs_boundary = if cortex_core::SCHEMA_VERSION >= 2 {
                contains_pre_cutover_v1_rows(&self.event_log).map_err(|err| {
                    ToolError::Internal(format!(
                        "failed to inspect event log for pre-cutover v1 rows: {err}"
                    ))
                })?
            } else {
                false
            };

            match verify_schema_migration_v1_to_v2_boundary(&self.event_log, needs_boundary) {
                Ok(boundary_report) if boundary_report.ok() => {}
                Ok(boundary_report) => {
                    for failure in &boundary_report.failures {
                        issues.push(format!("{}: {:?}", failure.invariant, failure.detail));
                    }
                }
                Err(err) => {
                    issues.push(format!("failed to verify schema boundary events: {err}"));
                }
            }
        }

        let ok = issues.is_empty();
        Ok(json!({ "ok": ok, "issues": issues }))
    }
}

/// Return `true` when the JSONL log contains at least one event row whose
/// `schema_version` is strictly less than the cut-over target.
///
/// Mirrors `contains_pre_cutover_v1_rows` in `crates/cortex-cli/src/cmd/doctor.rs`.
fn contains_pre_cutover_v1_rows(
    event_log_path: &std::path::Path,
) -> Result<bool, cortex_ledger::JsonlError> {
    let log = JsonlLog::open(event_log_path)?;
    for item in log.iter()? {
        let event = item?;
        if event.schema_version < cortex_core::SCHEMA_VERSION {
            return Ok(true);
        }
    }
    Ok(false)
}