lean_ctx/tools/registered/
ctx_knowledge.rs1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{json, Map, Value};
4
5use crate::server::tool_trait::{get_str, get_str_array, McpTool, ToolContext, ToolOutput};
6use crate::tool_defs::tool_def;
7
8pub struct CtxKnowledgeTool;
9
10impl McpTool for CtxKnowledgeTool {
11 fn name(&self) -> &'static str {
12 "ctx_knowledge"
13 }
14
15 fn tool_def(&self) -> Tool {
16 tool_def(
17 "ctx_knowledge",
18 "Persistent project knowledge across sessions (facts, patterns, gotchas, typed relations).",
19 json!({
20 "type": "object",
21 "properties": {
22 "action": {
23 "type": "string",
24 "description": "remember|recall|search|pattern|gotcha|feedback|relate|unrelate|relations|relations_diagram|consolidate|status|health|lifecycle_report|remove|export|timeline|rooms|wakeup|policy|embeddings_status|embeddings_reset|embeddings_reindex"
25 },
26 "trigger": { "type": "string", "description": "gotcha: what triggers it" },
27 "resolution": { "type": "string", "description": "gotcha: the fix" },
28 "severity": { "type": "string", "description": "gotcha: critical|warning|info" },
29 "category": { "type": "string", "description": "Fact category" },
30 "key": { "type": "string" },
31 "value": { "type": "string" },
32 "query": { "type": "string", "description": "For recall/search/relate" },
33 "mode": { "type": "string", "description": "Recall: auto|exact|semantic|hybrid" },
34 "as_of": { "type": "string", "description": "Recall facts valid at this date (YYYY-MM-DD)" },
35 "pattern_type": { "type": "string" },
36 "examples": { "type": "array", "items": { "type": "string" } },
37 "confidence": { "type": "number", "description": "0.0-1.0 (default 0.8)" }
38 },
39 "required": ["action"]
40 }),
41 )
42 }
43
44 fn handle(
45 &self,
46 args: &Map<String, Value>,
47 ctx: &ToolContext,
48 ) -> Result<ToolOutput, ErrorData> {
49 let action = get_str(args, "action")
50 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
51 let category = get_str(args, "category");
52 let key = get_str(args, "key");
53 let value = get_str(args, "value");
54 let query = get_str(args, "query");
55 let mode = get_str(args, "mode");
56 let as_of = get_str(args, "as_of");
57 let pattern_type = get_str(args, "pattern_type");
58 let examples = get_str_array(args, "examples");
59 let confidence: Option<f32> = args
60 .get("confidence")
61 .and_then(serde_json::Value::as_f64)
62 .map(|v| v as f32);
63
64 let session_handle = ctx
65 .session
66 .as_ref()
67 .ok_or_else(|| ErrorData::internal_error("session not available", None))?;
68 let (session_id, project_root) = {
69 let timeout_dur =
70 crate::core::io_health::adaptive_timeout(std::time::Duration::from_secs(10));
71 let read_result = tokio::task::block_in_place(|| {
72 tokio::runtime::Handle::current()
73 .block_on(tokio::time::timeout(timeout_dur, session_handle.read()))
74 });
75 if let Ok(session) = read_result {
76 let sid = session.id.clone();
77 let root = session
78 .project_root
79 .clone()
80 .unwrap_or_else(|| ctx.project_root.clone());
81 (sid, root)
82 } else {
83 tracing::warn!("ctx_knowledge: session read-lock timeout, using fallback");
84 ("unknown".to_string(), ctx.project_root.clone())
85 }
86 };
87
88 if action == "gotcha" {
89 let trigger = get_str(args, "trigger").unwrap_or_default();
90 let resolution = get_str(args, "resolution").unwrap_or_default();
91 let severity = get_str(args, "severity").unwrap_or_default();
92 let cat = category.as_deref().unwrap_or("convention");
93
94 if trigger.is_empty() || resolution.is_empty() {
95 return Ok(ToolOutput {
96 text: "ERROR: trigger and resolution are required for gotcha action"
97 .to_string(),
98 original_tokens: 0,
99 saved_tokens: 0,
100 mode: Some(action),
101 path: None,
102 changed: false,
103 shell_outcome: None,
104 });
105 }
106
107 let mut store = crate::core::gotcha_tracker::GotchaStore::load(&project_root);
108 let msg = match store.report_gotcha(&trigger, &resolution, cat, &severity, &session_id)
109 {
110 Some(gotcha) => {
111 let conf = (gotcha.confidence * 100.0) as u32;
112 let label = gotcha.category.short_label();
113 format!("Gotcha recorded: [{label}] {trigger} (confidence: {conf}%)")
114 }
115 None => {
116 format!("Gotcha noted: {trigger} (evicted by higher-confidence entries)")
117 }
118 };
119 let _ = store.save(&project_root);
120 return Ok(ToolOutput {
121 text: msg,
122 original_tokens: 0,
123 saved_tokens: 0,
124 mode: Some(action),
125 path: None,
126 changed: false,
127 shell_outcome: None,
128 });
129 }
130
131 let result = crate::tools::ctx_knowledge::handle(
132 &project_root,
133 &action,
134 category.as_deref(),
135 key.as_deref(),
136 value.as_deref(),
137 query.as_deref(),
138 &session_id,
139 pattern_type.as_deref(),
140 examples,
141 confidence,
142 mode.as_deref(),
143 as_of.as_deref(),
144 );
145
146 Ok(ToolOutput {
147 text: result,
148 original_tokens: 0,
149 saved_tokens: 0,
150 mode: Some(action),
151 path: None,
152 changed: false,
153 shell_outcome: None,
154 })
155 }
156}