cortex_mcp/tools/
principle_status.rs1use std::sync::{Arc, Mutex};
14
15use cortex_store::repo::{MemoryRepo, PrincipleRepo};
16use cortex_store::Pool;
17use serde_json::{json, Value};
18
19use crate::tool_handler::{GateId, ToolError, ToolHandler};
20
21#[derive(Debug)]
38pub struct CortexPrincipleStatusTool {
39 pool: Arc<Mutex<Pool>>,
40}
41
42impl CortexPrincipleStatusTool {
43 #[must_use]
45 pub fn new(pool: Arc<Mutex<Pool>>) -> Self {
46 Self { pool }
47 }
48}
49
50impl ToolHandler for CortexPrincipleStatusTool {
51 fn name(&self) -> &'static str {
52 "cortex_principle_status"
53 }
54
55 fn gate_set(&self) -> &'static [GateId] {
56 &[GateId::FtsRead]
57 }
58
59 fn call(&self, params: Value) -> Result<Value, ToolError> {
60 tracing::info!("cortex_principle_status called via MCP");
61
62 let principle_id_filter = params["principle_id"]
63 .as_str()
64 .filter(|s| !s.trim().is_empty())
65 .map(ToOwned::to_owned);
66
67 let pool = self
68 .pool
69 .lock()
70 .map_err(|err| ToolError::Internal(format!("pool lock poisoned: {err}")))?;
71
72 let active_count = MemoryRepo::new(&pool)
74 .list_by_status("active")
75 .map_err(|err| {
76 tracing::error!(error = %err, "cortex_principle_status: failed to read active memories");
77 ToolError::Internal(format!("failed to read active memories: {err}"))
78 })?
79 .len();
80
81 let principle_repo = PrincipleRepo::new(&pool);
82
83 let candidates = principle_repo.list_candidates().map_err(|err| {
85 tracing::error!(error = %err, "cortex_principle_status: failed to list principle candidates");
86 ToolError::Internal(format!("failed to list principle candidates: {err}"))
87 })?;
88
89 let doctrine = principle_repo.list_doctrine().map_err(|err| {
91 tracing::error!(error = %err, "cortex_principle_status: failed to list doctrine");
92 ToolError::Internal(format!("failed to list doctrine: {err}"))
93 })?;
94
95 let candidate_count = candidates.len();
96 let doctrine_count = doctrine.len();
97
98 let principles: Vec<Value> = if let Some(ref id) = principle_id_filter {
100 let pid: cortex_core::PrincipleId = id.parse().map_err(|err| {
102 ToolError::InvalidParams(format!("invalid principle_id `{id}`: {err}"))
103 })?;
104
105 match principle_repo.get_by_id(&pid).map_err(|err| {
106 ToolError::Internal(format!("failed to fetch principle {id}: {err}"))
107 })? {
108 Some(row) => vec![json!({
109 "id": row.id.to_string(),
110 "claim": row.statement,
111 "status": row.status,
112 "confidence": row.confidence,
113 })],
114 None => vec![],
115 }
116 } else {
117 candidates
118 .into_iter()
119 .map(|row| {
120 json!({
121 "id": row.id.to_string(),
122 "claim": row.statement,
123 "status": row.status,
124 "confidence": row.confidence,
125 })
126 })
127 .collect()
128 };
129
130 Ok(json!({
131 "active_count": active_count,
132 "candidate_count": candidate_count,
133 "doctrine_count": doctrine_count,
134 "principles": principles,
135 }))
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn make_tool() -> CortexPrincipleStatusTool {
144 let pool = rusqlite::Connection::open_in_memory().expect("in-memory sqlite");
145 cortex_store::migrate::apply_pending(&pool).expect("migrations");
146 CortexPrincipleStatusTool::new(Arc::new(Mutex::new(pool)))
147 }
148
149 #[test]
150 fn name_and_gate() {
151 let tool = make_tool();
152 assert_eq!(tool.name(), "cortex_principle_status");
153 assert_eq!(tool.gate_set(), &[GateId::FtsRead]);
154 }
155
156 #[test]
157 fn empty_store_returns_zero_counts() {
158 let tool = make_tool();
159 let result = tool.call(Value::Null).unwrap();
160 assert_eq!(result["active_count"], 0);
161 assert_eq!(result["candidate_count"], 0);
162 assert_eq!(result["doctrine_count"], 0);
163 assert_eq!(result["principles"], json!([]));
164 }
165
166 #[test]
167 fn unknown_principle_id_returns_empty_principles() {
168 let tool = make_tool();
169 let result = tool
171 .call(json!({ "principle_id": "prn_01JQZZZZZZZZZZZZZZZZZZZZZZ" }))
172 .unwrap();
173 assert_eq!(result["principles"], json!([]));
174 }
175
176 #[test]
177 fn invalid_principle_id_returns_invalid_params() {
178 let tool = make_tool();
179 let err = tool
180 .call(json!({ "principle_id": "not-a-ulid" }))
181 .unwrap_err();
182 assert!(matches!(err, ToolError::InvalidParams(_)));
183 }
184}