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 Ok(mut state) = dynamic_tools::global().lock() else {
66                        return Err(ErrorData::internal_error("dynamic_tools lock failed", None));
67                    };
68                    state.load_category(cat)
69                };
70                let text = if changed {
71                    format!(
72                        "Loaded category '{cat_name}'.\n{}",
73                        format_category_status()
74                    )
75                } else {
76                    format!("Category '{cat_name}' was already loaded.")
77                };
78                let mut out = ToolOutput::simple(text);
79                out.changed = changed;
80                Ok(out)
81            }
82            "unload" => {
83                let cat_name = category_str.ok_or_else(|| {
84                    ErrorData::invalid_params("'category' required for unload action", None)
85                })?;
86                let cat = ToolCategory::parse(cat_name).ok_or_else(|| {
87                    ErrorData::invalid_params(
88                        format!(
89                            "Unknown category '{cat_name}'. Available: {}",
90                            DynamicToolState::all_categories().join(", ")
91                        ),
92                        None,
93                    )
94                })?;
95                let changed = {
96                    let Ok(mut state) = dynamic_tools::global().lock() else {
97                        return Err(ErrorData::internal_error("dynamic_tools lock failed", None));
98                    };
99                    state.unload_category(cat)
100                };
101                let text = if changed {
102                    format!(
103                        "Unloaded category '{cat_name}'.\n{}",
104                        format_category_status()
105                    )
106                } else if cat == ToolCategory::Core {
107                    "Cannot unload 'core' category.".to_string()
108                } else {
109                    format!("Category '{cat_name}' was not loaded.")
110                };
111                let mut out = ToolOutput::simple(text);
112                out.changed = changed;
113                Ok(out)
114            }
115            other => Err(ErrorData::invalid_params(
116                format!("Unknown action '{other}'. Use load|unload|list."),
117                None,
118            )),
119        }
120    }
121}
122
123fn format_category_status() -> String {
124    let Ok(state) = dynamic_tools::global().lock() else {
125        return "dynamic_tools: lock unavailable".to_string();
126    };
127    let active = state.active_categories();
128    let all = DynamicToolState::all_categories();
129    let mut lines = vec![format!(
130        "Dynamic tools: {} (list_changed={})",
131        if state.supports_list_changed() {
132            "active"
133        } else {
134            "all-visible"
135        },
136        state.supports_list_changed()
137    )];
138    for cat in &all {
139        let status = if active.contains(cat) {
140            "loaded"
141        } else {
142            "unloaded"
143        };
144        lines.push(format!("  {cat}: {status}"));
145    }
146    lines.join("\n")
147}