cortex_mcp/tools/
doctor.rs1use std::path::PathBuf;
13use std::sync::{Arc, Mutex};
14
15use cortex_ledger::{audit::verify_schema_migration_v1_to_v2_boundary, JsonlLog};
16use serde_json::{json, Value};
17
18use crate::{GateId, ToolError, ToolHandler};
19
20#[derive(Debug)]
30pub struct CortexDoctorTool {
31 pool: Arc<Mutex<cortex_store::Pool>>,
32 event_log: PathBuf,
33}
34
35impl CortexDoctorTool {
36 #[must_use]
38 pub fn new(pool: Arc<Mutex<cortex_store::Pool>>, event_log: PathBuf) -> Self {
39 Self { pool, event_log }
40 }
41}
42
43impl ToolHandler for CortexDoctorTool {
44 fn name(&self) -> &'static str {
45 "cortex_doctor"
46 }
47
48 fn gate_set(&self) -> &'static [GateId] {
49 &[GateId::HealthRead]
50 }
51
52 fn call(&self, _params: Value) -> Result<Value, ToolError> {
53 tracing::info!("cortex_doctor called via MCP");
54
55 let pool = self
56 .pool
57 .lock()
58 .map_err(|err| ToolError::Internal(format!("failed to acquire store lock: {err}")))?;
59
60 let mut issues: Vec<String> = Vec::new();
61
62 let schema_report =
64 cortex_store::verify::verify_schema_version(&pool, cortex_core::SCHEMA_VERSION)
65 .map_err(|err| {
66 ToolError::Internal(format!("failed to verify schema version: {err}"))
67 })?;
68
69 if !schema_report.is_ok() {
70 for failure in &schema_report.failures {
71 issues.push(format!("{}: {}", failure.invariant(), failure.detail()));
72 }
73 } else {
74 let needs_boundary = if cortex_core::SCHEMA_VERSION >= 2 {
77 contains_pre_cutover_v1_rows(&self.event_log).map_err(|err| {
78 ToolError::Internal(format!(
79 "failed to inspect event log for pre-cutover v1 rows: {err}"
80 ))
81 })?
82 } else {
83 false
84 };
85
86 match verify_schema_migration_v1_to_v2_boundary(&self.event_log, needs_boundary) {
87 Ok(boundary_report) if boundary_report.ok() => {}
88 Ok(boundary_report) => {
89 for failure in &boundary_report.failures {
90 issues.push(format!("{}: {:?}", failure.invariant, failure.detail));
91 }
92 }
93 Err(err) => {
94 issues.push(format!("failed to verify schema boundary events: {err}"));
95 }
96 }
97 }
98
99 let ok = issues.is_empty();
100 Ok(json!({ "ok": ok, "issues": issues }))
101 }
102}
103
104fn contains_pre_cutover_v1_rows(
109 event_log_path: &std::path::Path,
110) -> Result<bool, cortex_ledger::JsonlError> {
111 let log = JsonlLog::open(event_log_path)?;
112 for item in log.iter()? {
113 let event = item?;
114 if event.schema_version < cortex_core::SCHEMA_VERSION {
115 return Ok(true);
116 }
117 }
118 Ok(false)
119}