Skip to main content

lean_ctx/tools/registered/
ctx_load_tools.rs

1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{json, Map, Value};
4
5use crate::server::dynamic_tools::{self, DynamicToolState, ToolCategory};
6use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput};
7use crate::tool_defs::tool_def;
8
9pub struct CtxLoadToolsTool;
10
11impl McpTool for CtxLoadToolsTool {
12    fn name(&self) -> &'static str {
13        "ctx_load_tools"
14    }
15
16    fn tool_def(&self) -> Tool {
17        tool_def(
18            "ctx_load_tools",
19            "Load/unload specialized tool categories on demand. Categories: arch, debug, memory, metrics, session. Core is always loaded.",
20            json!({
21                "type": "object",
22                "properties": {
23                    "action": {
24                        "type": "string",
25                        "enum": ["load", "unload", "list"],
26                        "description": "load = activate category, unload = deactivate, list = show status"
27                    },
28                    "category": {
29                        "type": "string",
30                        "description": "Category name: arch|debug|memory|metrics|session"
31                    }
32                },
33                "required": ["action"]
34            }),
35        )
36    }
37
38    fn handle(
39        &self,
40        args: &Map<String, Value>,
41        _ctx: &ToolContext,
42    ) -> Result<ToolOutput, ErrorData> {
43        let action = args
44            .get("action")
45            .and_then(|v| v.as_str())
46            .unwrap_or("list");
47        let category_str = args.get("category").and_then(|v| v.as_str());
48
49        match action {
50            "list" => Ok(ToolOutput::simple(format_category_status())),
51            "load" => {
52                let cat_name = category_str.ok_or_else(|| {
53                    ErrorData::invalid_params("'category' required for load action", None)
54                })?;
55                let cat = ToolCategory::parse(cat_name).ok_or_else(|| {
56                    ErrorData::invalid_params(
57                        format!(
58                            "Unknown category '{cat_name}'. Available: {}",
59                            DynamicToolState::all_categories().join(", ")
60                        ),
61                        None,
62                    )
63                })?;
64                let changed = {
65                    let mut state = dynamic_tools::global().lock().unwrap();
66                    state.load_category(cat)
67                };
68                let text = if changed {
69                    format!(
70                        "Loaded category '{cat_name}'.\n{}",
71                        format_category_status()
72                    )
73                } else {
74                    format!("Category '{cat_name}' was already loaded.")
75                };
76                let mut out = ToolOutput::simple(text);
77                out.changed = changed;
78                Ok(out)
79            }
80            "unload" => {
81                let cat_name = category_str.ok_or_else(|| {
82                    ErrorData::invalid_params("'category' required for unload action", None)
83                })?;
84                let cat = ToolCategory::parse(cat_name).ok_or_else(|| {
85                    ErrorData::invalid_params(
86                        format!(
87                            "Unknown category '{cat_name}'. Available: {}",
88                            DynamicToolState::all_categories().join(", ")
89                        ),
90                        None,
91                    )
92                })?;
93                let changed = {
94                    let mut state = dynamic_tools::global().lock().unwrap();
95                    state.unload_category(cat)
96                };
97                let text = if changed {
98                    format!(
99                        "Unloaded category '{cat_name}'.\n{}",
100                        format_category_status()
101                    )
102                } else if cat == ToolCategory::Core {
103                    "Cannot unload 'core' category.".to_string()
104                } else {
105                    format!("Category '{cat_name}' was not loaded.")
106                };
107                let mut out = ToolOutput::simple(text);
108                out.changed = changed;
109                Ok(out)
110            }
111            other => Err(ErrorData::invalid_params(
112                format!("Unknown action '{other}'. Use load|unload|list."),
113                None,
114            )),
115        }
116    }
117}
118
119fn format_category_status() -> String {
120    let state = dynamic_tools::global().lock().unwrap();
121    let active = state.active_categories();
122    let all = DynamicToolState::all_categories();
123    let mut lines = vec![format!(
124        "Dynamic tools: {} (list_changed={})",
125        if state.supports_list_changed() {
126            "active"
127        } else {
128            "all-visible"
129        },
130        state.supports_list_changed()
131    )];
132    for cat in &all {
133        let status = if active.contains(cat) {
134            "loaded"
135        } else {
136            "unloaded"
137        };
138        lines.push(format!("  {cat}: {status}"));
139    }
140    lines.join("\n")
141}