Skip to main content

lean_ctx/tools/registered/
ctx_load_tools.rs

1use rmcp::ErrorData;
2use rmcp::model::Tool;
3use serde_json::{Map, Value, json};
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 to reduce surface area.\n\
20             WORKFLOW: list → load → unload when done.\n\
21             ANTI-PATTERN: don't unload categories you're actively using.\n\
22             Actions: load|unload|list. Categories: arch, debug, memory, metrics, session.\n\
23             Core is always loaded.",
24            json!({
25                "type": "object",
26                "properties": {
27                    "action": {
28                        "type": "string",
29                        "enum": ["load", "unload", "list"],
30                        "description": "load|unload|list"
31                    },
32                    "category": {
33                        "type": "string",
34                        "description": "arch|debug|memory|metrics|session"
35                    }
36                },
37                "required": ["action"]
38            }),
39        )
40    }
41
42    fn handle(
43        &self,
44        args: &Map<String, Value>,
45        _ctx: &ToolContext,
46    ) -> Result<ToolOutput, ErrorData> {
47        let action = args
48            .get("action")
49            .and_then(|v| v.as_str())
50            .unwrap_or("list");
51        let category_str = args.get("category").and_then(|v| v.as_str());
52
53        match action {
54            "list" => Ok(ToolOutput::simple(format_category_status())),
55            "load" => {
56                let cat_name = category_str.ok_or_else(|| {
57                    ErrorData::invalid_params("'category' required for load action", None)
58                })?;
59                let cat = ToolCategory::parse(cat_name).ok_or_else(|| {
60                    ErrorData::invalid_params(
61                        format!(
62                            "Unknown category '{cat_name}'. Available: {}",
63                            DynamicToolState::all_categories().join(", ")
64                        ),
65                        None,
66                    )
67                })?;
68                let changed = {
69                    let Ok(mut state) = dynamic_tools::global().lock() else {
70                        return Err(ErrorData::internal_error("dynamic_tools lock failed", None));
71                    };
72                    state.load_category(cat)
73                };
74                let text = if changed {
75                    format!(
76                        "Loaded category '{cat_name}'.\n{}",
77                        format_category_status()
78                    )
79                } else {
80                    format!("Category '{cat_name}' was already loaded.")
81                };
82                let mut out = ToolOutput::simple(text);
83                out.changed = changed;
84                Ok(out)
85            }
86            "unload" => {
87                let cat_name = category_str.ok_or_else(|| {
88                    ErrorData::invalid_params("'category' required for unload action", None)
89                })?;
90                let cat = ToolCategory::parse(cat_name).ok_or_else(|| {
91                    ErrorData::invalid_params(
92                        format!(
93                            "Unknown category '{cat_name}'. Available: {}",
94                            DynamicToolState::all_categories().join(", ")
95                        ),
96                        None,
97                    )
98                })?;
99                let changed = {
100                    let Ok(mut state) = dynamic_tools::global().lock() else {
101                        return Err(ErrorData::internal_error("dynamic_tools lock failed", None));
102                    };
103                    state.unload_category(cat)
104                };
105                let text = if changed {
106                    format!(
107                        "Unloaded category '{cat_name}'.\n{}",
108                        format_category_status()
109                    )
110                } else if cat == ToolCategory::Core {
111                    "Cannot unload 'core' category.".to_string()
112                } else {
113                    format!("Category '{cat_name}' was not loaded.")
114                };
115                let mut out = ToolOutput::simple(text);
116                out.changed = changed;
117                Ok(out)
118            }
119            other => Err(ErrorData::invalid_params(
120                format!("Unknown action '{other}'. Use load|unload|list."),
121                None,
122            )),
123        }
124    }
125}
126
127fn format_category_status() -> String {
128    let Ok(state) = dynamic_tools::global().lock() else {
129        return "dynamic_tools: lock unavailable".to_string();
130    };
131    let active = state.active_categories();
132    let all = DynamicToolState::all_categories();
133    let mut lines = vec![format!(
134        "Dynamic tools: {} (list_changed={})",
135        if state.supports_list_changed() {
136            "active"
137        } else {
138            "all-visible"
139        },
140        state.supports_list_changed()
141    )];
142    for cat in &all {
143        let status = if active.contains(cat) {
144            "loaded"
145        } else {
146            "unloaded"
147        };
148        lines.push(format!("  {cat}: {status}"));
149    }
150    lines.join("\n")
151}