lean_ctx_server/tools/registered/
ctx_load_tools.rs1use rmcp::ErrorData;
3use rmcp::model::Tool;
4use serde_json::{Map, Value, json};
5
6use crate::server::dynamic_tools::{self, DynamicToolState, ToolCategory};
7use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput};
8use lean_ctx::tool_defs::tool_def;
9
10pub struct CtxLoadToolsTool;
11
12impl McpTool for CtxLoadToolsTool {
13 fn name(&self) -> &'static str {
14 "ctx_load_tools"
15 }
16
17 fn tool_def(&self) -> Tool {
18 tool_def(
19 "ctx_load_tools",
20 "Load/unload specialized tool categories to reduce surface area.\n\
21 WORKFLOW: list → load → unload when done.\n\
22 ANTI-PATTERN: don't unload categories you're actively using.\n\
23 Actions: load|unload|list. Categories: arch, debug, memory, metrics, session.\n\
24 Core is always loaded.",
25 json!({
26 "type": "object",
27 "properties": {
28 "action": {
29 "type": "string",
30 "enum": ["load", "unload", "list"],
31 "description": "load|unload|list"
32 },
33 "category": {
34 "type": "string",
35 "description": "arch|debug|memory|metrics|session"
36 }
37 },
38 "allOf": [
39 {
40 "if": {
41 "properties": { "action": { "const": "load" } },
42 "required": ["action"]
43 },
44 "then": { "required": ["action", "category"] }
45 },
46 {
47 "if": {
48 "properties": { "action": { "const": "unload" } },
49 "required": ["action"]
50 },
51 "then": { "required": ["action", "category"] }
52 }
53 ],
54 "required": ["action"]
55 }),
56 )
57 }
58
59 fn handle(
60 &self,
61 args: &Map<String, Value>,
62 _ctx: &ToolContext,
63 ) -> Result<ToolOutput, ErrorData> {
64 let action = args
65 .get("action")
66 .and_then(|v| v.as_str())
67 .unwrap_or("list");
68 let category_str = args.get("category").and_then(|v| v.as_str());
69
70 match action {
71 "list" => Ok(ToolOutput::simple(format_category_status())),
72 "load" => {
73 let cat_name = category_str.ok_or_else(|| {
74 ErrorData::invalid_params("'category' required for load action", None)
75 })?;
76 let cat = ToolCategory::parse(cat_name).ok_or_else(|| {
77 ErrorData::invalid_params(
78 format!(
79 "Unknown category '{cat_name}'. Available: {}",
80 DynamicToolState::all_categories().join(", ")
81 ),
82 None,
83 )
84 })?;
85 let changed = {
86 let Ok(mut state) = dynamic_tools::global().lock() else {
87 return Err(ErrorData::internal_error("dynamic_tools lock failed", None));
88 };
89 state.load_category(cat)
90 };
91 let text = if changed {
92 format!(
93 "Loaded category '{cat_name}'.\n{}",
94 format_category_status()
95 )
96 } else {
97 format!("Category '{cat_name}' was already loaded.")
98 };
99 let mut out = ToolOutput::simple(text);
100 out.changed = changed;
101 Ok(out)
102 }
103 "unload" => {
104 let cat_name = category_str.ok_or_else(|| {
105 ErrorData::invalid_params("'category' required for unload action", None)
106 })?;
107 let cat = ToolCategory::parse(cat_name).ok_or_else(|| {
108 ErrorData::invalid_params(
109 format!(
110 "Unknown category '{cat_name}'. Available: {}",
111 DynamicToolState::all_categories().join(", ")
112 ),
113 None,
114 )
115 })?;
116 let changed = {
117 let Ok(mut state) = dynamic_tools::global().lock() else {
118 return Err(ErrorData::internal_error("dynamic_tools lock failed", None));
119 };
120 state.unload_category(cat)
121 };
122 let text = if changed {
123 format!(
124 "Unloaded category '{cat_name}'.\n{}",
125 format_category_status()
126 )
127 } else if cat == ToolCategory::Core {
128 "Cannot unload 'core' category.".to_string()
129 } else {
130 format!("Category '{cat_name}' was not loaded.")
131 };
132 let mut out = ToolOutput::simple(text);
133 out.changed = changed;
134 Ok(out)
135 }
136 other => Err(ErrorData::invalid_params(
137 format!("Unknown action '{other}'. Use load|unload|list."),
138 None,
139 )),
140 }
141 }
142}
143
144fn format_category_status() -> String {
145 let Ok(state) = dynamic_tools::global().lock() else {
146 return "dynamic_tools: lock unavailable".to_string();
147 };
148 let active = state.active_categories();
149 let all = DynamicToolState::all_categories();
150 let mut lines = vec![format!(
151 "Dynamic tools: {} (list_changed={})",
152 if state.supports_list_changed() {
153 "active"
154 } else {
155 "all-visible"
156 },
157 state.supports_list_changed()
158 )];
159 for cat in &all {
160 let status = if active.contains(cat) {
161 "loaded"
162 } else {
163 "unloaded"
164 };
165 lines.push(format!(" {cat}: {status}"));
166 }
167 lines.join("\n")
168}