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                "allOf": [
38                    {
39                        "if": {
40                            "properties": { "action": { "const": "load" } },
41                            "required": ["action"]
42                        },
43                        "then": { "required": ["action", "category"] }
44                    },
45                    {
46                        "if": {
47                            "properties": { "action": { "const": "unload" } },
48                            "required": ["action"]
49                        },
50                        "then": { "required": ["action", "category"] }
51                    }
52                ],
53                "required": ["action"]
54            }),
55        )
56    }
57
58    fn handle(
59        &self,
60        args: &Map<String, Value>,
61        _ctx: &ToolContext,
62    ) -> Result<ToolOutput, ErrorData> {
63        let action = args
64            .get("action")
65            .and_then(|v| v.as_str())
66            .unwrap_or("list");
67        let category_str = args.get("category").and_then(|v| v.as_str());
68
69        match action {
70            "list" => Ok(ToolOutput::simple(format_category_status())),
71            "load" => {
72                let cat_name = category_str.ok_or_else(|| {
73                    ErrorData::invalid_params("'category' required for load action", None)
74                })?;
75                let cat = ToolCategory::parse(cat_name).ok_or_else(|| {
76                    ErrorData::invalid_params(
77                        format!(
78                            "Unknown category '{cat_name}'. Available: {}",
79                            DynamicToolState::all_categories().join(", ")
80                        ),
81                        None,
82                    )
83                })?;
84                let changed = {
85                    let Ok(mut state) = dynamic_tools::global().lock() else {
86                        return Err(ErrorData::internal_error("dynamic_tools lock failed", None));
87                    };
88                    state.load_category(cat)
89                };
90                let text = if changed {
91                    format!(
92                        "Loaded category '{cat_name}'.\n{}",
93                        format_category_status()
94                    )
95                } else {
96                    format!("Category '{cat_name}' was already loaded.")
97                };
98                let mut out = ToolOutput::simple(text);
99                out.changed = changed;
100                Ok(out)
101            }
102            "unload" => {
103                let cat_name = category_str.ok_or_else(|| {
104                    ErrorData::invalid_params("'category' required for unload action", None)
105                })?;
106                let cat = ToolCategory::parse(cat_name).ok_or_else(|| {
107                    ErrorData::invalid_params(
108                        format!(
109                            "Unknown category '{cat_name}'. Available: {}",
110                            DynamicToolState::all_categories().join(", ")
111                        ),
112                        None,
113                    )
114                })?;
115                let changed = {
116                    let Ok(mut state) = dynamic_tools::global().lock() else {
117                        return Err(ErrorData::internal_error("dynamic_tools lock failed", None));
118                    };
119                    state.unload_category(cat)
120                };
121                let text = if changed {
122                    format!(
123                        "Unloaded category '{cat_name}'.\n{}",
124                        format_category_status()
125                    )
126                } else if cat == ToolCategory::Core {
127                    "Cannot unload 'core' category.".to_string()
128                } else {
129                    format!("Category '{cat_name}' was not loaded.")
130                };
131                let mut out = ToolOutput::simple(text);
132                out.changed = changed;
133                Ok(out)
134            }
135            other => Err(ErrorData::invalid_params(
136                format!("Unknown action '{other}'. Use load|unload|list."),
137                None,
138            )),
139        }
140    }
141}
142
143fn format_category_status() -> String {
144    let Ok(state) = dynamic_tools::global().lock() else {
145        return "dynamic_tools: lock unavailable".to_string();
146    };
147    let active = state.active_categories();
148    let all = DynamicToolState::all_categories();
149    let mut lines = vec![format!(
150        "Dynamic tools: {} (list_changed={})",
151        if state.supports_list_changed() {
152            "active"
153        } else {
154            "all-visible"
155        },
156        state.supports_list_changed()
157    )];
158    for cat in &all {
159        let status = if active.contains(cat) {
160            "loaded"
161        } else {
162            "unloaded"
163        };
164        lines.push(format!("  {cat}: {status}"));
165    }
166    lines.join("\n")
167}