lean-ctx 3.6.5

Context Runtime for AI Agents with CCP. 51 MCP tools, 10 read modes, 60+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24+ AI tools. Reduces LLM token consumption by up to 99%.
Documentation
use rmcp::model::Tool;
use rmcp::ErrorData;
use serde_json::{json, Map, Value};

use crate::server::tool_trait::{get_str, get_str_array, McpTool, ToolContext, ToolOutput};
use crate::tool_defs::tool_def;

pub struct CtxKnowledgeTool;

impl McpTool for CtxKnowledgeTool {
    fn name(&self) -> &'static str {
        "ctx_knowledge"
    }

    fn tool_def(&self) -> Tool {
        tool_def(
            "ctx_knowledge",
            "Persistent project knowledge across sessions (facts, patterns, history). Supports recall modes, embeddings, feedback, and typed relations.",
            json!({
                "type": "object",
                "properties": {
                    "action": {
                        "type": "string",
                        "enum": ["policy", "remember", "recall", "pattern", "feedback", "relate", "unrelate", "relations", "relations_diagram", "consolidate", "status", "health", "remove", "export", "timeline", "rooms", "search", "wakeup", "embeddings_status", "embeddings_reset", "embeddings_reindex"],
                        "description": "Knowledge operation to perform."
                    },
                    "trigger": {
                        "type": "string",
                        "description": "For gotcha action: what triggers the bug"
                    },
                    "resolution": {
                        "type": "string",
                        "description": "For gotcha action: how to fix/avoid it"
                    },
                    "severity": {
                        "type": "string",
                        "enum": ["critical", "warning", "info"],
                        "description": "For gotcha action: severity level (default: warning)"
                    },
                    "category": {
                        "type": "string",
                        "description": "Fact category (architecture, api, testing, deployment, conventions, dependencies)"
                    },
                    "key": {
                        "type": "string",
                        "description": "Fact key/identifier"
                    },
                    "value": {
                        "type": "string",
                        "description": "Value for action (fact value, pattern text, feedback up/down, relation kind)."
                    },
                    "query": {
                        "type": "string",
                        "description": "Query/target for recall/relate/relations."
                    },
                    "mode": {
                        "type": "string",
                        "enum": ["auto", "exact", "semantic", "hybrid"],
                        "description": "Recall mode (default: auto)."
                    },
                    "pattern_type": {
                        "type": "string",
                        "description": "Pattern type for pattern action"
                    },
                    "examples": {
                        "type": "array",
                        "items": { "type": "string" },
                        "description": "Examples for pattern action"
                    },
                    "confidence": {
                        "type": "number",
                        "description": "Confidence score 0.0-1.0 for remember action (default: 0.8)"
                    }
                },
                "required": ["action"]
            }),
        )
    }

    fn handle(
        &self,
        args: &Map<String, Value>,
        ctx: &ToolContext,
    ) -> Result<ToolOutput, ErrorData> {
        let action = get_str(args, "action")
            .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
        let category = get_str(args, "category");
        let key = get_str(args, "key");
        let value = get_str(args, "value");
        let query = get_str(args, "query");
        let mode = get_str(args, "mode");
        let pattern_type = get_str(args, "pattern_type");
        let examples = get_str_array(args, "examples");
        let confidence: Option<f32> = args
            .get("confidence")
            .and_then(serde_json::Value::as_f64)
            .map(|v| v as f32);

        let session_handle = ctx.session.as_ref().unwrap();
        let (session_id, project_root) = {
            let session = session_handle.blocking_read();
            let sid = session.id.clone();
            let root = session.project_root.clone().unwrap_or_else(|| {
                std::env::current_dir().map_or_else(
                    |_| "unknown".to_string(),
                    |p| p.to_string_lossy().to_string(),
                )
            });
            (sid, root)
        };

        if action == "gotcha" {
            let trigger = get_str(args, "trigger").unwrap_or_default();
            let resolution = get_str(args, "resolution").unwrap_or_default();
            let severity = get_str(args, "severity").unwrap_or_default();
            let cat = category.as_deref().unwrap_or("convention");

            if trigger.is_empty() || resolution.is_empty() {
                return Ok(ToolOutput {
                    text: "ERROR: trigger and resolution are required for gotcha action"
                        .to_string(),
                    original_tokens: 0,
                    saved_tokens: 0,
                    mode: Some(action),
                    path: None,
                    changed: false,
                });
            }

            let mut store = crate::core::gotcha_tracker::GotchaStore::load(&project_root);
            let msg = match store.report_gotcha(&trigger, &resolution, cat, &severity, &session_id)
            {
                Some(gotcha) => {
                    let conf = (gotcha.confidence * 100.0) as u32;
                    let label = gotcha.category.short_label();
                    format!("Gotcha recorded: [{label}] {trigger} (confidence: {conf}%)")
                }
                None => {
                    format!("Gotcha noted: {trigger} (evicted by higher-confidence entries)")
                }
            };
            let _ = store.save(&project_root);
            return Ok(ToolOutput {
                text: msg,
                original_tokens: 0,
                saved_tokens: 0,
                mode: Some(action),
                path: None,
                changed: false,
            });
        }

        let result = crate::tools::ctx_knowledge::handle(
            &project_root,
            &action,
            category.as_deref(),
            key.as_deref(),
            value.as_deref(),
            query.as_deref(),
            &session_id,
            pattern_type.as_deref(),
            examples,
            confidence,
            mode.as_deref(),
        );

        Ok(ToolOutput {
            text: result,
            original_tokens: 0,
            saved_tokens: 0,
            mode: Some(action),
            path: None,
            changed: false,
        })
    }
}