Skip to main content

lean_ctx/
server.rs

1use rmcp::handler::server::ServerHandler;
2use rmcp::model::*;
3use rmcp::service::{RequestContext, RoleServer};
4use rmcp::ErrorData;
5use serde_json::Value;
6
7use crate::tools::{CrpMode, LeanCtxServer};
8
9impl ServerHandler for LeanCtxServer {
10    fn get_info(&self) -> ServerInfo {
11        let capabilities = ServerCapabilities::builder().enable_tools().build();
12
13        let instructions = crate::instructions::build_instructions(self.crp_mode);
14
15        InitializeResult::new(capabilities)
16            .with_server_info(Implementation::new("lean-ctx", env!("CARGO_PKG_VERSION")))
17            .with_instructions(instructions)
18    }
19
20    async fn initialize(
21        &self,
22        request: InitializeRequestParams,
23        _context: RequestContext<RoleServer>,
24    ) -> Result<InitializeResult, ErrorData> {
25        let name = request.client_info.name.clone();
26        tracing::info!("MCP client connected: {:?}", name);
27        *self.client_name.write().await = name.clone();
28
29        tokio::task::spawn_blocking(|| {
30            if let Some(home) = dirs::home_dir() {
31                let _ = crate::rules_inject::inject_all_rules(&home);
32            }
33            crate::hooks::refresh_installed_hooks();
34            crate::core::version_check::check_background();
35        });
36
37        let instructions =
38            crate::instructions::build_instructions_with_client(self.crp_mode, &name);
39        let capabilities = ServerCapabilities::builder().enable_tools().build();
40
41        Ok(InitializeResult::new(capabilities)
42            .with_server_info(Implementation::new("lean-ctx", env!("CARGO_PKG_VERSION")))
43            .with_instructions(instructions))
44    }
45
46    async fn list_tools(
47        &self,
48        _request: Option<PaginatedRequestParams>,
49        _context: RequestContext<RoleServer>,
50    ) -> Result<ListToolsResult, ErrorData> {
51        let all_tools = if std::env::var("LEAN_CTX_UNIFIED").is_ok()
52            && std::env::var("LEAN_CTX_FULL_TOOLS").is_err()
53        {
54            crate::tool_defs::unified_tool_defs()
55        } else {
56            crate::tool_defs::granular_tool_defs()
57        };
58
59        let disabled = crate::core::config::Config::load().disabled_tools_effective();
60        let tools = if disabled.is_empty() {
61            all_tools
62        } else {
63            all_tools
64                .into_iter()
65                .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
66                .collect()
67        };
68
69        Ok(ListToolsResult {
70            tools,
71            ..Default::default()
72        })
73    }
74
75    async fn call_tool(
76        &self,
77        request: CallToolRequestParams,
78        _context: RequestContext<RoleServer>,
79    ) -> Result<CallToolResult, ErrorData> {
80        self.check_idle_expiry().await;
81
82        let original_name = request.name.as_ref().to_string();
83        let (resolved_name, resolved_args) = if original_name == "ctx" {
84            let sub = request
85                .arguments
86                .as_ref()
87                .and_then(|a| a.get("tool"))
88                .and_then(|v| v.as_str())
89                .map(|s| s.to_string())
90                .ok_or_else(|| {
91                    ErrorData::invalid_params("'tool' is required for ctx meta-tool", None)
92                })?;
93            let tool_name = if sub.starts_with("ctx_") {
94                sub
95            } else {
96                format!("ctx_{sub}")
97            };
98            let mut args = request.arguments.unwrap_or_default();
99            args.remove("tool");
100            (tool_name, Some(args))
101        } else {
102            (original_name, request.arguments)
103        };
104        let name = resolved_name.as_str();
105        let args = &resolved_args;
106
107        let auto_context = {
108            let task = {
109                let session = self.session.read().await;
110                session.task.as_ref().map(|t| t.description.clone())
111            };
112            let project_root = {
113                let session = self.session.read().await;
114                session.project_root.clone()
115            };
116            let mut cache = self.cache.write().await;
117            crate::tools::autonomy::session_lifecycle_pre_hook(
118                &self.autonomy,
119                name,
120                &mut cache,
121                task.as_deref(),
122                project_root.as_deref(),
123                self.crp_mode,
124            )
125        };
126
127        let throttle_result = {
128            let fp = args
129                .as_ref()
130                .map(|a| {
131                    crate::core::loop_detection::LoopDetector::fingerprint(
132                        &serde_json::Value::Object(a.clone()),
133                    )
134                })
135                .unwrap_or_default();
136            let mut detector = self.loop_detector.write().await;
137
138            let is_search = crate::core::loop_detection::LoopDetector::is_search_tool(name);
139            let is_search_shell = name == "ctx_shell" && {
140                let cmd = args
141                    .as_ref()
142                    .and_then(|a| a.get("command"))
143                    .and_then(|v| v.as_str())
144                    .unwrap_or("");
145                crate::core::loop_detection::LoopDetector::is_search_shell_command(cmd)
146            };
147
148            if is_search || is_search_shell {
149                let search_pattern = args.as_ref().and_then(|a| {
150                    a.get("pattern")
151                        .or_else(|| a.get("query"))
152                        .and_then(|v| v.as_str())
153                });
154                let shell_pattern = if is_search_shell {
155                    args.as_ref()
156                        .and_then(|a| a.get("command"))
157                        .and_then(|v| v.as_str())
158                        .and_then(extract_search_pattern_from_command)
159                } else {
160                    None
161                };
162                let pat = search_pattern.or(shell_pattern.as_deref());
163                detector.record_search(name, &fp, pat)
164            } else {
165                detector.record_call(name, &fp)
166            }
167        };
168
169        if throttle_result.level == crate::core::loop_detection::ThrottleLevel::Blocked {
170            let msg = throttle_result.message.unwrap_or_default();
171            return Ok(CallToolResult::success(vec![Content::text(msg)]));
172        }
173
174        let throttle_warning =
175            if throttle_result.level == crate::core::loop_detection::ThrottleLevel::Reduced {
176                throttle_result.message.clone()
177            } else {
178                None
179            };
180
181        let tool_start = std::time::Instant::now();
182        let result_text = match name {
183            "ctx_read" => {
184                let path = match get_str(args, "path") {
185                    Some(p) => self.resolve_path(&p).await,
186                    None => return Err(ErrorData::invalid_params("path is required", None)),
187                };
188                let current_task = {
189                    let session = self.session.read().await;
190                    session.task.as_ref().map(|t| t.description.clone())
191                };
192                let task_ref = current_task.as_deref();
193                let mut mode = match get_str(args, "mode") {
194                    Some(m) => m,
195                    None => {
196                        let cache = self.cache.read().await;
197                        crate::tools::ctx_smart_read::select_mode_with_task(&cache, &path, task_ref)
198                    }
199                };
200                let fresh = get_bool(args, "fresh").unwrap_or(false);
201                let start_line = get_int(args, "start_line");
202                if let Some(sl) = start_line {
203                    let sl = sl.max(1_i64);
204                    mode = format!("lines:{sl}-999999");
205                }
206                let stale = self.is_prompt_cache_stale().await;
207                let effective_mode = LeanCtxServer::upgrade_mode_if_stale(&mode, stale).to_string();
208                let mut cache = self.cache.write().await;
209                let output = if fresh {
210                    crate::tools::ctx_read::handle_fresh_with_task(
211                        &mut cache,
212                        &path,
213                        &effective_mode,
214                        self.crp_mode,
215                        task_ref,
216                    )
217                } else {
218                    crate::tools::ctx_read::handle_with_task(
219                        &mut cache,
220                        &path,
221                        &effective_mode,
222                        self.crp_mode,
223                        task_ref,
224                    )
225                };
226                let stale_note = if effective_mode != mode {
227                    format!("[cache stale, {mode}→{effective_mode}]\n")
228                } else {
229                    String::new()
230                };
231                let original = cache.get(&path).map_or(0, |e| e.original_tokens);
232                let output_tokens = crate::core::tokens::count_tokens(&output);
233                let saved = original.saturating_sub(output_tokens);
234                let is_cache_hit = output.contains(" cached ");
235                let output = format!("{stale_note}{output}");
236                let file_ref = cache.file_ref_map().get(&path).cloned();
237                drop(cache);
238                let mut ensured_root: Option<String> = None;
239                {
240                    let mut session = self.session.write().await;
241                    session.touch_file(&path, file_ref.as_deref(), &effective_mode, original);
242                    if is_cache_hit {
243                        session.record_cache_hit();
244                    }
245                    let root_missing = session
246                        .project_root
247                        .as_deref()
248                        .map(|r| r.trim().is_empty())
249                        .unwrap_or(true);
250                    if root_missing {
251                        if let Some(root) = crate::core::protocol::detect_project_root(&path) {
252                            session.project_root = Some(root.clone());
253                            ensured_root = Some(root.clone());
254                            let mut current = self.agent_id.write().await;
255                            if current.is_none() {
256                                let mut registry =
257                                    crate::core::agents::AgentRegistry::load_or_create();
258                                registry.cleanup_stale(24);
259                                let role = std::env::var("LEAN_CTX_AGENT_ROLE").ok();
260                                let id = registry.register("mcp", role.as_deref(), &root);
261                                let _ = registry.save();
262                                *current = Some(id);
263                            }
264                        }
265                    }
266                }
267                if let Some(root) = ensured_root.as_deref() {
268                    crate::core::index_orchestrator::ensure_all_background(root);
269                }
270                self.record_call("ctx_read", original, saved, Some(mode.clone()))
271                    .await;
272                {
273                    let sig =
274                        crate::core::mode_predictor::FileSignature::from_path(&path, original);
275                    let density = if output_tokens > 0 {
276                        original as f64 / output_tokens as f64
277                    } else {
278                        1.0
279                    };
280                    let outcome = crate::core::mode_predictor::ModeOutcome {
281                        mode: mode.clone(),
282                        tokens_in: original,
283                        tokens_out: output_tokens,
284                        density: density.min(1.0),
285                    };
286                    let mut predictor = crate::core::mode_predictor::ModePredictor::new();
287                    predictor.record(sig, outcome);
288                    predictor.save();
289
290                    let ext = std::path::Path::new(&path)
291                        .extension()
292                        .and_then(|e| e.to_str())
293                        .unwrap_or("")
294                        .to_string();
295                    let thresholds = crate::core::adaptive_thresholds::thresholds_for_path(&path);
296                    let cache = self.cache.read().await;
297                    let stats = cache.get_stats();
298                    let feedback_outcome = crate::core::feedback::CompressionOutcome {
299                        session_id: format!("{}", std::process::id()),
300                        language: ext,
301                        entropy_threshold: thresholds.bpe_entropy,
302                        jaccard_threshold: thresholds.jaccard,
303                        total_turns: stats.total_reads as u32,
304                        tokens_saved: saved as u64,
305                        tokens_original: original as u64,
306                        cache_hits: stats.cache_hits as u32,
307                        total_reads: stats.total_reads as u32,
308                        task_completed: true,
309                        timestamp: chrono::Local::now().to_rfc3339(),
310                    };
311                    drop(cache);
312                    let mut store = crate::core::feedback::FeedbackStore::load();
313                    store.record_outcome(feedback_outcome);
314                }
315                output
316            }
317            "ctx_multi_read" => {
318                let raw_paths = get_str_array(args, "paths")
319                    .ok_or_else(|| ErrorData::invalid_params("paths array is required", None))?;
320                let mut paths = Vec::with_capacity(raw_paths.len());
321                for p in raw_paths {
322                    paths.push(self.resolve_path(&p).await);
323                }
324                let mode = get_str(args, "mode").unwrap_or_else(|| "full".to_string());
325                let current_task = {
326                    let session = self.session.read().await;
327                    session.task.as_ref().map(|t| t.description.clone())
328                };
329                let mut cache = self.cache.write().await;
330                let output = crate::tools::ctx_multi_read::handle_with_task(
331                    &mut cache,
332                    &paths,
333                    &mode,
334                    self.crp_mode,
335                    current_task.as_deref(),
336                );
337                let mut total_original: usize = 0;
338                for path in &paths {
339                    total_original = total_original
340                        .saturating_add(cache.get(path).map(|e| e.original_tokens).unwrap_or(0));
341                }
342                let tokens = crate::core::tokens::count_tokens(&output);
343                drop(cache);
344                self.record_call(
345                    "ctx_multi_read",
346                    total_original,
347                    total_original.saturating_sub(tokens),
348                    Some(mode),
349                )
350                .await;
351                output
352            }
353            "ctx_tree" => {
354                let path = self
355                    .resolve_path(&get_str(args, "path").unwrap_or_else(|| ".".to_string()))
356                    .await;
357                let depth = get_int(args, "depth").unwrap_or(3) as usize;
358                let show_hidden = get_bool(args, "show_hidden").unwrap_or(false);
359                let (result, original) = crate::tools::ctx_tree::handle(&path, depth, show_hidden);
360                let sent = crate::core::tokens::count_tokens(&result);
361                let saved = original.saturating_sub(sent);
362                self.record_call("ctx_tree", original, saved, None).await;
363                let savings_note = if saved > 0 {
364                    format!("\n[saved {saved} tokens vs native ls]")
365                } else {
366                    String::new()
367                };
368                format!("{result}{savings_note}")
369            }
370            "ctx_shell" => {
371                let command = get_str(args, "command")
372                    .ok_or_else(|| ErrorData::invalid_params("command is required", None))?;
373
374                if let Some(rejection) = crate::tools::ctx_shell::validate_command(&command) {
375                    self.record_call("ctx_shell", 0, 0, None).await;
376                    return Ok(CallToolResult::success(vec![Content::text(rejection)]));
377                }
378
379                let explicit_cwd = get_str(args, "cwd");
380                let effective_cwd = {
381                    let session = self.session.read().await;
382                    session.effective_cwd(explicit_cwd.as_deref())
383                };
384
385                let ensured_root = {
386                    let mut session = self.session.write().await;
387                    session.update_shell_cwd(&command);
388                    let root_missing = session
389                        .project_root
390                        .as_deref()
391                        .map(|r| r.trim().is_empty())
392                        .unwrap_or(true);
393                    if !root_missing {
394                        None
395                    } else {
396                        let home = dirs::home_dir().map(|h| h.to_string_lossy().to_string());
397                        crate::core::protocol::detect_project_root(&effective_cwd).and_then(|r| {
398                            if home.as_deref() == Some(r.as_str()) {
399                                None
400                            } else {
401                                session.project_root = Some(r.clone());
402                                Some(r)
403                            }
404                        })
405                    }
406                };
407                if let Some(root) = ensured_root.as_deref() {
408                    crate::core::index_orchestrator::ensure_all_background(root);
409                    let mut current = self.agent_id.write().await;
410                    if current.is_none() {
411                        let mut registry = crate::core::agents::AgentRegistry::load_or_create();
412                        registry.cleanup_stale(24);
413                        let role = std::env::var("LEAN_CTX_AGENT_ROLE").ok();
414                        let id = registry.register("mcp", role.as_deref(), root);
415                        let _ = registry.save();
416                        *current = Some(id);
417                    }
418                }
419
420                let raw = get_bool(args, "raw").unwrap_or(false)
421                    || std::env::var("LEAN_CTX_DISABLED").is_ok();
422                let cmd_clone = command.clone();
423                let cwd_clone = effective_cwd.clone();
424                let (output, real_exit_code) =
425                    tokio::task::spawn_blocking(move || execute_command_in(&cmd_clone, &cwd_clone))
426                        .await
427                        .unwrap_or_else(|e| (format!("ERROR: shell task failed: {e}"), 1));
428
429                if raw {
430                    let original = crate::core::tokens::count_tokens(&output);
431                    self.record_call("ctx_shell", original, 0, None).await;
432                    output
433                } else {
434                    let result = crate::tools::ctx_shell::handle(&command, &output, self.crp_mode);
435                    let original = crate::core::tokens::count_tokens(&output);
436                    let sent = crate::core::tokens::count_tokens(&result);
437                    let saved = original.saturating_sub(sent);
438                    self.record_call("ctx_shell", original, saved, None).await;
439
440                    let cfg = crate::core::config::Config::load();
441                    let tee_hint = match cfg.tee_mode {
442                        crate::core::config::TeeMode::Always => {
443                            crate::shell::save_tee(&command, &output)
444                                .map(|p| format!("\n[full output: {p}]"))
445                                .unwrap_or_default()
446                        }
447                        crate::core::config::TeeMode::Failures
448                            if !output.trim().is_empty() && output.contains("error")
449                                || output.contains("Error")
450                                || output.contains("ERROR") =>
451                        {
452                            crate::shell::save_tee(&command, &output)
453                                .map(|p| format!("\n[full output: {p}]"))
454                                .unwrap_or_default()
455                        }
456                        _ => String::new(),
457                    };
458
459                    let savings_note = if saved > 0 {
460                        format!("\n[saved {saved} tokens vs native Shell]")
461                    } else {
462                        String::new()
463                    };
464
465                    // Bug Memory: detect errors / resolve pending
466                    {
467                        let sess = self.session.read().await;
468                        let root = sess.project_root.clone();
469                        let sid = sess.id.clone();
470                        let files: Vec<String> = sess
471                            .files_touched
472                            .iter()
473                            .map(|ft| ft.path.clone())
474                            .collect();
475                        drop(sess);
476
477                        if let Some(ref root) = root {
478                            let mut store = crate::core::gotcha_tracker::GotchaStore::load(root);
479
480                            if real_exit_code != 0 {
481                                store.detect_error(&output, &command, real_exit_code, &files, &sid);
482                            } else {
483                                // Success: check if any injected gotchas prevented a repeat
484                                let relevant = store.top_relevant(&files, 7);
485                                let relevant_ids: Vec<String> =
486                                    relevant.iter().map(|g| g.id.clone()).collect();
487                                for gid in &relevant_ids {
488                                    store.mark_prevented(gid);
489                                }
490
491                                if store.try_resolve_pending(&command, &files, &sid).is_some() {
492                                    store.cross_session_boost();
493                                }
494
495                                // Promote mature gotchas to ProjectKnowledge
496                                let promotions = store.check_promotions();
497                                if !promotions.is_empty() {
498                                    let mut knowledge =
499                                        crate::core::knowledge::ProjectKnowledge::load_or_create(
500                                            root,
501                                        );
502                                    for (cat, trigger, resolution, conf) in &promotions {
503                                        knowledge.remember(
504                                            &format!("gotcha-{cat}"),
505                                            trigger,
506                                            resolution,
507                                            &sid,
508                                            *conf,
509                                        );
510                                    }
511                                    let _ = knowledge.save();
512                                }
513                            }
514
515                            let _ = store.save(root);
516                        }
517                    }
518
519                    format!("{result}{savings_note}{tee_hint}")
520                }
521            }
522            "ctx_search" => {
523                let pattern = get_str(args, "pattern")
524                    .ok_or_else(|| ErrorData::invalid_params("pattern is required", None))?;
525                let path = self
526                    .resolve_path(&get_str(args, "path").unwrap_or_else(|| ".".to_string()))
527                    .await;
528                let ext = get_str(args, "ext");
529                let max = get_int(args, "max_results").unwrap_or(20) as usize;
530                let no_gitignore = get_bool(args, "ignore_gitignore").unwrap_or(false);
531                let crp = self.crp_mode;
532                let respect = !no_gitignore;
533                let search_result = tokio::time::timeout(
534                    std::time::Duration::from_secs(30),
535                    tokio::task::spawn_blocking(move || {
536                        crate::tools::ctx_search::handle(
537                            &pattern,
538                            &path,
539                            ext.as_deref(),
540                            max,
541                            crp,
542                            respect,
543                        )
544                    }),
545                )
546                .await;
547                let (result, original) = match search_result {
548                    Ok(Ok(r)) => r,
549                    Ok(Err(e)) => {
550                        return Err(ErrorData::internal_error(
551                            format!("search task failed: {e}"),
552                            None,
553                        ))
554                    }
555                    Err(_) => {
556                        let msg = "ctx_search timed out after 30s. Try narrowing the search:\n\
557                                   • Use a more specific pattern\n\
558                                   • Specify ext= to limit file types\n\
559                                   • Specify a subdirectory in path=";
560                        self.record_call("ctx_search", 0, 0, None).await;
561                        return Ok(CallToolResult::success(vec![Content::text(msg)]));
562                    }
563                };
564                let sent = crate::core::tokens::count_tokens(&result);
565                let saved = original.saturating_sub(sent);
566                self.record_call("ctx_search", original, saved, None).await;
567                let savings_note = if saved > 0 {
568                    format!("\n[saved {saved} tokens vs native Grep]")
569                } else {
570                    String::new()
571                };
572                format!("{result}{savings_note}")
573            }
574            "ctx_compress" => {
575                let include_sigs = get_bool(args, "include_signatures").unwrap_or(true);
576                let cache = self.cache.read().await;
577                let result =
578                    crate::tools::ctx_compress::handle(&cache, include_sigs, self.crp_mode);
579                drop(cache);
580                self.record_call("ctx_compress", 0, 0, None).await;
581                result
582            }
583            "ctx_benchmark" => {
584                let path = match get_str(args, "path") {
585                    Some(p) => self.resolve_path(&p).await,
586                    None => return Err(ErrorData::invalid_params("path is required", None)),
587                };
588                let action = get_str(args, "action").unwrap_or_default();
589                let result = if action == "project" {
590                    let fmt = get_str(args, "format").unwrap_or_default();
591                    let bench = crate::core::benchmark::run_project_benchmark(&path);
592                    match fmt.as_str() {
593                        "json" => crate::core::benchmark::format_json(&bench),
594                        "markdown" | "md" => crate::core::benchmark::format_markdown(&bench),
595                        _ => crate::core::benchmark::format_terminal(&bench),
596                    }
597                } else {
598                    crate::tools::ctx_benchmark::handle(&path, self.crp_mode)
599                };
600                self.record_call("ctx_benchmark", 0, 0, None).await;
601                result
602            }
603            "ctx_metrics" => {
604                let cache = self.cache.read().await;
605                let calls = self.tool_calls.read().await;
606                let result = crate::tools::ctx_metrics::handle(&cache, &calls, self.crp_mode);
607                drop(cache);
608                drop(calls);
609                self.record_call("ctx_metrics", 0, 0, None).await;
610                result
611            }
612            "ctx_analyze" => {
613                let path = match get_str(args, "path") {
614                    Some(p) => self.resolve_path(&p).await,
615                    None => return Err(ErrorData::invalid_params("path is required", None)),
616                };
617                let result = crate::tools::ctx_analyze::handle(&path, self.crp_mode);
618                self.record_call("ctx_analyze", 0, 0, None).await;
619                result
620            }
621            "ctx_discover" => {
622                let limit = get_int(args, "limit").unwrap_or(15) as usize;
623                let history = crate::cli::load_shell_history_pub();
624                let result = crate::tools::ctx_discover::discover_from_history(&history, limit);
625                self.record_call("ctx_discover", 0, 0, None).await;
626                result
627            }
628            "ctx_smart_read" => {
629                let path = match get_str(args, "path") {
630                    Some(p) => self.resolve_path(&p).await,
631                    None => return Err(ErrorData::invalid_params("path is required", None)),
632                };
633                let mut cache = self.cache.write().await;
634                let output = crate::tools::ctx_smart_read::handle(&mut cache, &path, self.crp_mode);
635                let original = cache.get(&path).map_or(0, |e| e.original_tokens);
636                let tokens = crate::core::tokens::count_tokens(&output);
637                drop(cache);
638                self.record_call(
639                    "ctx_smart_read",
640                    original,
641                    original.saturating_sub(tokens),
642                    Some("auto".to_string()),
643                )
644                .await;
645                output
646            }
647            "ctx_delta" => {
648                let path = match get_str(args, "path") {
649                    Some(p) => self.resolve_path(&p).await,
650                    None => return Err(ErrorData::invalid_params("path is required", None)),
651                };
652                let mut cache = self.cache.write().await;
653                let output = crate::tools::ctx_delta::handle(&mut cache, &path);
654                let original = cache.get(&path).map_or(0, |e| e.original_tokens);
655                let tokens = crate::core::tokens::count_tokens(&output);
656                drop(cache);
657                {
658                    let mut session = self.session.write().await;
659                    session.mark_modified(&path);
660                }
661                self.record_call(
662                    "ctx_delta",
663                    original,
664                    original.saturating_sub(tokens),
665                    Some("delta".to_string()),
666                )
667                .await;
668                output
669            }
670            "ctx_edit" => {
671                let path = match get_str(args, "path") {
672                    Some(p) => self.resolve_path(&p).await,
673                    None => return Err(ErrorData::invalid_params("path is required", None)),
674                };
675                let old_string = get_str(args, "old_string").unwrap_or_default();
676                let new_string = get_str(args, "new_string")
677                    .ok_or_else(|| ErrorData::invalid_params("new_string is required", None))?;
678                let replace_all = args
679                    .as_ref()
680                    .and_then(|a| a.get("replace_all"))
681                    .and_then(|v| v.as_bool())
682                    .unwrap_or(false);
683                let create = args
684                    .as_ref()
685                    .and_then(|a| a.get("create"))
686                    .and_then(|v| v.as_bool())
687                    .unwrap_or(false);
688
689                let mut cache = self.cache.write().await;
690                let output = crate::tools::ctx_edit::handle(
691                    &mut cache,
692                    crate::tools::ctx_edit::EditParams {
693                        path: path.clone(),
694                        old_string,
695                        new_string,
696                        replace_all,
697                        create,
698                    },
699                );
700                drop(cache);
701
702                {
703                    let mut session = self.session.write().await;
704                    session.mark_modified(&path);
705                }
706                self.record_call("ctx_edit", 0, 0, None).await;
707                output
708            }
709            "ctx_dedup" => {
710                let action = get_str(args, "action").unwrap_or_default();
711                if action == "apply" {
712                    let mut cache = self.cache.write().await;
713                    let result = crate::tools::ctx_dedup::handle_action(&mut cache, &action);
714                    drop(cache);
715                    self.record_call("ctx_dedup", 0, 0, None).await;
716                    result
717                } else {
718                    let cache = self.cache.read().await;
719                    let result = crate::tools::ctx_dedup::handle(&cache);
720                    drop(cache);
721                    self.record_call("ctx_dedup", 0, 0, None).await;
722                    result
723                }
724            }
725            "ctx_fill" => {
726                let raw_paths = get_str_array(args, "paths")
727                    .ok_or_else(|| ErrorData::invalid_params("paths array is required", None))?;
728                let mut paths = Vec::with_capacity(raw_paths.len());
729                for p in raw_paths {
730                    paths.push(self.resolve_path(&p).await);
731                }
732                let budget = get_int(args, "budget")
733                    .ok_or_else(|| ErrorData::invalid_params("budget is required", None))?
734                    as usize;
735                let mut cache = self.cache.write().await;
736                let output =
737                    crate::tools::ctx_fill::handle(&mut cache, &paths, budget, self.crp_mode);
738                drop(cache);
739                self.record_call("ctx_fill", 0, 0, Some(format!("budget:{budget}")))
740                    .await;
741                output
742            }
743            "ctx_intent" => {
744                let query = get_str(args, "query")
745                    .ok_or_else(|| ErrorData::invalid_params("query is required", None))?;
746                let root = get_str(args, "project_root").unwrap_or_else(|| ".".to_string());
747                let mut cache = self.cache.write().await;
748                let output =
749                    crate::tools::ctx_intent::handle(&mut cache, &query, &root, self.crp_mode);
750                drop(cache);
751                {
752                    let mut session = self.session.write().await;
753                    session.set_task(&query, Some("intent"));
754                }
755                self.record_call("ctx_intent", 0, 0, Some("semantic".to_string()))
756                    .await;
757                output
758            }
759            "ctx_response" => {
760                let text = get_str(args, "text")
761                    .ok_or_else(|| ErrorData::invalid_params("text is required", None))?;
762                let output = crate::tools::ctx_response::handle(&text, self.crp_mode);
763                self.record_call("ctx_response", 0, 0, None).await;
764                output
765            }
766            "ctx_context" => {
767                let cache = self.cache.read().await;
768                let turn = self.call_count.load(std::sync::atomic::Ordering::Relaxed);
769                let result = crate::tools::ctx_context::handle_status(&cache, turn, self.crp_mode);
770                drop(cache);
771                self.record_call("ctx_context", 0, 0, None).await;
772                result
773            }
774            "ctx_graph" => {
775                let action = get_str(args, "action")
776                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
777                let path = match get_str(args, "path") {
778                    Some(p) => Some(self.resolve_path(&p).await),
779                    None => None,
780                };
781                let root = self
782                    .resolve_path(&get_str(args, "project_root").unwrap_or_else(|| ".".to_string()))
783                    .await;
784                let mut cache = self.cache.write().await;
785                let result = crate::tools::ctx_graph::handle(
786                    &action,
787                    path.as_deref(),
788                    &root,
789                    &mut cache,
790                    self.crp_mode,
791                );
792                drop(cache);
793                self.record_call("ctx_graph", 0, 0, Some(action)).await;
794                result
795            }
796            "ctx_cache" => {
797                let action = get_str(args, "action")
798                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
799                let mut cache = self.cache.write().await;
800                let result = match action.as_str() {
801                    "status" => {
802                        let entries = cache.get_all_entries();
803                        if entries.is_empty() {
804                            "Cache empty — no files tracked.".to_string()
805                        } else {
806                            let mut lines = vec![format!("Cache: {} file(s)", entries.len())];
807                            for (path, entry) in &entries {
808                                let fref = cache
809                                    .file_ref_map()
810                                    .get(*path)
811                                    .map(|s| s.as_str())
812                                    .unwrap_or("F?");
813                                lines.push(format!(
814                                    "  {fref}={} [{}L, {}t, read {}x]",
815                                    crate::core::protocol::shorten_path(path),
816                                    entry.line_count,
817                                    entry.original_tokens,
818                                    entry.read_count
819                                ));
820                            }
821                            lines.join("\n")
822                        }
823                    }
824                    "clear" => {
825                        let count = cache.clear();
826                        format!("Cache cleared — {count} file(s) removed. Next ctx_read will return full content.")
827                    }
828                    "invalidate" => {
829                        let path = match get_str(args, "path") {
830                            Some(p) => self.resolve_path(&p).await,
831                            None => {
832                                return Err(ErrorData::invalid_params(
833                                    "path is required for invalidate",
834                                    None,
835                                ))
836                            }
837                        };
838                        if cache.invalidate(&path) {
839                            format!(
840                                "Invalidated cache for {}. Next ctx_read will return full content.",
841                                crate::core::protocol::shorten_path(&path)
842                            )
843                        } else {
844                            format!(
845                                "{} was not in cache.",
846                                crate::core::protocol::shorten_path(&path)
847                            )
848                        }
849                    }
850                    _ => "Unknown action. Use: status, clear, invalidate".to_string(),
851                };
852                drop(cache);
853                self.record_call("ctx_cache", 0, 0, Some(action)).await;
854                result
855            }
856            "ctx_session" => {
857                let action = get_str(args, "action")
858                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
859                let value = get_str(args, "value");
860                let sid = get_str(args, "session_id");
861                let mut session = self.session.write().await;
862                let result = crate::tools::ctx_session::handle(
863                    &mut session,
864                    &action,
865                    value.as_deref(),
866                    sid.as_deref(),
867                );
868                drop(session);
869                self.record_call("ctx_session", 0, 0, Some(action)).await;
870                result
871            }
872            "ctx_knowledge" => {
873                let action = get_str(args, "action")
874                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
875                let category = get_str(args, "category");
876                let key = get_str(args, "key");
877                let value = get_str(args, "value");
878                let query = get_str(args, "query");
879                let pattern_type = get_str(args, "pattern_type");
880                let examples = get_str_array(args, "examples");
881                let confidence: Option<f32> = args
882                    .as_ref()
883                    .and_then(|a| a.get("confidence"))
884                    .and_then(|v| v.as_f64())
885                    .map(|v| v as f32);
886
887                let session = self.session.read().await;
888                let session_id = session.id.clone();
889                let project_root = session.project_root.clone().unwrap_or_else(|| {
890                    std::env::current_dir()
891                        .map(|p| p.to_string_lossy().to_string())
892                        .unwrap_or_else(|_| "unknown".to_string())
893                });
894                drop(session);
895
896                if action == "gotcha" {
897                    let trigger = get_str(args, "trigger").unwrap_or_default();
898                    let resolution = get_str(args, "resolution").unwrap_or_default();
899                    let severity = get_str(args, "severity").unwrap_or_default();
900                    let cat = category.as_deref().unwrap_or("convention");
901
902                    if trigger.is_empty() || resolution.is_empty() {
903                        self.record_call("ctx_knowledge", 0, 0, Some(action)).await;
904                        return Ok(CallToolResult::success(vec![Content::text(
905                            "ERROR: trigger and resolution are required for gotcha action",
906                        )]));
907                    }
908
909                    let mut store = crate::core::gotcha_tracker::GotchaStore::load(&project_root);
910                    let msg = match store.report_gotcha(
911                        &trigger,
912                        &resolution,
913                        cat,
914                        &severity,
915                        &session_id,
916                    ) {
917                        Some(gotcha) => {
918                            let conf = (gotcha.confidence * 100.0) as u32;
919                            let label = gotcha.category.short_label();
920                            format!("Gotcha recorded: [{label}] {trigger} (confidence: {conf}%)")
921                        }
922                        None => format!(
923                            "Gotcha noted: {trigger} (evicted by higher-confidence entries)"
924                        ),
925                    };
926                    let _ = store.save(&project_root);
927                    self.record_call("ctx_knowledge", 0, 0, Some(action)).await;
928                    return Ok(CallToolResult::success(vec![Content::text(msg)]));
929                }
930
931                let result = crate::tools::ctx_knowledge::handle(
932                    &project_root,
933                    &action,
934                    category.as_deref(),
935                    key.as_deref(),
936                    value.as_deref(),
937                    query.as_deref(),
938                    &session_id,
939                    pattern_type.as_deref(),
940                    examples,
941                    confidence,
942                );
943                self.record_call("ctx_knowledge", 0, 0, Some(action)).await;
944                result
945            }
946            "ctx_agent" => {
947                let action = get_str(args, "action")
948                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
949                let agent_type = get_str(args, "agent_type");
950                let role = get_str(args, "role");
951                let message = get_str(args, "message");
952                let category = get_str(args, "category");
953                let to_agent = get_str(args, "to_agent");
954                let status = get_str(args, "status");
955
956                let session = self.session.read().await;
957                let project_root = session.project_root.clone().unwrap_or_else(|| {
958                    std::env::current_dir()
959                        .map(|p| p.to_string_lossy().to_string())
960                        .unwrap_or_else(|_| "unknown".to_string())
961                });
962                drop(session);
963
964                let current_agent_id = self.agent_id.read().await.clone();
965                let result = crate::tools::ctx_agent::handle(
966                    &action,
967                    agent_type.as_deref(),
968                    role.as_deref(),
969                    &project_root,
970                    current_agent_id.as_deref(),
971                    message.as_deref(),
972                    category.as_deref(),
973                    to_agent.as_deref(),
974                    status.as_deref(),
975                );
976
977                if action == "register" {
978                    if let Some(id) = result.split(':').nth(1) {
979                        let id = id.split_whitespace().next().unwrap_or("").to_string();
980                        if !id.is_empty() {
981                            *self.agent_id.write().await = Some(id);
982                        }
983                    }
984                }
985
986                self.record_call("ctx_agent", 0, 0, Some(action)).await;
987                result
988            }
989            "ctx_share" => {
990                let action = get_str(args, "action")
991                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
992                let to_agent = get_str(args, "to_agent");
993                let paths = get_str(args, "paths");
994                let message = get_str(args, "message");
995
996                let from_agent = self.agent_id.read().await.clone();
997                let cache = self.cache.read().await;
998                let result = crate::tools::ctx_share::handle(
999                    &action,
1000                    from_agent.as_deref(),
1001                    to_agent.as_deref(),
1002                    paths.as_deref(),
1003                    message.as_deref(),
1004                    &cache,
1005                );
1006                drop(cache);
1007
1008                self.record_call("ctx_share", 0, 0, Some(action)).await;
1009                result
1010            }
1011            "ctx_overview" => {
1012                let task = get_str(args, "task");
1013                let resolved_path = match get_str(args, "path") {
1014                    Some(p) => Some(self.resolve_path(&p).await),
1015                    None => {
1016                        let session = self.session.read().await;
1017                        session.project_root.clone()
1018                    }
1019                };
1020                let cache = self.cache.read().await;
1021                let result = crate::tools::ctx_overview::handle(
1022                    &cache,
1023                    task.as_deref(),
1024                    resolved_path.as_deref(),
1025                    self.crp_mode,
1026                );
1027                drop(cache);
1028                self.record_call("ctx_overview", 0, 0, Some("overview".to_string()))
1029                    .await;
1030                result
1031            }
1032            "ctx_preload" => {
1033                let task = get_str(args, "task").unwrap_or_default();
1034                let resolved_path = match get_str(args, "path") {
1035                    Some(p) => Some(self.resolve_path(&p).await),
1036                    None => {
1037                        let session = self.session.read().await;
1038                        session.project_root.clone()
1039                    }
1040                };
1041                let mut cache = self.cache.write().await;
1042                let result = crate::tools::ctx_preload::handle(
1043                    &mut cache,
1044                    &task,
1045                    resolved_path.as_deref(),
1046                    self.crp_mode,
1047                );
1048                drop(cache);
1049                self.record_call("ctx_preload", 0, 0, Some("preload".to_string()))
1050                    .await;
1051                result
1052            }
1053            "ctx_wrapped" => {
1054                let period = get_str(args, "period").unwrap_or_else(|| "week".to_string());
1055                let result = crate::tools::ctx_wrapped::handle(&period);
1056                self.record_call("ctx_wrapped", 0, 0, Some(period)).await;
1057                result
1058            }
1059            "ctx_semantic_search" => {
1060                let query = get_str(args, "query")
1061                    .ok_or_else(|| ErrorData::invalid_params("query is required", None))?;
1062                let path = self
1063                    .resolve_path(&get_str(args, "path").unwrap_or_else(|| ".".to_string()))
1064                    .await;
1065                let top_k = get_int(args, "top_k").unwrap_or(10) as usize;
1066                let action = get_str(args, "action").unwrap_or_default();
1067                let mode = get_str(args, "mode");
1068                let languages = get_str_array(args, "languages");
1069                let path_glob = get_str(args, "path_glob");
1070                let result = if action == "reindex" {
1071                    crate::tools::ctx_semantic_search::handle_reindex(&path)
1072                } else {
1073                    crate::tools::ctx_semantic_search::handle(
1074                        &query,
1075                        &path,
1076                        top_k,
1077                        self.crp_mode,
1078                        languages,
1079                        path_glob.as_deref(),
1080                        mode.as_deref(),
1081                    )
1082                };
1083                self.record_call("ctx_semantic_search", 0, 0, Some("semantic".to_string()))
1084                    .await;
1085                result
1086            }
1087            "ctx_execute" => {
1088                let action = get_str(args, "action").unwrap_or_default();
1089
1090                let result = if action == "batch" {
1091                    let items_str = get_str(args, "items").ok_or_else(|| {
1092                        ErrorData::invalid_params("items is required for batch", None)
1093                    })?;
1094                    let items: Vec<serde_json::Value> =
1095                        serde_json::from_str(&items_str).map_err(|e| {
1096                            ErrorData::invalid_params(format!("Invalid items JSON: {e}"), None)
1097                        })?;
1098                    let batch: Vec<(String, String)> = items
1099                        .iter()
1100                        .filter_map(|item| {
1101                            let lang = item.get("language")?.as_str()?.to_string();
1102                            let code = item.get("code")?.as_str()?.to_string();
1103                            Some((lang, code))
1104                        })
1105                        .collect();
1106                    crate::tools::ctx_execute::handle_batch(&batch)
1107                } else if action == "file" {
1108                    let path = get_str(args, "path").ok_or_else(|| {
1109                        ErrorData::invalid_params("path is required for action=file", None)
1110                    })?;
1111                    let intent = get_str(args, "intent");
1112                    crate::tools::ctx_execute::handle_file(&path, intent.as_deref())
1113                } else {
1114                    let language = get_str(args, "language")
1115                        .ok_or_else(|| ErrorData::invalid_params("language is required", None))?;
1116                    let code = get_str(args, "code")
1117                        .ok_or_else(|| ErrorData::invalid_params("code is required", None))?;
1118                    let intent = get_str(args, "intent");
1119                    let timeout = get_int(args, "timeout").map(|t| t as u64);
1120                    crate::tools::ctx_execute::handle(&language, &code, intent.as_deref(), timeout)
1121                };
1122
1123                self.record_call("ctx_execute", 0, 0, Some(action)).await;
1124                result
1125            }
1126            "ctx_symbol" => {
1127                let sym_name = get_str(args, "name")
1128                    .ok_or_else(|| ErrorData::invalid_params("name is required", None))?;
1129                let file = get_str(args, "file");
1130                let kind = get_str(args, "kind");
1131                let session = self.session.read().await;
1132                let project_root = session
1133                    .project_root
1134                    .clone()
1135                    .unwrap_or_else(|| ".".to_string());
1136                drop(session);
1137                let (result, original) = crate::tools::ctx_symbol::handle(
1138                    &sym_name,
1139                    file.as_deref(),
1140                    kind.as_deref(),
1141                    &project_root,
1142                );
1143                let sent = crate::core::tokens::count_tokens(&result);
1144                let saved = original.saturating_sub(sent);
1145                self.record_call("ctx_symbol", original, saved, kind).await;
1146                result
1147            }
1148            "ctx_graph_diagram" => {
1149                let file = get_str(args, "file");
1150                let depth = get_int(args, "depth").map(|d| d as usize);
1151                let kind = get_str(args, "kind");
1152                let session = self.session.read().await;
1153                let project_root = session
1154                    .project_root
1155                    .clone()
1156                    .unwrap_or_else(|| ".".to_string());
1157                drop(session);
1158                let result = crate::tools::ctx_graph_diagram::handle(
1159                    file.as_deref(),
1160                    depth,
1161                    kind.as_deref(),
1162                    &project_root,
1163                );
1164                self.record_call("ctx_graph_diagram", 0, 0, kind).await;
1165                result
1166            }
1167            "ctx_routes" => {
1168                let method = get_str(args, "method");
1169                let path_prefix = get_str(args, "path");
1170                let session = self.session.read().await;
1171                let project_root = session
1172                    .project_root
1173                    .clone()
1174                    .unwrap_or_else(|| ".".to_string());
1175                drop(session);
1176                let result = crate::tools::ctx_routes::handle(
1177                    method.as_deref(),
1178                    path_prefix.as_deref(),
1179                    &project_root,
1180                );
1181                self.record_call("ctx_routes", 0, 0, None).await;
1182                result
1183            }
1184            "ctx_compress_memory" => {
1185                let path = self
1186                    .resolve_path(
1187                        &get_str(args, "path")
1188                            .ok_or_else(|| ErrorData::invalid_params("path is required", None))?,
1189                    )
1190                    .await;
1191                let result = crate::tools::ctx_compress_memory::handle(&path);
1192                self.record_call("ctx_compress_memory", 0, 0, None).await;
1193                result
1194            }
1195            "ctx_callers" => {
1196                let symbol = get_str(args, "symbol")
1197                    .ok_or_else(|| ErrorData::invalid_params("symbol is required", None))?;
1198                let file = get_str(args, "file");
1199                let session = self.session.read().await;
1200                let project_root = session
1201                    .project_root
1202                    .clone()
1203                    .unwrap_or_else(|| ".".to_string());
1204                drop(session);
1205                let result =
1206                    crate::tools::ctx_callers::handle(&symbol, file.as_deref(), &project_root);
1207                self.record_call("ctx_callers", 0, 0, None).await;
1208                result
1209            }
1210            "ctx_callees" => {
1211                let symbol = get_str(args, "symbol")
1212                    .ok_or_else(|| ErrorData::invalid_params("symbol is required", None))?;
1213                let file = get_str(args, "file");
1214                let session = self.session.read().await;
1215                let project_root = session
1216                    .project_root
1217                    .clone()
1218                    .unwrap_or_else(|| ".".to_string());
1219                drop(session);
1220                let result =
1221                    crate::tools::ctx_callees::handle(&symbol, file.as_deref(), &project_root);
1222                self.record_call("ctx_callees", 0, 0, None).await;
1223                result
1224            }
1225            "ctx_outline" => {
1226                let path = self
1227                    .resolve_path(
1228                        &get_str(args, "path")
1229                            .ok_or_else(|| ErrorData::invalid_params("path is required", None))?,
1230                    )
1231                    .await;
1232                let kind = get_str(args, "kind");
1233                let (result, original) = crate::tools::ctx_outline::handle(&path, kind.as_deref());
1234                let sent = crate::core::tokens::count_tokens(&result);
1235                let saved = original.saturating_sub(sent);
1236                self.record_call("ctx_outline", original, saved, kind).await;
1237                result
1238            }
1239            _ => {
1240                return Err(ErrorData::invalid_params(
1241                    format!("Unknown tool: {name}"),
1242                    None,
1243                ));
1244            }
1245        };
1246
1247        let mut result_text = result_text;
1248
1249        {
1250            let config = crate::core::config::Config::load();
1251            let density = crate::core::config::OutputDensity::effective(&config.output_density);
1252            result_text = crate::core::protocol::compress_output(&result_text, &density);
1253        }
1254
1255        if let Some(ctx) = auto_context {
1256            result_text = format!("{ctx}\n\n{result_text}");
1257        }
1258
1259        if let Some(warning) = throttle_warning {
1260            result_text = format!("{result_text}\n\n{warning}");
1261        }
1262
1263        if name == "ctx_read" {
1264            let read_path = self
1265                .resolve_path(&get_str(args, "path").unwrap_or_default())
1266                .await;
1267            let project_root = {
1268                let session = self.session.read().await;
1269                session.project_root.clone()
1270            };
1271            let mut cache = self.cache.write().await;
1272            let enrich = crate::tools::autonomy::enrich_after_read(
1273                &self.autonomy,
1274                &mut cache,
1275                &read_path,
1276                project_root.as_deref(),
1277            );
1278            if let Some(hint) = enrich.related_hint {
1279                result_text = format!("{result_text}\n{hint}");
1280            }
1281
1282            crate::tools::autonomy::maybe_auto_dedup(&self.autonomy, &mut cache);
1283        }
1284
1285        if name == "ctx_shell" {
1286            let cmd = get_str(args, "command").unwrap_or_default();
1287            let output_tokens = crate::core::tokens::count_tokens(&result_text);
1288            let calls = self.tool_calls.read().await;
1289            let last_original = calls.last().map(|c| c.original_tokens).unwrap_or(0);
1290            drop(calls);
1291            if let Some(hint) = crate::tools::autonomy::shell_efficiency_hint(
1292                &self.autonomy,
1293                &cmd,
1294                last_original,
1295                output_tokens,
1296            ) {
1297                result_text = format!("{result_text}\n{hint}");
1298            }
1299        }
1300
1301        let skip_checkpoint = matches!(
1302            name,
1303            "ctx_compress"
1304                | "ctx_metrics"
1305                | "ctx_benchmark"
1306                | "ctx_analyze"
1307                | "ctx_cache"
1308                | "ctx_discover"
1309                | "ctx_dedup"
1310                | "ctx_session"
1311                | "ctx_knowledge"
1312                | "ctx_agent"
1313                | "ctx_share"
1314                | "ctx_wrapped"
1315                | "ctx_overview"
1316                | "ctx_preload"
1317        );
1318
1319        if !skip_checkpoint && self.increment_and_check() {
1320            if let Some(checkpoint) = self.auto_checkpoint().await {
1321                let combined = format!(
1322                    "{result_text}\n\n--- AUTO CHECKPOINT (every {} calls) ---\n{checkpoint}",
1323                    self.checkpoint_interval
1324                );
1325                return Ok(CallToolResult::success(vec![Content::text(combined)]));
1326            }
1327        }
1328
1329        let tool_duration_ms = tool_start.elapsed().as_millis() as u64;
1330        if tool_duration_ms > 100 {
1331            LeanCtxServer::append_tool_call_log(
1332                name,
1333                tool_duration_ms,
1334                0,
1335                0,
1336                None,
1337                &chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
1338            );
1339        }
1340
1341        let current_count = self.call_count.load(std::sync::atomic::Ordering::Relaxed);
1342        if current_count > 0 && current_count.is_multiple_of(100) {
1343            std::thread::spawn(crate::cloud_sync::cloud_background_tasks);
1344        }
1345
1346        Ok(CallToolResult::success(vec![Content::text(result_text)]))
1347    }
1348}
1349
1350pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
1351    crate::instructions::build_instructions(crp_mode)
1352}
1353
1354fn get_str_array(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<Vec<String>> {
1355    let arr = args.as_ref()?.get(key)?.as_array()?;
1356    let mut out = Vec::with_capacity(arr.len());
1357    for v in arr {
1358        let s = v.as_str()?.to_string();
1359        out.push(s);
1360    }
1361    Some(out)
1362}
1363
1364fn get_str(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<String> {
1365    args.as_ref()?.get(key)?.as_str().map(|s| s.to_string())
1366}
1367
1368fn get_int(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<i64> {
1369    args.as_ref()?.get(key)?.as_i64()
1370}
1371
1372fn get_bool(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<bool> {
1373    args.as_ref()?.get(key)?.as_bool()
1374}
1375
1376fn extract_search_pattern_from_command(command: &str) -> Option<String> {
1377    let parts: Vec<&str> = command.split_whitespace().collect();
1378    if parts.len() < 2 {
1379        return None;
1380    }
1381    let cmd = parts[0];
1382    if cmd == "grep" || cmd == "rg" || cmd == "ag" || cmd == "ack" {
1383        for (i, part) in parts.iter().enumerate().skip(1) {
1384            if !part.starts_with('-') {
1385                return Some(part.to_string());
1386            }
1387            if (*part == "-e" || *part == "--regexp" || *part == "-m") && i + 1 < parts.len() {
1388                return Some(parts[i + 1].to_string());
1389            }
1390        }
1391    }
1392    if cmd == "find" || cmd == "fd" {
1393        for (i, part) in parts.iter().enumerate() {
1394            if (*part == "-name" || *part == "-iname") && i + 1 < parts.len() {
1395                return Some(
1396                    parts[i + 1]
1397                        .trim_matches('\'')
1398                        .trim_matches('"')
1399                        .to_string(),
1400                );
1401            }
1402        }
1403        if cmd == "fd" && parts.len() >= 2 && !parts[1].starts_with('-') {
1404            return Some(parts[1].to_string());
1405        }
1406    }
1407    None
1408}
1409
1410fn execute_command_in(command: &str, cwd: &str) -> (String, i32) {
1411    let (shell, flag) = crate::shell::shell_and_flag();
1412    let normalized_cmd = crate::tools::ctx_shell::normalize_command_for_shell(command);
1413    let dir = std::path::Path::new(cwd);
1414    let mut cmd = std::process::Command::new(&shell);
1415    cmd.arg(&flag)
1416        .arg(&normalized_cmd)
1417        .env("LEAN_CTX_ACTIVE", "1");
1418    if dir.is_dir() {
1419        cmd.current_dir(dir);
1420    }
1421    let output = cmd.output();
1422
1423    match output {
1424        Ok(out) => {
1425            let code = out.status.code().unwrap_or(1);
1426            let stdout = String::from_utf8_lossy(&out.stdout);
1427            let stderr = String::from_utf8_lossy(&out.stderr);
1428            let text = if stdout.is_empty() {
1429                stderr.to_string()
1430            } else if stderr.is_empty() {
1431                stdout.to_string()
1432            } else {
1433                format!("{stdout}\n{stderr}")
1434            };
1435            (text, code)
1436        }
1437        Err(e) => (format!("ERROR: {e}"), 1),
1438    }
1439}
1440
1441pub fn tool_descriptions_for_test() -> Vec<(&'static str, &'static str)> {
1442    crate::tool_defs::list_all_tool_defs()
1443        .into_iter()
1444        .map(|(name, desc, _)| (name, desc))
1445        .collect()
1446}
1447
1448pub fn tool_schemas_json_for_test() -> String {
1449    crate::tool_defs::list_all_tool_defs()
1450        .iter()
1451        .map(|(name, _, schema)| format!("{}: {}", name, schema))
1452        .collect::<Vec<_>>()
1453        .join("\n")
1454}
1455
1456#[cfg(test)]
1457mod tests {
1458    #[test]
1459    fn test_unified_tool_count() {
1460        let tools = crate::tool_defs::unified_tool_defs();
1461        assert_eq!(tools.len(), 5, "Expected 5 unified tools");
1462    }
1463
1464    #[test]
1465    fn test_granular_tool_count() {
1466        let tools = crate::tool_defs::granular_tool_defs();
1467        assert!(tools.len() >= 25, "Expected at least 25 granular tools");
1468    }
1469
1470    #[test]
1471    fn disabled_tools_filters_list() {
1472        let all = crate::tool_defs::granular_tool_defs();
1473        let total = all.len();
1474        let disabled = vec!["ctx_graph".to_string(), "ctx_agent".to_string()];
1475        let filtered: Vec<_> = all
1476            .into_iter()
1477            .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
1478            .collect();
1479        assert_eq!(filtered.len(), total - 2);
1480        assert!(!filtered.iter().any(|t| t.name.as_ref() == "ctx_graph"));
1481        assert!(!filtered.iter().any(|t| t.name.as_ref() == "ctx_agent"));
1482    }
1483
1484    #[test]
1485    fn empty_disabled_tools_returns_all() {
1486        let all = crate::tool_defs::granular_tool_defs();
1487        let total = all.len();
1488        let disabled: Vec<String> = vec![];
1489        let filtered: Vec<_> = all
1490            .into_iter()
1491            .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
1492            .collect();
1493        assert_eq!(filtered.len(), total);
1494    }
1495
1496    #[test]
1497    fn misspelled_disabled_tool_is_silently_ignored() {
1498        let all = crate::tool_defs::granular_tool_defs();
1499        let total = all.len();
1500        let disabled = vec!["ctx_nonexistent_tool".to_string()];
1501        let filtered: Vec<_> = all
1502            .into_iter()
1503            .filter(|t| !disabled.iter().any(|d| t.name.as_ref() == d.as_str()))
1504            .collect();
1505        assert_eq!(filtered.len(), total);
1506    }
1507}