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 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}