lean_ctx/tools/registered/
ctx_load_tools.rs1use 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}