Skip to main content

lean_ctx/tools/registered/
ctx_knowledge.rs

1use 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}