1use md5::{Digest, Md5};
2use rmcp::handler::server::ServerHandler;
3use rmcp::model::*;
4use rmcp::service::{RequestContext, RoleServer};
5use rmcp::ErrorData;
6use serde_json::Value;
7
8use crate::tools::{CrpMode, LeanCtxServer};
9
10impl ServerHandler for LeanCtxServer {
11 fn get_info(&self) -> ServerInfo {
12 let capabilities = ServerCapabilities::builder().enable_tools().build();
13
14 let instructions = crate::instructions::build_instructions(self.crp_mode);
15
16 InitializeResult::new(capabilities)
17 .with_server_info(Implementation::new("lean-ctx", env!("CARGO_PKG_VERSION")))
18 .with_instructions(instructions)
19 }
20
21 async fn initialize(
22 &self,
23 request: InitializeRequestParams,
24 _context: RequestContext<RoleServer>,
25 ) -> Result<InitializeResult, ErrorData> {
26 let name = request.client_info.name.clone();
27 tracing::info!("MCP client connected: {:?}", name);
28 *self.client_name.write().await = name.clone();
29
30 tokio::task::spawn_blocking(|| {
31 if let Some(home) = dirs::home_dir() {
32 let _ = crate::rules_inject::inject_all_rules(&home);
33 }
34 crate::hooks::refresh_installed_hooks();
35 crate::core::version_check::check_background();
36 });
37
38 let instructions =
39 crate::instructions::build_instructions_with_client(self.crp_mode, &name);
40 let capabilities = ServerCapabilities::builder().enable_tools().build();
41
42 Ok(InitializeResult::new(capabilities)
43 .with_server_info(Implementation::new("lean-ctx", env!("CARGO_PKG_VERSION")))
44 .with_instructions(instructions))
45 }
46
47 async fn list_tools(
48 &self,
49 _request: Option<PaginatedRequestParams>,
50 _context: RequestContext<RoleServer>,
51 ) -> Result<ListToolsResult, ErrorData> {
52 let all_tools = if crate::tool_defs::is_lazy_mode() {
53 crate::tool_defs::lazy_tool_defs()
54 } else if std::env::var("LEAN_CTX_UNIFIED").is_ok()
55 && std::env::var("LEAN_CTX_FULL_TOOLS").is_err()
56 {
57 crate::tool_defs::unified_tool_defs()
58 } else {
59 crate::tool_defs::granular_tool_defs()
60 };
61
62 let disabled = crate::core::config::Config::load().disabled_tools_effective();
63 let tools = if disabled.is_empty() {
64 all_tools
65 } else {
66 all_tools
67 .into_iter()
68 .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
69 .collect()
70 };
71
72 let tools = {
73 let active = self.workflow.read().await.clone();
74 if let Some(run) = active {
75 if let Some(state) = run.spec.state(&run.current) {
76 if let Some(allowed) = &state.allowed_tools {
77 let mut allow: std::collections::HashSet<&str> =
78 allowed.iter().map(|s| s.as_str()).collect();
79 allow.insert("ctx");
80 allow.insert("ctx_workflow");
81 return Ok(ListToolsResult {
82 tools: tools
83 .into_iter()
84 .filter(|t| allow.contains(t.name.as_ref()))
85 .collect(),
86 ..Default::default()
87 });
88 }
89 }
90 }
91 tools
92 };
93
94 Ok(ListToolsResult {
95 tools,
96 ..Default::default()
97 })
98 }
99
100 async fn call_tool(
101 &self,
102 request: CallToolRequestParams,
103 _context: RequestContext<RoleServer>,
104 ) -> Result<CallToolResult, ErrorData> {
105 self.check_idle_expiry().await;
106
107 let original_name = request.name.as_ref().to_string();
108 let (resolved_name, resolved_args) = if original_name == "ctx" {
109 let sub = request
110 .arguments
111 .as_ref()
112 .and_then(|a| a.get("tool"))
113 .and_then(|v| v.as_str())
114 .map(|s| s.to_string())
115 .ok_or_else(|| {
116 ErrorData::invalid_params("'tool' is required for ctx meta-tool", None)
117 })?;
118 let tool_name = if sub.starts_with("ctx_") {
119 sub
120 } else {
121 format!("ctx_{sub}")
122 };
123 let mut args = request.arguments.unwrap_or_default();
124 args.remove("tool");
125 (tool_name, Some(args))
126 } else {
127 (original_name, request.arguments)
128 };
129 let name = resolved_name.as_str();
130 let args = &resolved_args;
131
132 if name != "ctx_workflow" {
133 let active = self.workflow.read().await.clone();
134 if let Some(run) = active {
135 if let Some(state) = run.spec.state(&run.current) {
136 if let Some(allowed) = &state.allowed_tools {
137 let allowed_ok = allowed.iter().any(|t| t == name) || name == "ctx";
138 if !allowed_ok {
139 let mut shown = allowed.clone();
140 shown.sort();
141 shown.truncate(30);
142 return Ok(CallToolResult::success(vec![Content::text(format!(
143 "Tool '{name}' blocked by workflow '{}' (state: {}). Allowed ({} shown): {}",
144 run.spec.name,
145 run.current,
146 shown.len(),
147 shown.join(", ")
148 ))]));
149 }
150 }
151 }
152 }
153 }
154
155 let auto_context = {
156 let task = {
157 let session = self.session.read().await;
158 session.task.as_ref().map(|t| t.description.clone())
159 };
160 let project_root = {
161 let session = self.session.read().await;
162 session.project_root.clone()
163 };
164 let mut cache = self.cache.write().await;
165 crate::tools::autonomy::session_lifecycle_pre_hook(
166 &self.autonomy,
167 name,
168 &mut cache,
169 task.as_deref(),
170 project_root.as_deref(),
171 self.crp_mode,
172 )
173 };
174
175 let throttle_result = {
176 let fp = args
177 .as_ref()
178 .map(|a| {
179 crate::core::loop_detection::LoopDetector::fingerprint(
180 &serde_json::Value::Object(a.clone()),
181 )
182 })
183 .unwrap_or_default();
184 let mut detector = self.loop_detector.write().await;
185
186 let is_search = crate::core::loop_detection::LoopDetector::is_search_tool(name);
187 let is_search_shell = name == "ctx_shell" && {
188 let cmd = args
189 .as_ref()
190 .and_then(|a| a.get("command"))
191 .and_then(|v| v.as_str())
192 .unwrap_or("");
193 crate::core::loop_detection::LoopDetector::is_search_shell_command(cmd)
194 };
195
196 if is_search || is_search_shell {
197 let search_pattern = args.as_ref().and_then(|a| {
198 a.get("pattern")
199 .or_else(|| a.get("query"))
200 .and_then(|v| v.as_str())
201 });
202 let shell_pattern = if is_search_shell {
203 args.as_ref()
204 .and_then(|a| a.get("command"))
205 .and_then(|v| v.as_str())
206 .and_then(extract_search_pattern_from_command)
207 } else {
208 None
209 };
210 let pat = search_pattern.or(shell_pattern.as_deref());
211 detector.record_search(name, &fp, pat)
212 } else {
213 detector.record_call(name, &fp)
214 }
215 };
216
217 if throttle_result.level == crate::core::loop_detection::ThrottleLevel::Blocked {
218 let msg = throttle_result.message.unwrap_or_default();
219 return Ok(CallToolResult::success(vec![Content::text(msg)]));
220 }
221
222 let throttle_warning =
223 if throttle_result.level == crate::core::loop_detection::ThrottleLevel::Reduced {
224 throttle_result.message.clone()
225 } else {
226 None
227 };
228
229 let tool_start = std::time::Instant::now();
230 let result_text = match name {
231 "ctx_read" => {
232 let path = match get_str(args, "path") {
233 Some(p) => self
234 .resolve_path(&p)
235 .await
236 .map_err(|e| ErrorData::invalid_params(e, None))?,
237 None => return Err(ErrorData::invalid_params("path is required", None)),
238 };
239 let current_task = {
240 let session = self.session.read().await;
241 session.task.as_ref().map(|t| t.description.clone())
242 };
243 let task_ref = current_task.as_deref();
244 let mut mode = match get_str(args, "mode") {
245 Some(m) => m,
246 None => {
247 let cache = self.cache.read().await;
248 crate::tools::ctx_smart_read::select_mode_with_task(&cache, &path, task_ref)
249 }
250 };
251 let fresh = get_bool(args, "fresh").unwrap_or(false);
252 let start_line = get_int(args, "start_line");
253 if let Some(sl) = start_line {
254 let sl = sl.max(1_i64);
255 mode = format!("lines:{sl}-999999");
256 }
257 let stale = self.is_prompt_cache_stale().await;
258 let effective_mode = LeanCtxServer::upgrade_mode_if_stale(&mode, stale).to_string();
259 let mut cache = self.cache.write().await;
260 let (output, resolved_mode) = if fresh {
261 crate::tools::ctx_read::handle_fresh_with_task_resolved(
262 &mut cache,
263 &path,
264 &effective_mode,
265 self.crp_mode,
266 task_ref,
267 )
268 } else {
269 crate::tools::ctx_read::handle_with_task_resolved(
270 &mut cache,
271 &path,
272 &effective_mode,
273 self.crp_mode,
274 task_ref,
275 )
276 };
277 let stale_note = if effective_mode != mode {
278 format!("[cache stale, {mode}→{effective_mode}]\n")
279 } else {
280 String::new()
281 };
282 let original = cache.get(&path).map_or(0, |e| e.original_tokens);
283 let output_tokens = crate::core::tokens::count_tokens(&output);
284 let saved = original.saturating_sub(output_tokens);
285 let is_cache_hit = output.contains(" cached ");
286 let output = format!("{stale_note}{output}");
287 let file_ref = cache.file_ref_map().get(&path).cloned();
288 drop(cache);
289 let mut ensured_root: Option<String> = None;
290 {
291 let mut session = self.session.write().await;
292 session.touch_file(&path, file_ref.as_deref(), &resolved_mode, original);
293 if is_cache_hit {
294 session.record_cache_hit();
295 }
296 let root_missing = session
297 .project_root
298 .as_deref()
299 .map(|r| r.trim().is_empty())
300 .unwrap_or(true);
301 if root_missing {
302 if let Some(root) = crate::core::protocol::detect_project_root(&path) {
303 session.project_root = Some(root.clone());
304 ensured_root = Some(root.clone());
305 let mut current = self.agent_id.write().await;
306 if current.is_none() {
307 let mut registry =
308 crate::core::agents::AgentRegistry::load_or_create();
309 registry.cleanup_stale(24);
310 let role = std::env::var("LEAN_CTX_AGENT_ROLE").ok();
311 let id = registry.register("mcp", role.as_deref(), &root);
312 let _ = registry.save();
313 *current = Some(id);
314 }
315 }
316 }
317 }
318 if let Some(root) = ensured_root.as_deref() {
319 crate::core::index_orchestrator::ensure_all_background(root);
320 }
321 self.record_call("ctx_read", original, saved, Some(resolved_mode.clone()))
322 .await;
323 crate::core::heatmap::record_file_access(&path, original, saved);
324 {
325 let sig =
326 crate::core::mode_predictor::FileSignature::from_path(&path, original);
327 let density = if output_tokens > 0 {
328 original as f64 / output_tokens as f64
329 } else {
330 1.0
331 };
332 let outcome = crate::core::mode_predictor::ModeOutcome {
333 mode: resolved_mode.clone(),
334 tokens_in: original,
335 tokens_out: output_tokens,
336 density: density.min(1.0),
337 };
338 let mut predictor = crate::core::mode_predictor::ModePredictor::new();
339 predictor.record(sig, outcome);
340 predictor.save();
341
342 let ext = std::path::Path::new(&path)
343 .extension()
344 .and_then(|e| e.to_str())
345 .unwrap_or("")
346 .to_string();
347 let thresholds = crate::core::adaptive_thresholds::thresholds_for_path(&path);
348 let cache = self.cache.read().await;
349 let stats = cache.get_stats();
350 let feedback_outcome = crate::core::feedback::CompressionOutcome {
351 session_id: format!("{}", std::process::id()),
352 language: ext,
353 entropy_threshold: thresholds.bpe_entropy,
354 jaccard_threshold: thresholds.jaccard,
355 total_turns: stats.total_reads as u32,
356 tokens_saved: saved as u64,
357 tokens_original: original as u64,
358 cache_hits: stats.cache_hits as u32,
359 total_reads: stats.total_reads as u32,
360 task_completed: true,
361 timestamp: chrono::Local::now().to_rfc3339(),
362 };
363 drop(cache);
364 let mut store = crate::core::feedback::FeedbackStore::load();
365 store.record_outcome(feedback_outcome);
366 }
367 output
368 }
369 "ctx_multi_read" => {
370 let raw_paths = get_str_array(args, "paths")
371 .ok_or_else(|| ErrorData::invalid_params("paths array is required", None))?;
372 let mut paths = Vec::with_capacity(raw_paths.len());
373 for p in raw_paths {
374 paths.push(
375 self.resolve_path(&p)
376 .await
377 .map_err(|e| ErrorData::invalid_params(e, None))?,
378 );
379 }
380 let mode = get_str(args, "mode").unwrap_or_else(|| "full".to_string());
381 let current_task = {
382 let session = self.session.read().await;
383 session.task.as_ref().map(|t| t.description.clone())
384 };
385 let mut cache = self.cache.write().await;
386 let output = crate::tools::ctx_multi_read::handle_with_task(
387 &mut cache,
388 &paths,
389 &mode,
390 self.crp_mode,
391 current_task.as_deref(),
392 );
393 let mut total_original: usize = 0;
394 for path in &paths {
395 total_original = total_original
396 .saturating_add(cache.get(path).map(|e| e.original_tokens).unwrap_or(0));
397 }
398 let tokens = crate::core::tokens::count_tokens(&output);
399 drop(cache);
400 self.record_call(
401 "ctx_multi_read",
402 total_original,
403 total_original.saturating_sub(tokens),
404 Some(mode),
405 )
406 .await;
407 output
408 }
409 "ctx_tree" => {
410 let path = self
411 .resolve_path(&get_str(args, "path").unwrap_or_else(|| ".".to_string()))
412 .await
413 .map_err(|e| ErrorData::invalid_params(e, None))?;
414 let depth = get_int(args, "depth").unwrap_or(3) as usize;
415 let show_hidden = get_bool(args, "show_hidden").unwrap_or(false);
416 let (result, original) = crate::tools::ctx_tree::handle(&path, depth, show_hidden);
417 let sent = crate::core::tokens::count_tokens(&result);
418 let saved = original.saturating_sub(sent);
419 self.record_call("ctx_tree", original, saved, None).await;
420 let savings_note = if saved > 0 {
421 format!("\n[saved {saved} tokens vs native ls]")
422 } else {
423 String::new()
424 };
425 format!("{result}{savings_note}")
426 }
427 "ctx_shell" => {
428 let command = get_str(args, "command")
429 .ok_or_else(|| ErrorData::invalid_params("command is required", None))?;
430
431 if let Some(rejection) = crate::tools::ctx_shell::validate_command(&command) {
432 self.record_call("ctx_shell", 0, 0, None).await;
433 return Ok(CallToolResult::success(vec![Content::text(rejection)]));
434 }
435
436 let explicit_cwd = get_str(args, "cwd");
437 let effective_cwd = {
438 let session = self.session.read().await;
439 session.effective_cwd(explicit_cwd.as_deref())
440 };
441
442 let ensured_root = {
443 let mut session = self.session.write().await;
444 session.update_shell_cwd(&command);
445 let root_missing = session
446 .project_root
447 .as_deref()
448 .map(|r| r.trim().is_empty())
449 .unwrap_or(true);
450 if !root_missing {
451 None
452 } else {
453 let home = dirs::home_dir().map(|h| h.to_string_lossy().to_string());
454 crate::core::protocol::detect_project_root(&effective_cwd).and_then(|r| {
455 if home.as_deref() == Some(r.as_str()) {
456 None
457 } else {
458 session.project_root = Some(r.clone());
459 Some(r)
460 }
461 })
462 }
463 };
464 if let Some(root) = ensured_root.as_deref() {
465 crate::core::index_orchestrator::ensure_all_background(root);
466 let mut current = self.agent_id.write().await;
467 if current.is_none() {
468 let mut registry = crate::core::agents::AgentRegistry::load_or_create();
469 registry.cleanup_stale(24);
470 let role = std::env::var("LEAN_CTX_AGENT_ROLE").ok();
471 let id = registry.register("mcp", role.as_deref(), root);
472 let _ = registry.save();
473 *current = Some(id);
474 }
475 }
476
477 let raw = get_bool(args, "raw").unwrap_or(false)
478 || std::env::var("LEAN_CTX_DISABLED").is_ok();
479 let cmd_clone = command.clone();
480 let cwd_clone = effective_cwd.clone();
481 let crp_mode = self.crp_mode;
482
483 let (result_out, original, saved, tee_hint) =
484 tokio::task::spawn_blocking(move || {
485 let (output, _real_exit_code) = execute_command_in(&cmd_clone, &cwd_clone);
486
487 if raw {
489 let tokens = crate::core::tokens::count_tokens(&output);
490 (output, tokens, 0, String::new())
491 } else {
492 let result =
493 crate::tools::ctx_shell::handle(&cmd_clone, &output, crp_mode);
494 let original = crate::core::tokens::count_tokens(&output);
495 let sent = crate::core::tokens::count_tokens(&result);
496 let saved = original.saturating_sub(sent);
497
498 let cfg = crate::core::config::Config::load();
499 let tee_hint = match cfg.tee_mode {
500 crate::core::config::TeeMode::Always => {
501 crate::shell::save_tee(&cmd_clone, &output)
502 .map(|p| format!("\n[full output: {p}]"))
503 .unwrap_or_default()
504 }
505 crate::core::config::TeeMode::Failures
506 if !output.trim().is_empty()
507 && (output.contains("error")
508 || output.contains("Error")
509 || output.contains("ERROR")) =>
510 {
511 crate::shell::save_tee(&cmd_clone, &output)
512 .map(|p| format!("\n[full output: {p}]"))
513 .unwrap_or_default()
514 }
515 _ => String::new(),
516 };
517
518 (result, original, saved, tee_hint)
524 }
525 })
526 .await
527 .unwrap_or_else(|e| {
528 (
529 format!("ERROR: shell task failed: {e}"),
530 0,
531 0,
532 String::new(),
533 )
534 });
535
536 self.record_call("ctx_shell", original, saved, None).await;
537
538 let savings_note = if !raw && saved > 0 {
539 format!("\n[saved {saved} tokens vs native Shell]")
540 } else {
541 String::new()
542 };
543
544 format!("{result_out}{savings_note}{tee_hint}")
545 }
546 "ctx_search" => {
547 let pattern = get_str(args, "pattern")
548 .ok_or_else(|| ErrorData::invalid_params("pattern is required", None))?;
549 let path = self
550 .resolve_path(&get_str(args, "path").unwrap_or_else(|| ".".to_string()))
551 .await
552 .map_err(|e| ErrorData::invalid_params(e, None))?;
553 let ext = get_str(args, "ext");
554 let max = get_int(args, "max_results").unwrap_or(20) as usize;
555 let no_gitignore = get_bool(args, "ignore_gitignore").unwrap_or(false);
556 let crp = self.crp_mode;
557 let respect = !no_gitignore;
558 let search_result = tokio::time::timeout(
559 std::time::Duration::from_secs(30),
560 tokio::task::spawn_blocking(move || {
561 crate::tools::ctx_search::handle(
562 &pattern,
563 &path,
564 ext.as_deref(),
565 max,
566 crp,
567 respect,
568 )
569 }),
570 )
571 .await;
572 let (result, original) = match search_result {
573 Ok(Ok(r)) => r,
574 Ok(Err(e)) => {
575 return Err(ErrorData::internal_error(
576 format!("search task failed: {e}"),
577 None,
578 ))
579 }
580 Err(_) => {
581 let msg = "ctx_search timed out after 30s. Try narrowing the search:\n\
582 • Use a more specific pattern\n\
583 • Specify ext= to limit file types\n\
584 • Specify a subdirectory in path=";
585 self.record_call("ctx_search", 0, 0, None).await;
586 return Ok(CallToolResult::success(vec![Content::text(msg)]));
587 }
588 };
589 let sent = crate::core::tokens::count_tokens(&result);
590 let saved = original.saturating_sub(sent);
591 self.record_call("ctx_search", original, saved, None).await;
592 let savings_note = if saved > 0 {
593 format!("\n[saved {saved} tokens vs native Grep]")
594 } else {
595 String::new()
596 };
597 format!("{result}{savings_note}")
598 }
599 "ctx_compress" => {
600 let include_sigs = get_bool(args, "include_signatures").unwrap_or(true);
601 let cache = self.cache.read().await;
602 let result =
603 crate::tools::ctx_compress::handle(&cache, include_sigs, self.crp_mode);
604 drop(cache);
605 self.record_call("ctx_compress", 0, 0, None).await;
606 result
607 }
608 "ctx_benchmark" => {
609 let path = match get_str(args, "path") {
610 Some(p) => self
611 .resolve_path(&p)
612 .await
613 .map_err(|e| ErrorData::invalid_params(e, None))?,
614 None => return Err(ErrorData::invalid_params("path is required", None)),
615 };
616 let action = get_str(args, "action").unwrap_or_default();
617 let result = if action == "project" {
618 let fmt = get_str(args, "format").unwrap_or_default();
619 let bench = crate::core::benchmark::run_project_benchmark(&path);
620 match fmt.as_str() {
621 "json" => crate::core::benchmark::format_json(&bench),
622 "markdown" | "md" => crate::core::benchmark::format_markdown(&bench),
623 _ => crate::core::benchmark::format_terminal(&bench),
624 }
625 } else {
626 crate::tools::ctx_benchmark::handle(&path, self.crp_mode)
627 };
628 self.record_call("ctx_benchmark", 0, 0, None).await;
629 result
630 }
631 "ctx_metrics" => {
632 let cache = self.cache.read().await;
633 let calls = self.tool_calls.read().await;
634 let result = crate::tools::ctx_metrics::handle(&cache, &calls, self.crp_mode);
635 drop(cache);
636 drop(calls);
637 self.record_call("ctx_metrics", 0, 0, None).await;
638 result
639 }
640 "ctx_analyze" => {
641 let path = match get_str(args, "path") {
642 Some(p) => self
643 .resolve_path(&p)
644 .await
645 .map_err(|e| ErrorData::invalid_params(e, None))?,
646 None => return Err(ErrorData::invalid_params("path is required", None)),
647 };
648 let result = crate::tools::ctx_analyze::handle(&path, self.crp_mode);
649 self.record_call("ctx_analyze", 0, 0, None).await;
650 result
651 }
652 "ctx_discover" => {
653 let limit = get_int(args, "limit").unwrap_or(15) as usize;
654 let history = crate::cli::load_shell_history_pub();
655 let result = crate::tools::ctx_discover::discover_from_history(&history, limit);
656 self.record_call("ctx_discover", 0, 0, None).await;
657 result
658 }
659 "ctx_smart_read" => {
660 let path = match get_str(args, "path") {
661 Some(p) => self
662 .resolve_path(&p)
663 .await
664 .map_err(|e| ErrorData::invalid_params(e, None))?,
665 None => return Err(ErrorData::invalid_params("path is required", None)),
666 };
667 let mut cache = self.cache.write().await;
668 let output = crate::tools::ctx_smart_read::handle(&mut cache, &path, self.crp_mode);
669 let original = cache.get(&path).map_or(0, |e| e.original_tokens);
670 let tokens = crate::core::tokens::count_tokens(&output);
671 drop(cache);
672 self.record_call(
673 "ctx_smart_read",
674 original,
675 original.saturating_sub(tokens),
676 Some("auto".to_string()),
677 )
678 .await;
679 output
680 }
681 "ctx_delta" => {
682 let path = match get_str(args, "path") {
683 Some(p) => self
684 .resolve_path(&p)
685 .await
686 .map_err(|e| ErrorData::invalid_params(e, None))?,
687 None => return Err(ErrorData::invalid_params("path is required", None)),
688 };
689 let mut cache = self.cache.write().await;
690 let output = crate::tools::ctx_delta::handle(&mut cache, &path);
691 let original = cache.get(&path).map_or(0, |e| e.original_tokens);
692 let tokens = crate::core::tokens::count_tokens(&output);
693 drop(cache);
694 {
695 let mut session = self.session.write().await;
696 session.mark_modified(&path);
697 }
698 self.record_call(
699 "ctx_delta",
700 original,
701 original.saturating_sub(tokens),
702 Some("delta".to_string()),
703 )
704 .await;
705 output
706 }
707 "ctx_edit" => {
708 let path = match get_str(args, "path") {
709 Some(p) => self
710 .resolve_path(&p)
711 .await
712 .map_err(|e| ErrorData::invalid_params(e, None))?,
713 None => return Err(ErrorData::invalid_params("path is required", None)),
714 };
715 let old_string = get_str(args, "old_string").unwrap_or_default();
716 let new_string = get_str(args, "new_string")
717 .ok_or_else(|| ErrorData::invalid_params("new_string is required", None))?;
718 let replace_all = args
719 .as_ref()
720 .and_then(|a| a.get("replace_all"))
721 .and_then(|v| v.as_bool())
722 .unwrap_or(false);
723 let create = args
724 .as_ref()
725 .and_then(|a| a.get("create"))
726 .and_then(|v| v.as_bool())
727 .unwrap_or(false);
728
729 let mut cache = self.cache.write().await;
730 let output = crate::tools::ctx_edit::handle(
731 &mut cache,
732 crate::tools::ctx_edit::EditParams {
733 path: path.clone(),
734 old_string,
735 new_string,
736 replace_all,
737 create,
738 },
739 );
740 drop(cache);
741
742 {
743 let mut session = self.session.write().await;
744 session.mark_modified(&path);
745 }
746 self.record_call("ctx_edit", 0, 0, None).await;
747 output
748 }
749 "ctx_dedup" => {
750 let action = get_str(args, "action").unwrap_or_default();
751 if action == "apply" {
752 let mut cache = self.cache.write().await;
753 let result = crate::tools::ctx_dedup::handle_action(&mut cache, &action);
754 drop(cache);
755 self.record_call("ctx_dedup", 0, 0, None).await;
756 result
757 } else {
758 let cache = self.cache.read().await;
759 let result = crate::tools::ctx_dedup::handle(&cache);
760 drop(cache);
761 self.record_call("ctx_dedup", 0, 0, None).await;
762 result
763 }
764 }
765 "ctx_fill" => {
766 let raw_paths = get_str_array(args, "paths")
767 .ok_or_else(|| ErrorData::invalid_params("paths array is required", None))?;
768 let mut paths = Vec::with_capacity(raw_paths.len());
769 for p in raw_paths {
770 paths.push(
771 self.resolve_path(&p)
772 .await
773 .map_err(|e| ErrorData::invalid_params(e, None))?,
774 );
775 }
776 let budget = get_int(args, "budget")
777 .ok_or_else(|| ErrorData::invalid_params("budget is required", None))?
778 as usize;
779 let task = get_str(args, "task");
780 let mut cache = self.cache.write().await;
781 let output = crate::tools::ctx_fill::handle(
782 &mut cache,
783 &paths,
784 budget,
785 self.crp_mode,
786 task.as_deref(),
787 );
788 drop(cache);
789 self.record_call("ctx_fill", 0, 0, Some(format!("budget:{budget}")))
790 .await;
791 output
792 }
793 "ctx_intent" => {
794 let query = get_str(args, "query")
795 .ok_or_else(|| ErrorData::invalid_params("query is required", None))?;
796 let root = get_str(args, "project_root").unwrap_or_else(|| ".".to_string());
797 let mut cache = self.cache.write().await;
798 let output =
799 crate::tools::ctx_intent::handle(&mut cache, &query, &root, self.crp_mode);
800 drop(cache);
801 {
802 let mut session = self.session.write().await;
803 session.set_task(&query, Some("intent"));
804 }
805 self.record_call("ctx_intent", 0, 0, Some("semantic".to_string()))
806 .await;
807 output
808 }
809 "ctx_response" => {
810 let text = get_str(args, "text")
811 .ok_or_else(|| ErrorData::invalid_params("text is required", None))?;
812 let output = crate::tools::ctx_response::handle(&text, self.crp_mode);
813 self.record_call("ctx_response", 0, 0, None).await;
814 output
815 }
816 "ctx_context" => {
817 let cache = self.cache.read().await;
818 let turn = self.call_count.load(std::sync::atomic::Ordering::Relaxed);
819 let result = crate::tools::ctx_context::handle_status(&cache, turn, self.crp_mode);
820 drop(cache);
821 self.record_call("ctx_context", 0, 0, None).await;
822 result
823 }
824 "ctx_graph" => {
825 let action = get_str(args, "action")
826 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
827 let path = match get_str(args, "path") {
828 Some(p) => Some(
829 self.resolve_path(&p)
830 .await
831 .map_err(|e| ErrorData::invalid_params(e, None))?,
832 ),
833 None => None,
834 };
835 let root = self
836 .resolve_path(&get_str(args, "project_root").unwrap_or_else(|| ".".to_string()))
837 .await
838 .map_err(|e| ErrorData::invalid_params(e, None))?;
839 let crp_mode = self.crp_mode;
840 let action_for_record = action.clone();
841 let mut cache = self.cache.write().await;
842 let result = crate::tools::ctx_graph::handle(
843 &action,
844 path.as_deref(),
845 &root,
846 &mut cache,
847 crp_mode,
848 );
849 drop(cache);
850 self.record_call("ctx_graph", 0, 0, Some(action_for_record))
851 .await;
852 result
853 }
854 "ctx_cache" => {
855 let action = get_str(args, "action")
856 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
857 let mut cache = self.cache.write().await;
858 let result = match action.as_str() {
859 "status" => {
860 let entries = cache.get_all_entries();
861 if entries.is_empty() {
862 "Cache empty — no files tracked.".to_string()
863 } else {
864 let mut lines = vec![format!("Cache: {} file(s)", entries.len())];
865 for (path, entry) in &entries {
866 let fref = cache
867 .file_ref_map()
868 .get(*path)
869 .map(|s| s.as_str())
870 .unwrap_or("F?");
871 lines.push(format!(
872 " {fref}={} [{}L, {}t, read {}x]",
873 crate::core::protocol::shorten_path(path),
874 entry.line_count,
875 entry.original_tokens,
876 entry.read_count
877 ));
878 }
879 lines.join("\n")
880 }
881 }
882 "clear" => {
883 let count = cache.clear();
884 format!("Cache cleared — {count} file(s) removed. Next ctx_read will return full content.")
885 }
886 "invalidate" => {
887 let path = match get_str(args, "path") {
888 Some(p) => self
889 .resolve_path(&p)
890 .await
891 .map_err(|e| ErrorData::invalid_params(e, None))?,
892 None => {
893 return Err(ErrorData::invalid_params(
894 "path is required for invalidate",
895 None,
896 ))
897 }
898 };
899 if cache.invalidate(&path) {
900 format!(
901 "Invalidated cache for {}. Next ctx_read will return full content.",
902 crate::core::protocol::shorten_path(&path)
903 )
904 } else {
905 format!(
906 "{} was not in cache.",
907 crate::core::protocol::shorten_path(&path)
908 )
909 }
910 }
911 _ => "Unknown action. Use: status, clear, invalidate".to_string(),
912 };
913 drop(cache);
914 self.record_call("ctx_cache", 0, 0, Some(action)).await;
915 result
916 }
917 "ctx_session" => {
918 let action = get_str(args, "action")
919 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
920 let value = get_str(args, "value");
921 let sid = get_str(args, "session_id");
922 let mut session = self.session.write().await;
923 let result = crate::tools::ctx_session::handle(
924 &mut session,
925 &action,
926 value.as_deref(),
927 sid.as_deref(),
928 );
929 drop(session);
930 self.record_call("ctx_session", 0, 0, Some(action)).await;
931 result
932 }
933 "ctx_knowledge" => {
934 let action = get_str(args, "action")
935 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
936 let category = get_str(args, "category");
937 let key = get_str(args, "key");
938 let value = get_str(args, "value");
939 let query = get_str(args, "query");
940 let pattern_type = get_str(args, "pattern_type");
941 let examples = get_str_array(args, "examples");
942 let confidence: Option<f32> = args
943 .as_ref()
944 .and_then(|a| a.get("confidence"))
945 .and_then(|v| v.as_f64())
946 .map(|v| v as f32);
947
948 let session = self.session.read().await;
949 let session_id = session.id.clone();
950 let project_root = session.project_root.clone().unwrap_or_else(|| {
951 std::env::current_dir()
952 .map(|p| p.to_string_lossy().to_string())
953 .unwrap_or_else(|_| "unknown".to_string())
954 });
955 drop(session);
956
957 if action == "gotcha" {
958 let trigger = get_str(args, "trigger").unwrap_or_default();
959 let resolution = get_str(args, "resolution").unwrap_or_default();
960 let severity = get_str(args, "severity").unwrap_or_default();
961 let cat = category.as_deref().unwrap_or("convention");
962
963 if trigger.is_empty() || resolution.is_empty() {
964 self.record_call("ctx_knowledge", 0, 0, Some(action)).await;
965 return Ok(CallToolResult::success(vec![Content::text(
966 "ERROR: trigger and resolution are required for gotcha action",
967 )]));
968 }
969
970 let mut store = crate::core::gotcha_tracker::GotchaStore::load(&project_root);
971 let msg = match store.report_gotcha(
972 &trigger,
973 &resolution,
974 cat,
975 &severity,
976 &session_id,
977 ) {
978 Some(gotcha) => {
979 let conf = (gotcha.confidence * 100.0) as u32;
980 let label = gotcha.category.short_label();
981 format!("Gotcha recorded: [{label}] {trigger} (confidence: {conf}%)")
982 }
983 None => format!(
984 "Gotcha noted: {trigger} (evicted by higher-confidence entries)"
985 ),
986 };
987 let _ = store.save(&project_root);
988 self.record_call("ctx_knowledge", 0, 0, Some(action)).await;
989 return Ok(CallToolResult::success(vec![Content::text(msg)]));
990 }
991
992 let result = crate::tools::ctx_knowledge::handle(
993 &project_root,
994 &action,
995 category.as_deref(),
996 key.as_deref(),
997 value.as_deref(),
998 query.as_deref(),
999 &session_id,
1000 pattern_type.as_deref(),
1001 examples,
1002 confidence,
1003 );
1004 self.record_call("ctx_knowledge", 0, 0, Some(action)).await;
1005 result
1006 }
1007 "ctx_agent" => {
1008 let action = get_str(args, "action")
1009 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
1010 let agent_type = get_str(args, "agent_type");
1011 let role = get_str(args, "role");
1012 let message = get_str(args, "message");
1013 let category = get_str(args, "category");
1014 let to_agent = get_str(args, "to_agent");
1015 let status = get_str(args, "status");
1016
1017 let session = self.session.read().await;
1018 let project_root = session.project_root.clone().unwrap_or_else(|| {
1019 std::env::current_dir()
1020 .map(|p| p.to_string_lossy().to_string())
1021 .unwrap_or_else(|_| "unknown".to_string())
1022 });
1023 drop(session);
1024
1025 let current_agent_id = self.agent_id.read().await.clone();
1026 let result = crate::tools::ctx_agent::handle(
1027 &action,
1028 agent_type.as_deref(),
1029 role.as_deref(),
1030 &project_root,
1031 current_agent_id.as_deref(),
1032 message.as_deref(),
1033 category.as_deref(),
1034 to_agent.as_deref(),
1035 status.as_deref(),
1036 );
1037
1038 if action == "register" {
1039 if let Some(id) = result.split(':').nth(1) {
1040 let id = id.split_whitespace().next().unwrap_or("").to_string();
1041 if !id.is_empty() {
1042 *self.agent_id.write().await = Some(id);
1043 }
1044 }
1045 }
1046
1047 self.record_call("ctx_agent", 0, 0, Some(action)).await;
1048 result
1049 }
1050 "ctx_share" => {
1051 let action = get_str(args, "action")
1052 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
1053 let to_agent = get_str(args, "to_agent");
1054 let paths = get_str(args, "paths");
1055 let message = get_str(args, "message");
1056
1057 let from_agent = self.agent_id.read().await.clone();
1058 let cache = self.cache.read().await;
1059 let result = crate::tools::ctx_share::handle(
1060 &action,
1061 from_agent.as_deref(),
1062 to_agent.as_deref(),
1063 paths.as_deref(),
1064 message.as_deref(),
1065 &cache,
1066 );
1067 drop(cache);
1068
1069 self.record_call("ctx_share", 0, 0, Some(action)).await;
1070 result
1071 }
1072 "ctx_overview" => {
1073 let task = get_str(args, "task");
1074 let resolved_path = match get_str(args, "path") {
1075 Some(p) => Some(
1076 self.resolve_path(&p)
1077 .await
1078 .map_err(|e| ErrorData::invalid_params(e, None))?,
1079 ),
1080 None => {
1081 let session = self.session.read().await;
1082 session.project_root.clone()
1083 }
1084 };
1085 let cache = self.cache.read().await;
1086 let crp_mode = self.crp_mode;
1087 let result = crate::tools::ctx_overview::handle(
1088 &cache,
1089 task.as_deref(),
1090 resolved_path.as_deref(),
1091 crp_mode,
1092 );
1093 drop(cache);
1094 self.record_call("ctx_overview", 0, 0, Some("overview".to_string()))
1095 .await;
1096 result
1097 }
1098 "ctx_preload" => {
1099 let task = get_str(args, "task").unwrap_or_default();
1100 let resolved_path = match get_str(args, "path") {
1101 Some(p) => Some(
1102 self.resolve_path(&p)
1103 .await
1104 .map_err(|e| ErrorData::invalid_params(e, None))?,
1105 ),
1106 None => {
1107 let session = self.session.read().await;
1108 session.project_root.clone()
1109 }
1110 };
1111 let mut cache = self.cache.write().await;
1112 let result = crate::tools::ctx_preload::handle(
1113 &mut cache,
1114 &task,
1115 resolved_path.as_deref(),
1116 self.crp_mode,
1117 );
1118 drop(cache);
1119 self.record_call("ctx_preload", 0, 0, Some("preload".to_string()))
1120 .await;
1121 result
1122 }
1123 "ctx_prefetch" => {
1124 let root = match get_str(args, "root") {
1125 Some(r) => self
1126 .resolve_path(&r)
1127 .await
1128 .map_err(|e| ErrorData::invalid_params(e, None))?,
1129 None => {
1130 let session = self.session.read().await;
1131 session
1132 .project_root
1133 .clone()
1134 .unwrap_or_else(|| ".".to_string())
1135 }
1136 };
1137 let task = get_str(args, "task");
1138 let changed_files = get_str_array(args, "changed_files");
1139 let budget_tokens = get_int(args, "budget_tokens")
1140 .map(|n| n.max(0) as usize)
1141 .unwrap_or(3000);
1142 let max_files = get_int(args, "max_files").map(|n| n.max(1) as usize);
1143
1144 let mut resolved_changed: Option<Vec<String>> = None;
1145 if let Some(files) = changed_files {
1146 let mut v = Vec::with_capacity(files.len());
1147 for p in files {
1148 v.push(
1149 self.resolve_path(&p)
1150 .await
1151 .map_err(|e| ErrorData::invalid_params(e, None))?,
1152 );
1153 }
1154 resolved_changed = Some(v);
1155 }
1156
1157 let mut cache = self.cache.write().await;
1158 let result = crate::tools::ctx_prefetch::handle(
1159 &mut cache,
1160 &root,
1161 task.as_deref(),
1162 resolved_changed.as_deref(),
1163 budget_tokens,
1164 max_files,
1165 self.crp_mode,
1166 );
1167 drop(cache);
1168 self.record_call("ctx_prefetch", 0, 0, Some("prefetch".to_string()))
1169 .await;
1170 result
1171 }
1172 "ctx_wrapped" => {
1173 let period = get_str(args, "period").unwrap_or_else(|| "week".to_string());
1174 let result = crate::tools::ctx_wrapped::handle(&period);
1175 self.record_call("ctx_wrapped", 0, 0, Some(period)).await;
1176 result
1177 }
1178 "ctx_semantic_search" => {
1179 let query = get_str(args, "query")
1180 .ok_or_else(|| ErrorData::invalid_params("query is required", None))?;
1181 let path = self
1182 .resolve_path(&get_str(args, "path").unwrap_or_else(|| ".".to_string()))
1183 .await
1184 .map_err(|e| ErrorData::invalid_params(e, None))?;
1185 let top_k = get_int(args, "top_k").unwrap_or(10) as usize;
1186 let action = get_str(args, "action").unwrap_or_default();
1187 let mode = get_str(args, "mode");
1188 let languages = get_str_array(args, "languages");
1189 let path_glob = get_str(args, "path_glob");
1190 let result = if action == "reindex" {
1191 crate::tools::ctx_semantic_search::handle_reindex(&path)
1192 } else {
1193 crate::tools::ctx_semantic_search::handle(
1194 &query,
1195 &path,
1196 top_k,
1197 self.crp_mode,
1198 languages,
1199 path_glob.as_deref(),
1200 mode.as_deref(),
1201 )
1202 };
1203 self.record_call("ctx_semantic_search", 0, 0, Some("semantic".to_string()))
1204 .await;
1205 result
1206 }
1207 "ctx_execute" => {
1208 let action = get_str(args, "action").unwrap_or_default();
1209
1210 let result = if action == "batch" {
1211 let items_str = get_str(args, "items").ok_or_else(|| {
1212 ErrorData::invalid_params("items is required for batch", None)
1213 })?;
1214 let items: Vec<serde_json::Value> =
1215 serde_json::from_str(&items_str).map_err(|e| {
1216 ErrorData::invalid_params(format!("Invalid items JSON: {e}"), None)
1217 })?;
1218 let batch: Vec<(String, String)> = items
1219 .iter()
1220 .filter_map(|item| {
1221 let lang = item.get("language")?.as_str()?.to_string();
1222 let code = item.get("code")?.as_str()?.to_string();
1223 Some((lang, code))
1224 })
1225 .collect();
1226 crate::tools::ctx_execute::handle_batch(&batch)
1227 } else if action == "file" {
1228 let raw_path = get_str(args, "path").ok_or_else(|| {
1229 ErrorData::invalid_params("path is required for action=file", None)
1230 })?;
1231 let path = self.resolve_path(&raw_path).await.map_err(|e| {
1232 ErrorData::invalid_params(format!("path rejected: {e}"), None)
1233 })?;
1234 let intent = get_str(args, "intent");
1235 crate::tools::ctx_execute::handle_file(&path, intent.as_deref())
1236 } else {
1237 let language = get_str(args, "language")
1238 .ok_or_else(|| ErrorData::invalid_params("language is required", None))?;
1239 let code = get_str(args, "code")
1240 .ok_or_else(|| ErrorData::invalid_params("code is required", None))?;
1241 let intent = get_str(args, "intent");
1242 let timeout = get_int(args, "timeout").map(|t| t as u64);
1243 crate::tools::ctx_execute::handle(&language, &code, intent.as_deref(), timeout)
1244 };
1245
1246 self.record_call("ctx_execute", 0, 0, Some(action)).await;
1247 result
1248 }
1249 "ctx_symbol" => {
1250 let sym_name = get_str(args, "name")
1251 .ok_or_else(|| ErrorData::invalid_params("name is required", None))?;
1252 let file = get_str(args, "file");
1253 let kind = get_str(args, "kind");
1254 let session = self.session.read().await;
1255 let project_root = session
1256 .project_root
1257 .clone()
1258 .unwrap_or_else(|| ".".to_string());
1259 drop(session);
1260 let (result, original) = crate::tools::ctx_symbol::handle(
1261 &sym_name,
1262 file.as_deref(),
1263 kind.as_deref(),
1264 &project_root,
1265 );
1266 let sent = crate::core::tokens::count_tokens(&result);
1267 let saved = original.saturating_sub(sent);
1268 self.record_call("ctx_symbol", original, saved, kind).await;
1269 result
1270 }
1271 "ctx_graph_diagram" => {
1272 let file = get_str(args, "file");
1273 let depth = get_int(args, "depth").map(|d| d as usize);
1274 let kind = get_str(args, "kind");
1275 let session = self.session.read().await;
1276 let project_root = session
1277 .project_root
1278 .clone()
1279 .unwrap_or_else(|| ".".to_string());
1280 drop(session);
1281 let result = crate::tools::ctx_graph_diagram::handle(
1282 file.as_deref(),
1283 depth,
1284 kind.as_deref(),
1285 &project_root,
1286 );
1287 self.record_call("ctx_graph_diagram", 0, 0, kind).await;
1288 result
1289 }
1290 "ctx_routes" => {
1291 let method = get_str(args, "method");
1292 let path_prefix = get_str(args, "path");
1293 let session = self.session.read().await;
1294 let project_root = session
1295 .project_root
1296 .clone()
1297 .unwrap_or_else(|| ".".to_string());
1298 drop(session);
1299 let result = crate::tools::ctx_routes::handle(
1300 method.as_deref(),
1301 path_prefix.as_deref(),
1302 &project_root,
1303 );
1304 self.record_call("ctx_routes", 0, 0, None).await;
1305 result
1306 }
1307 "ctx_compress_memory" => {
1308 let path = self
1309 .resolve_path(
1310 &get_str(args, "path")
1311 .ok_or_else(|| ErrorData::invalid_params("path is required", None))?,
1312 )
1313 .await
1314 .map_err(|e| ErrorData::invalid_params(e, None))?;
1315 let result = crate::tools::ctx_compress_memory::handle(&path);
1316 self.record_call("ctx_compress_memory", 0, 0, None).await;
1317 result
1318 }
1319 "ctx_callers" => {
1320 let symbol = get_str(args, "symbol")
1321 .ok_or_else(|| ErrorData::invalid_params("symbol is required", None))?;
1322 let file = get_str(args, "file");
1323 let session = self.session.read().await;
1324 let project_root = session
1325 .project_root
1326 .clone()
1327 .unwrap_or_else(|| ".".to_string());
1328 drop(session);
1329 let result =
1330 crate::tools::ctx_callers::handle(&symbol, file.as_deref(), &project_root);
1331 self.record_call("ctx_callers", 0, 0, None).await;
1332 result
1333 }
1334 "ctx_callees" => {
1335 let symbol = get_str(args, "symbol")
1336 .ok_or_else(|| ErrorData::invalid_params("symbol is required", None))?;
1337 let file = get_str(args, "file");
1338 let session = self.session.read().await;
1339 let project_root = session
1340 .project_root
1341 .clone()
1342 .unwrap_or_else(|| ".".to_string());
1343 drop(session);
1344 let result =
1345 crate::tools::ctx_callees::handle(&symbol, file.as_deref(), &project_root);
1346 self.record_call("ctx_callees", 0, 0, None).await;
1347 result
1348 }
1349 "ctx_outline" => {
1350 let path = self
1351 .resolve_path(
1352 &get_str(args, "path")
1353 .ok_or_else(|| ErrorData::invalid_params("path is required", None))?,
1354 )
1355 .await
1356 .map_err(|e| ErrorData::invalid_params(e, None))?;
1357 let kind = get_str(args, "kind");
1358 let (result, original) = crate::tools::ctx_outline::handle(&path, kind.as_deref());
1359 let sent = crate::core::tokens::count_tokens(&result);
1360 let saved = original.saturating_sub(sent);
1361 self.record_call("ctx_outline", original, saved, kind).await;
1362 result
1363 }
1364 "ctx_cost" => {
1365 let action = get_str(args, "action").unwrap_or_else(|| "report".to_string());
1366 let agent_id = get_str(args, "agent_id");
1367 let limit = get_int(args, "limit").map(|n| n as usize);
1368 let result = crate::tools::ctx_cost::handle(&action, agent_id.as_deref(), limit);
1369 self.record_call("ctx_cost", 0, 0, Some(action)).await;
1370 result
1371 }
1372 "ctx_discover_tools" => {
1373 let query = get_str(args, "query").unwrap_or_default();
1374 let result = crate::tool_defs::discover_tools(&query);
1375 self.record_call("ctx_discover_tools", 0, 0, None).await;
1376 result
1377 }
1378 "ctx_gain" => {
1379 let action = get_str(args, "action").unwrap_or_else(|| "status".to_string());
1380 let period = get_str(args, "period");
1381 let model = get_str(args, "model");
1382 let limit = get_int(args, "limit").map(|n| n as usize);
1383 let result = crate::tools::ctx_gain::handle(
1384 &action,
1385 period.as_deref(),
1386 model.as_deref(),
1387 limit,
1388 );
1389 self.record_call("ctx_gain", 0, 0, Some(action)).await;
1390 result
1391 }
1392 "ctx_feedback" => {
1393 let action = get_str(args, "action").unwrap_or_else(|| "report".to_string());
1394 let limit = get_int(args, "limit")
1395 .map(|n| n.max(1) as usize)
1396 .unwrap_or(500);
1397 match action.as_str() {
1398 "record" => {
1399 let current_agent_id = { self.agent_id.read().await.clone() };
1400 let agent_id = get_str(args, "agent_id").or(current_agent_id);
1401 let agent_id = agent_id.ok_or_else(|| {
1402 ErrorData::invalid_params(
1403 "agent_id is required (or register an agent via project_root detection first)",
1404 None,
1405 )
1406 })?;
1407
1408 let (ctx_read_last_mode, ctx_read_modes) = {
1409 let calls = self.tool_calls.read().await;
1410 let mut last: Option<String> = None;
1411 let mut modes: std::collections::BTreeMap<String, u64> =
1412 std::collections::BTreeMap::new();
1413 for rec in calls.iter().rev().take(50) {
1414 if rec.tool != "ctx_read" {
1415 continue;
1416 }
1417 if let Some(m) = rec.mode.as_ref() {
1418 *modes.entry(m.clone()).or_insert(0) += 1;
1419 if last.is_none() {
1420 last = Some(m.clone());
1421 }
1422 }
1423 }
1424 (last, if modes.is_empty() { None } else { Some(modes) })
1425 };
1426
1427 let llm_input_tokens =
1428 get_int(args, "llm_input_tokens").ok_or_else(|| {
1429 ErrorData::invalid_params("llm_input_tokens is required", None)
1430 })?;
1431 let llm_output_tokens =
1432 get_int(args, "llm_output_tokens").ok_or_else(|| {
1433 ErrorData::invalid_params("llm_output_tokens is required", None)
1434 })?;
1435 if llm_input_tokens <= 0 || llm_output_tokens <= 0 {
1436 return Err(ErrorData::invalid_params(
1437 "llm_input_tokens and llm_output_tokens must be > 0",
1438 None,
1439 ));
1440 }
1441
1442 let ev = crate::core::llm_feedback::LlmFeedbackEvent {
1443 agent_id,
1444 intent: get_str(args, "intent"),
1445 model: get_str(args, "model"),
1446 llm_input_tokens: llm_input_tokens as u64,
1447 llm_output_tokens: llm_output_tokens as u64,
1448 latency_ms: get_int(args, "latency_ms").map(|n| n.max(0) as u64),
1449 note: get_str(args, "note"),
1450 ctx_read_last_mode,
1451 ctx_read_modes,
1452 timestamp: chrono::Local::now().to_rfc3339(),
1453 };
1454 let result = crate::tools::ctx_feedback::record(ev)
1455 .unwrap_or_else(|e| format!("Error recording feedback: {e}"));
1456 self.record_call("ctx_feedback", 0, 0, Some(action)).await;
1457 result
1458 }
1459 "status" => {
1460 let result = crate::tools::ctx_feedback::status();
1461 self.record_call("ctx_feedback", 0, 0, Some(action)).await;
1462 result
1463 }
1464 "json" => {
1465 let result = crate::tools::ctx_feedback::json(limit);
1466 self.record_call("ctx_feedback", 0, 0, Some(action)).await;
1467 result
1468 }
1469 "reset" => {
1470 let result = crate::tools::ctx_feedback::reset();
1471 self.record_call("ctx_feedback", 0, 0, Some(action)).await;
1472 result
1473 }
1474 _ => {
1475 let result = crate::tools::ctx_feedback::report(limit);
1476 self.record_call("ctx_feedback", 0, 0, Some(action)).await;
1477 result
1478 }
1479 }
1480 }
1481 "ctx_handoff" => {
1482 let action = get_str(args, "action").unwrap_or_else(|| "list".to_string());
1483 match action.as_str() {
1484 "list" => {
1485 let items = crate::core::handoff_ledger::list_ledgers();
1486 let result = crate::tools::ctx_handoff::format_list(&items);
1487 self.record_call("ctx_handoff", 0, 0, Some(action)).await;
1488 result
1489 }
1490 "clear" => {
1491 let removed =
1492 crate::core::handoff_ledger::clear_ledgers().unwrap_or_default();
1493 let result = crate::tools::ctx_handoff::format_clear(removed);
1494 self.record_call("ctx_handoff", 0, 0, Some(action)).await;
1495 result
1496 }
1497 "show" => {
1498 let path = get_str(args, "path").ok_or_else(|| {
1499 ErrorData::invalid_params("path is required for action=show", None)
1500 })?;
1501 let path = self
1502 .resolve_path(&path)
1503 .await
1504 .map_err(|e| ErrorData::invalid_params(e, None))?;
1505 let ledger =
1506 crate::core::handoff_ledger::load_ledger(std::path::Path::new(&path))
1507 .map_err(|e| {
1508 ErrorData::internal_error(format!("load ledger: {e}"), None)
1509 })?;
1510 let result = crate::tools::ctx_handoff::format_show(
1511 std::path::Path::new(&path),
1512 &ledger,
1513 );
1514 self.record_call("ctx_handoff", 0, 0, Some(action)).await;
1515 result
1516 }
1517 "create" => {
1518 let curated_paths = get_str_array(args, "paths").unwrap_or_default();
1519 let mut curated_refs: Vec<(String, String)> = Vec::new();
1520 if !curated_paths.is_empty() {
1521 let mut cache = self.cache.write().await;
1522 for p in curated_paths.into_iter().take(20) {
1523 let abs = self
1524 .resolve_path(&p)
1525 .await
1526 .map_err(|e| ErrorData::invalid_params(e, None))?;
1527 let text = crate::tools::ctx_read::handle_with_task(
1528 &mut cache,
1529 &abs,
1530 "signatures",
1531 self.crp_mode,
1532 None,
1533 );
1534 curated_refs.push((abs, text));
1535 }
1536 }
1537
1538 let session = { self.session.read().await.clone() };
1539 let tool_calls = { self.tool_calls.read().await.clone() };
1540 let workflow = { self.workflow.read().await.clone() };
1541 let agent_id = { self.agent_id.read().await.clone() };
1542 let client_name = { self.client_name.read().await.clone() };
1543 let project_root = session.project_root.clone();
1544
1545 let (ledger, path) = crate::core::handoff_ledger::create_ledger(
1546 crate::core::handoff_ledger::CreateLedgerInput {
1547 agent_id,
1548 client_name: Some(client_name),
1549 project_root,
1550 session,
1551 tool_calls,
1552 workflow,
1553 curated_refs,
1554 },
1555 )
1556 .map_err(|e| {
1557 ErrorData::internal_error(format!("create ledger: {e}"), None)
1558 })?;
1559
1560 let result = crate::tools::ctx_handoff::format_created(&path, &ledger);
1561 self.record_call("ctx_handoff", 0, 0, Some(action)).await;
1562 result
1563 }
1564 "pull" => {
1565 let path = get_str(args, "path").ok_or_else(|| {
1566 ErrorData::invalid_params("path is required for action=pull", None)
1567 })?;
1568 let path = self
1569 .resolve_path(&path)
1570 .await
1571 .map_err(|e| ErrorData::invalid_params(e, None))?;
1572 let ledger =
1573 crate::core::handoff_ledger::load_ledger(std::path::Path::new(&path))
1574 .map_err(|e| {
1575 ErrorData::internal_error(format!("load ledger: {e}"), None)
1576 })?;
1577
1578 let apply_workflow = get_bool(args, "apply_workflow").unwrap_or(true);
1579 let apply_session = get_bool(args, "apply_session").unwrap_or(true);
1580 let apply_knowledge = get_bool(args, "apply_knowledge").unwrap_or(true);
1581
1582 if apply_workflow {
1583 let mut wf = self.workflow.write().await;
1584 *wf = ledger.workflow.clone();
1585 }
1586
1587 if apply_session {
1588 let mut session = self.session.write().await;
1589 if let Some(t) = ledger.session.task.as_deref() {
1590 session.set_task(t, None);
1591 }
1592 for d in &ledger.session.decisions {
1593 session.add_decision(d, None);
1594 }
1595 for f in &ledger.session.findings {
1596 session.add_finding(None, None, f);
1597 }
1598 session.next_steps = ledger.session.next_steps.clone();
1599 let _ = session.save();
1600 }
1601
1602 let mut knowledge_imported = 0u32;
1603 let mut contradictions = 0u32;
1604 if apply_knowledge {
1605 let root = if let Some(r) = ledger.project_root.as_deref() {
1606 r.to_string()
1607 } else {
1608 let session = self.session.read().await;
1609 session
1610 .project_root
1611 .clone()
1612 .unwrap_or_else(|| ".".to_string())
1613 };
1614 let session_id = {
1615 let s = self.session.read().await;
1616 s.id.clone()
1617 };
1618 let mut knowledge =
1619 crate::core::knowledge::ProjectKnowledge::load_or_create(&root);
1620 for fact in &ledger.knowledge.facts {
1621 let c = knowledge.remember(
1622 &fact.category,
1623 &fact.key,
1624 &fact.value,
1625 &session_id,
1626 fact.confidence,
1627 );
1628 if c.is_some() {
1629 contradictions += 1;
1630 }
1631 knowledge_imported += 1;
1632 }
1633 let _ = knowledge.run_memory_lifecycle();
1634 let _ = knowledge.save();
1635 }
1636
1637 let lines = [
1638 "ctx_handoff pull".to_string(),
1639 format!(" path: {}", path),
1640 format!(" md5: {}", ledger.content_md5),
1641 format!(" applied_workflow: {}", apply_workflow),
1642 format!(" applied_session: {}", apply_session),
1643 format!(" imported_knowledge: {}", knowledge_imported),
1644 format!(" contradictions: {}", contradictions),
1645 ];
1646 let result = lines.join("\n");
1647 self.record_call("ctx_handoff", 0, 0, Some(action)).await;
1648 result
1649 }
1650 _ => {
1651 let result =
1652 "Unknown action. Use: create, show, list, pull, clear".to_string();
1653 self.record_call("ctx_handoff", 0, 0, Some(action)).await;
1654 result
1655 }
1656 }
1657 }
1658 "ctx_heatmap" => {
1659 let action = get_str(args, "action").unwrap_or_else(|| "status".to_string());
1660 let path = get_str(args, "path");
1661 let result = crate::tools::ctx_heatmap::handle(&action, path.as_deref());
1662 self.record_call("ctx_heatmap", 0, 0, Some(action)).await;
1663 result
1664 }
1665 "ctx_task" => {
1666 let action = get_str(args, "action").unwrap_or_else(|| "list".to_string());
1667 let current_agent_id = { self.agent_id.read().await.clone() };
1668 let task_id = get_str(args, "task_id");
1669 let to_agent = get_str(args, "to_agent");
1670 let description = get_str(args, "description");
1671 let state = get_str(args, "state");
1672 let message = get_str(args, "message");
1673 let result = crate::tools::ctx_task::handle(
1674 &action,
1675 current_agent_id.as_deref(),
1676 task_id.as_deref(),
1677 to_agent.as_deref(),
1678 description.as_deref(),
1679 state.as_deref(),
1680 message.as_deref(),
1681 );
1682 self.record_call("ctx_task", 0, 0, Some(action)).await;
1683 result
1684 }
1685 "ctx_impact" => {
1686 let action = get_str(args, "action").unwrap_or_else(|| "analyze".to_string());
1687 let path = get_str(args, "path");
1688 let depth = get_int(args, "depth").map(|d| d as usize);
1689 let root = if let Some(r) = get_str(args, "root") {
1690 r
1691 } else {
1692 let session = self.session.read().await;
1693 session
1694 .project_root
1695 .clone()
1696 .unwrap_or_else(|| ".".to_string())
1697 };
1698 let result =
1699 crate::tools::ctx_impact::handle(&action, path.as_deref(), &root, depth);
1700 self.record_call("ctx_impact", 0, 0, Some(action)).await;
1701 result
1702 }
1703 "ctx_architecture" => {
1704 let action = get_str(args, "action").unwrap_or_else(|| "overview".to_string());
1705 let path = get_str(args, "path");
1706 let root = if let Some(r) = get_str(args, "root") {
1707 r
1708 } else {
1709 let session = self.session.read().await;
1710 session
1711 .project_root
1712 .clone()
1713 .unwrap_or_else(|| ".".to_string())
1714 };
1715 let result =
1716 crate::tools::ctx_architecture::handle(&action, path.as_deref(), &root);
1717 self.record_call("ctx_architecture", 0, 0, Some(action))
1718 .await;
1719 result
1720 }
1721 "ctx_workflow" => {
1722 let action = get_str(args, "action").unwrap_or_else(|| "status".to_string());
1723 let result = {
1724 let mut session = self.session.write().await;
1725 crate::tools::ctx_workflow::handle_with_session(args, &mut session)
1726 };
1727 *self.workflow.write().await = crate::core::workflow::load_active().ok().flatten();
1728 self.record_call("ctx_workflow", 0, 0, Some(action)).await;
1729 result
1730 }
1731 _ => {
1732 return Err(ErrorData::invalid_params(
1733 format!("Unknown tool: {name}"),
1734 None,
1735 ));
1736 }
1737 };
1738
1739 let mut result_text = result_text;
1740
1741 {
1742 let config = crate::core::config::Config::load();
1743 let density = crate::core::config::OutputDensity::effective(&config.output_density);
1744 result_text = crate::core::protocol::compress_output(&result_text, &density);
1745 }
1746
1747 if let Some(ctx) = auto_context {
1748 result_text = format!("{ctx}\n\n{result_text}");
1749 }
1750
1751 if let Some(warning) = throttle_warning {
1752 result_text = format!("{result_text}\n\n{warning}");
1753 }
1754
1755 if name == "ctx_read" {
1756 let read_path = self
1757 .resolve_path_or_passthrough(&get_str(args, "path").unwrap_or_default())
1758 .await;
1759 let project_root = {
1760 let session = self.session.read().await;
1761 session.project_root.clone()
1762 };
1763 let mut cache = self.cache.write().await;
1764 let enrich = crate::tools::autonomy::enrich_after_read(
1765 &self.autonomy,
1766 &mut cache,
1767 &read_path,
1768 project_root.as_deref(),
1769 );
1770 if let Some(hint) = enrich.related_hint {
1771 result_text = format!("{result_text}\n{hint}");
1772 }
1773
1774 crate::tools::autonomy::maybe_auto_dedup(&self.autonomy, &mut cache);
1775 }
1776
1777 if name == "ctx_shell" {
1778 let cmd = get_str(args, "command").unwrap_or_default();
1779 let output_tokens = crate::core::tokens::count_tokens(&result_text);
1780 let calls = self.tool_calls.read().await;
1781 let last_original = calls.last().map(|c| c.original_tokens).unwrap_or(0);
1782 drop(calls);
1783 if let Some(hint) = crate::tools::autonomy::shell_efficiency_hint(
1784 &self.autonomy,
1785 &cmd,
1786 last_original,
1787 output_tokens,
1788 ) {
1789 result_text = format!("{result_text}\n{hint}");
1790 }
1791 }
1792
1793 {
1794 let input = canonical_args_string(args);
1795 let input_md5 = md5_hex(&input);
1796 let output_md5 = md5_hex(&result_text);
1797 let action = get_str(args, "action");
1798 let agent_id = self.agent_id.read().await.clone();
1799 let client_name = self.client_name.read().await.clone();
1800 let mut explicit_intent: Option<(
1801 crate::core::intent_protocol::IntentRecord,
1802 Option<String>,
1803 String,
1804 )> = None;
1805
1806 {
1807 let empty_args = serde_json::Map::new();
1808 let args_map = args.as_ref().unwrap_or(&empty_args);
1809 let mut session = self.session.write().await;
1810 session.record_tool_receipt(
1811 name,
1812 action.as_deref(),
1813 &input_md5,
1814 &output_md5,
1815 agent_id.as_deref(),
1816 Some(&client_name),
1817 );
1818
1819 if let Some(intent) = crate::core::intent_protocol::infer_from_tool_call(
1820 name,
1821 action.as_deref(),
1822 args_map,
1823 session.project_root.as_deref(),
1824 ) {
1825 let is_explicit =
1826 intent.source == crate::core::intent_protocol::IntentSource::Explicit;
1827 let root = session.project_root.clone();
1828 let sid = session.id.clone();
1829 session.record_intent(intent.clone());
1830 if is_explicit {
1831 explicit_intent = Some((intent, root, sid));
1832 }
1833 }
1834 if session.should_save() {
1835 let _ = session.save();
1836 }
1837 }
1838
1839 if let Some((intent, root, session_id)) = explicit_intent {
1840 crate::core::intent_protocol::apply_side_effects(
1841 &intent,
1842 root.as_deref(),
1843 &session_id,
1844 );
1845 }
1846
1847 if self.autonomy.is_enabled() {
1849 let (calls, project_root) = {
1850 let session = self.session.read().await;
1851 (session.stats.total_tool_calls, session.project_root.clone())
1852 };
1853
1854 if let Some(root) = project_root {
1855 if crate::tools::autonomy::should_auto_consolidate(&self.autonomy, calls) {
1856 let root_clone = root.clone();
1857 tokio::task::spawn_blocking(move || {
1858 let _ = crate::core::consolidation_engine::consolidate_latest(
1859 &root_clone,
1860 crate::core::consolidation_engine::ConsolidationBudgets::default(),
1861 );
1862 });
1863 }
1864 }
1865 }
1866
1867 let agent_key = agent_id.unwrap_or_else(|| "unknown".to_string());
1868 let input_tokens = crate::core::tokens::count_tokens(&input) as u64;
1869 let output_tokens = crate::core::tokens::count_tokens(&result_text) as u64;
1870 let mut store = crate::core::a2a::cost_attribution::CostStore::load();
1871 store.record_tool_call(&agent_key, &client_name, name, input_tokens, output_tokens);
1872 let _ = store.save();
1873 }
1874
1875 let skip_checkpoint = matches!(
1876 name,
1877 "ctx_compress"
1878 | "ctx_metrics"
1879 | "ctx_benchmark"
1880 | "ctx_analyze"
1881 | "ctx_cache"
1882 | "ctx_discover"
1883 | "ctx_dedup"
1884 | "ctx_session"
1885 | "ctx_knowledge"
1886 | "ctx_agent"
1887 | "ctx_share"
1888 | "ctx_wrapped"
1889 | "ctx_overview"
1890 | "ctx_preload"
1891 | "ctx_cost"
1892 | "ctx_gain"
1893 | "ctx_heatmap"
1894 | "ctx_task"
1895 | "ctx_impact"
1896 | "ctx_architecture"
1897 | "ctx_workflow"
1898 );
1899
1900 if !skip_checkpoint && self.increment_and_check() {
1901 if let Some(checkpoint) = self.auto_checkpoint().await {
1902 let combined = format!(
1903 "{result_text}\n\n--- AUTO CHECKPOINT (every {} calls) ---\n{checkpoint}",
1904 self.checkpoint_interval
1905 );
1906 return Ok(CallToolResult::success(vec![Content::text(combined)]));
1907 }
1908 }
1909
1910 let tool_duration_ms = tool_start.elapsed().as_millis() as u64;
1911 if tool_duration_ms > 100 {
1912 LeanCtxServer::append_tool_call_log(
1913 name,
1914 tool_duration_ms,
1915 0,
1916 0,
1917 None,
1918 &chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
1919 );
1920 }
1921
1922 let current_count = self.call_count.load(std::sync::atomic::Ordering::Relaxed);
1923 if current_count > 0 && current_count.is_multiple_of(100) {
1924 std::thread::spawn(crate::cloud_sync::cloud_background_tasks);
1925 }
1926
1927 Ok(CallToolResult::success(vec![Content::text(result_text)]))
1928 }
1929}
1930
1931pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
1932 crate::instructions::build_instructions(crp_mode)
1933}
1934
1935pub fn build_claude_code_instructions_for_test() -> String {
1936 crate::instructions::claude_code_instructions()
1937}
1938
1939fn get_str_array(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<Vec<String>> {
1940 let arr = args.as_ref()?.get(key)?.as_array()?;
1941 let mut out = Vec::with_capacity(arr.len());
1942 for v in arr {
1943 let s = v.as_str()?.to_string();
1944 out.push(s);
1945 }
1946 Some(out)
1947}
1948
1949fn get_str(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<String> {
1950 args.as_ref()?.get(key)?.as_str().map(|s| s.to_string())
1951}
1952
1953fn get_int(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<i64> {
1954 args.as_ref()?.get(key)?.as_i64()
1955}
1956
1957fn get_bool(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<bool> {
1958 args.as_ref()?.get(key)?.as_bool()
1959}
1960
1961fn md5_hex(s: &str) -> String {
1962 let mut hasher = Md5::new();
1963 hasher.update(s.as_bytes());
1964 format!("{:x}", hasher.finalize())
1965}
1966
1967fn canonicalize_json(v: &Value) -> Value {
1968 match v {
1969 Value::Object(map) => {
1970 let mut keys: Vec<&String> = map.keys().collect();
1971 keys.sort();
1972 let mut out = serde_json::Map::new();
1973 for k in keys {
1974 if let Some(val) = map.get(k) {
1975 out.insert(k.clone(), canonicalize_json(val));
1976 }
1977 }
1978 Value::Object(out)
1979 }
1980 Value::Array(arr) => Value::Array(arr.iter().map(canonicalize_json).collect()),
1981 other => other.clone(),
1982 }
1983}
1984
1985fn canonical_args_string(args: &Option<serde_json::Map<String, Value>>) -> String {
1986 let v = args
1987 .as_ref()
1988 .map(|m| Value::Object(m.clone()))
1989 .unwrap_or(Value::Null);
1990 let canon = canonicalize_json(&v);
1991 serde_json::to_string(&canon).unwrap_or_default()
1992}
1993
1994fn extract_search_pattern_from_command(command: &str) -> Option<String> {
1995 let parts: Vec<&str> = command.split_whitespace().collect();
1996 if parts.len() < 2 {
1997 return None;
1998 }
1999 let cmd = parts[0];
2000 if cmd == "grep" || cmd == "rg" || cmd == "ag" || cmd == "ack" {
2001 for (i, part) in parts.iter().enumerate().skip(1) {
2002 if !part.starts_with('-') {
2003 return Some(part.to_string());
2004 }
2005 if (*part == "-e" || *part == "--regexp" || *part == "-m") && i + 1 < parts.len() {
2006 return Some(parts[i + 1].to_string());
2007 }
2008 }
2009 }
2010 if cmd == "find" || cmd == "fd" {
2011 for (i, part) in parts.iter().enumerate() {
2012 if (*part == "-name" || *part == "-iname") && i + 1 < parts.len() {
2013 return Some(
2014 parts[i + 1]
2015 .trim_matches('\'')
2016 .trim_matches('"')
2017 .to_string(),
2018 );
2019 }
2020 }
2021 if cmd == "fd" && parts.len() >= 2 && !parts[1].starts_with('-') {
2022 return Some(parts[1].to_string());
2023 }
2024 }
2025 None
2026}
2027
2028fn execute_command_in(command: &str, cwd: &str) -> (String, i32) {
2029 let (shell, flag) = crate::shell::shell_and_flag();
2030 let normalized_cmd = crate::tools::ctx_shell::normalize_command_for_shell(command);
2031 let dir = std::path::Path::new(cwd);
2032 let mut cmd = std::process::Command::new(&shell);
2033 cmd.arg(&flag)
2034 .arg(&normalized_cmd)
2035 .env("LEAN_CTX_ACTIVE", "1");
2036 if dir.is_dir() {
2037 cmd.current_dir(dir);
2038 }
2039 let cap = crate::core::limits::max_shell_bytes();
2040
2041 fn read_bounded<R: std::io::Read>(mut r: R, cap: usize) -> (Vec<u8>, bool, usize) {
2042 let mut kept: Vec<u8> = Vec::with_capacity(cap.min(8192));
2043 let mut buf = [0u8; 8192];
2044 let mut total = 0usize;
2045 let mut truncated = false;
2046 loop {
2047 match r.read(&mut buf) {
2048 Ok(0) => break,
2049 Ok(n) => {
2050 total = total.saturating_add(n);
2051 if kept.len() < cap {
2052 let remaining = cap - kept.len();
2053 let take = remaining.min(n);
2054 kept.extend_from_slice(&buf[..take]);
2055 if take < n {
2056 truncated = true;
2057 }
2058 } else {
2059 truncated = true;
2060 }
2061 }
2062 Err(_) => break,
2063 }
2064 }
2065 (kept, truncated, total)
2066 }
2067
2068 let mut child = match cmd
2069 .stdout(std::process::Stdio::piped())
2070 .stderr(std::process::Stdio::piped())
2071 .spawn()
2072 {
2073 Ok(c) => c,
2074 Err(e) => return (format!("ERROR: {e}"), 1),
2075 };
2076 let stdout = child.stdout.take();
2077 let stderr = child.stderr.take();
2078
2079 let out_handle = std::thread::spawn(move || {
2080 stdout
2081 .map(|s| read_bounded(s, cap))
2082 .unwrap_or_else(|| (Vec::new(), false, 0))
2083 });
2084 let err_handle = std::thread::spawn(move || {
2085 stderr
2086 .map(|s| read_bounded(s, cap))
2087 .unwrap_or_else(|| (Vec::new(), false, 0))
2088 });
2089
2090 let status = child.wait();
2091 let code = status.ok().and_then(|s| s.code()).unwrap_or(1);
2092
2093 let (out_bytes, out_trunc, _out_total) = out_handle.join().unwrap_or_default();
2094 let (err_bytes, err_trunc, _err_total) = err_handle.join().unwrap_or_default();
2095
2096 let stdout = String::from_utf8_lossy(&out_bytes);
2097 let stderr = String::from_utf8_lossy(&err_bytes);
2098 let mut text = if stdout.is_empty() {
2099 stderr.to_string()
2100 } else if stderr.is_empty() {
2101 stdout.to_string()
2102 } else {
2103 format!("{stdout}\n{stderr}")
2104 };
2105
2106 if out_trunc || err_trunc {
2107 text.push_str(&format!(
2108 "\n[truncated: cap={}B stdout={}B stderr={}B]",
2109 cap,
2110 out_bytes.len(),
2111 err_bytes.len()
2112 ));
2113 }
2114
2115 (text, code)
2116}
2117
2118pub fn tool_descriptions_for_test() -> Vec<(&'static str, &'static str)> {
2119 crate::tool_defs::list_all_tool_defs()
2120 .into_iter()
2121 .map(|(name, desc, _)| (name, desc))
2122 .collect()
2123}
2124
2125pub fn tool_schemas_json_for_test() -> String {
2126 crate::tool_defs::list_all_tool_defs()
2127 .iter()
2128 .map(|(name, _, schema)| format!("{}: {}", name, schema))
2129 .collect::<Vec<_>>()
2130 .join("\n")
2131}
2132
2133#[cfg(test)]
2134mod tests {
2135 #[test]
2136 fn test_unified_tool_count() {
2137 let tools = crate::tool_defs::unified_tool_defs();
2138 assert_eq!(tools.len(), 5, "Expected 5 unified tools");
2139 }
2140
2141 #[test]
2142 fn test_granular_tool_count() {
2143 let tools = crate::tool_defs::granular_tool_defs();
2144 assert!(tools.len() >= 25, "Expected at least 25 granular tools");
2145 }
2146
2147 #[test]
2148 fn disabled_tools_filters_list() {
2149 let all = crate::tool_defs::granular_tool_defs();
2150 let total = all.len();
2151 let disabled = ["ctx_graph".to_string(), "ctx_agent".to_string()];
2152 let filtered: Vec<_> = all
2153 .into_iter()
2154 .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
2155 .collect();
2156 assert_eq!(filtered.len(), total - 2);
2157 assert!(!filtered.iter().any(|t| t.name.as_ref() == "ctx_graph"));
2158 assert!(!filtered.iter().any(|t| t.name.as_ref() == "ctx_agent"));
2159 }
2160
2161 #[test]
2162 fn empty_disabled_tools_returns_all() {
2163 let all = crate::tool_defs::granular_tool_defs();
2164 let total = all.len();
2165 let disabled: Vec<String> = vec![];
2166 let filtered: Vec<_> = all
2167 .into_iter()
2168 .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
2169 .collect();
2170 assert_eq!(filtered.len(), total);
2171 }
2172
2173 #[test]
2174 fn misspelled_disabled_tool_is_silently_ignored() {
2175 let all = crate::tool_defs::granular_tool_defs();
2176 let total = all.len();
2177 let disabled = ["ctx_nonexistent_tool".to_string()];
2178 let filtered: Vec<_> = all
2179 .into_iter()
2180 .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
2181 .collect();
2182 assert_eq!(filtered.len(), total);
2183 }
2184}