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