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