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