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, history). Supports recall modes, embeddings, feedback, and typed relations.",
19 json!({
20 "type": "object",
21 "properties": {
22 "action": {
23 "type": "string",
24 "enum": ["policy", "remember", "recall", "pattern", "feedback", "relate", "unrelate", "relations", "relations_diagram", "consolidate", "status", "health", "lifecycle_report", "remove", "export", "timeline", "rooms", "search", "wakeup", "embeddings_status", "embeddings_reset", "embeddings_reindex"],
25 "description": "Knowledge operation to perform."
26 },
27 "trigger": {
28 "type": "string",
29 "description": "For gotcha action: what triggers the bug"
30 },
31 "resolution": {
32 "type": "string",
33 "description": "For gotcha action: how to fix/avoid it"
34 },
35 "severity": {
36 "type": "string",
37 "enum": ["critical", "warning", "info"],
38 "description": "For gotcha action: severity level (default: warning)"
39 },
40 "category": {
41 "type": "string",
42 "description": "Fact category (architecture, api, testing, deployment, conventions, dependencies)"
43 },
44 "key": {
45 "type": "string",
46 "description": "Fact key/identifier"
47 },
48 "value": {
49 "type": "string",
50 "description": "Value for action (fact value, pattern text, feedback up/down, relation kind)."
51 },
52 "query": {
53 "type": "string",
54 "description": "Query/target for recall/relate/relations."
55 },
56 "mode": {
57 "type": "string",
58 "enum": ["auto", "exact", "semantic", "hybrid"],
59 "description": "Recall mode (default: auto)."
60 },
61 "as_of": {
62 "type": "string",
63 "description": "Temporal recall: only facts valid at this time (RFC 3339 or YYYY-MM-DD). Shows superseded facts with validity windows."
64 },
65 "pattern_type": {
66 "type": "string",
67 "description": "Pattern type for pattern action"
68 },
69 "examples": {
70 "type": "array",
71 "items": { "type": "string" },
72 "description": "Examples for pattern action"
73 },
74 "confidence": {
75 "type": "number",
76 "description": "Confidence score 0.0-1.0 for remember action (default: 0.8)"
77 }
78 },
79 "required": ["action"]
80 }),
81 )
82 }
83
84 fn handle(
85 &self,
86 args: &Map<String, Value>,
87 ctx: &ToolContext,
88 ) -> Result<ToolOutput, ErrorData> {
89 let action = get_str(args, "action")
90 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
91 let category = get_str(args, "category");
92 let key = get_str(args, "key");
93 let value = get_str(args, "value");
94 let query = get_str(args, "query");
95 let mode = get_str(args, "mode");
96 let as_of = get_str(args, "as_of");
97 let pattern_type = get_str(args, "pattern_type");
98 let examples = get_str_array(args, "examples");
99 let confidence: Option<f32> = args
100 .get("confidence")
101 .and_then(serde_json::Value::as_f64)
102 .map(|v| v as f32);
103
104 let session_handle = ctx
105 .session
106 .as_ref()
107 .ok_or_else(|| ErrorData::internal_error("session not available", None))?;
108 let (session_id, project_root) = {
109 let timeout_dur =
110 crate::core::io_health::adaptive_timeout(std::time::Duration::from_secs(10));
111 let read_result = tokio::task::block_in_place(|| {
112 tokio::runtime::Handle::current()
113 .block_on(tokio::time::timeout(timeout_dur, session_handle.read()))
114 });
115 if let Ok(session) = read_result {
116 let sid = session.id.clone();
117 let root = session
118 .project_root
119 .clone()
120 .unwrap_or_else(|| ctx.project_root.clone());
121 (sid, root)
122 } else {
123 tracing::warn!("ctx_knowledge: session read-lock timeout, using fallback");
124 ("unknown".to_string(), ctx.project_root.clone())
125 }
126 };
127
128 if action == "gotcha" {
129 let trigger = get_str(args, "trigger").unwrap_or_default();
130 let resolution = get_str(args, "resolution").unwrap_or_default();
131 let severity = get_str(args, "severity").unwrap_or_default();
132 let cat = category.as_deref().unwrap_or("convention");
133
134 if trigger.is_empty() || resolution.is_empty() {
135 return Ok(ToolOutput {
136 text: "ERROR: trigger and resolution are required for gotcha action"
137 .to_string(),
138 original_tokens: 0,
139 saved_tokens: 0,
140 mode: Some(action),
141 path: None,
142 changed: false,
143 shell_outcome: None,
144 });
145 }
146
147 let mut store = crate::core::gotcha_tracker::GotchaStore::load(&project_root);
148 let msg = match store.report_gotcha(&trigger, &resolution, cat, &severity, &session_id)
149 {
150 Some(gotcha) => {
151 let conf = (gotcha.confidence * 100.0) as u32;
152 let label = gotcha.category.short_label();
153 format!("Gotcha recorded: [{label}] {trigger} (confidence: {conf}%)")
154 }
155 None => {
156 format!("Gotcha noted: {trigger} (evicted by higher-confidence entries)")
157 }
158 };
159 let _ = store.save(&project_root);
160 return Ok(ToolOutput {
161 text: msg,
162 original_tokens: 0,
163 saved_tokens: 0,
164 mode: Some(action),
165 path: None,
166 changed: false,
167 shell_outcome: None,
168 });
169 }
170
171 let result = crate::tools::ctx_knowledge::handle(
172 &project_root,
173 &action,
174 category.as_deref(),
175 key.as_deref(),
176 value.as_deref(),
177 query.as_deref(),
178 &session_id,
179 pattern_type.as_deref(),
180 examples,
181 confidence,
182 mode.as_deref(),
183 as_of.as_deref(),
184 );
185
186 Ok(ToolOutput {
187 text: result,
188 original_tokens: 0,
189 saved_tokens: 0,
190 mode: Some(action),
191 path: None,
192 changed: false,
193 shell_outcome: None,
194 })
195 }
196}