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