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        if std::env::var("LEAN_CTX_UNIFIED").is_ok()
52            && std::env::var("LEAN_CTX_FULL_TOOLS").is_err()
53        {
54            return Ok(ListToolsResult {
55                tools: crate::tool_defs::unified_tool_defs(),
56                ..Default::default()
57            });
58        }
59
60        Ok(ListToolsResult {
61            tools: crate::tool_defs::granular_tool_defs(),
62            ..Default::default()
63        })
64    }
65
66    async fn call_tool(
67        &self,
68        request: CallToolRequestParams,
69        _context: RequestContext<RoleServer>,
70    ) -> Result<CallToolResult, ErrorData> {
71        self.check_idle_expiry().await;
72
73        let original_name = request.name.as_ref().to_string();
74        let (resolved_name, resolved_args) = if original_name == "ctx" {
75            let sub = request
76                .arguments
77                .as_ref()
78                .and_then(|a| a.get("tool"))
79                .and_then(|v| v.as_str())
80                .map(|s| s.to_string())
81                .ok_or_else(|| {
82                    ErrorData::invalid_params("'tool' is required for ctx meta-tool", None)
83                })?;
84            let tool_name = if sub.starts_with("ctx_") {
85                sub
86            } else {
87                format!("ctx_{sub}")
88            };
89            let mut args = request.arguments.unwrap_or_default();
90            args.remove("tool");
91            (tool_name, Some(args))
92        } else {
93            (original_name, request.arguments)
94        };
95        let name = resolved_name.as_str();
96        let args = &resolved_args;
97
98        let auto_context = {
99            let task = {
100                let session = self.session.read().await;
101                session.task.as_ref().map(|t| t.description.clone())
102            };
103            let project_root = {
104                let session = self.session.read().await;
105                session.project_root.clone()
106            };
107            let mut cache = self.cache.write().await;
108            crate::tools::autonomy::session_lifecycle_pre_hook(
109                &self.autonomy,
110                name,
111                &mut cache,
112                task.as_deref(),
113                project_root.as_deref(),
114                self.crp_mode,
115            )
116        };
117
118        let tool_start = std::time::Instant::now();
119        let result_text = match name {
120            "ctx_read" => {
121                let path = get_str(args, "path")
122                    .map(|p| crate::hooks::normalize_tool_path(&p))
123                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
124                let current_task = {
125                    let session = self.session.read().await;
126                    session.task.as_ref().map(|t| t.description.clone())
127                };
128                let task_ref = current_task.as_deref();
129                let mut mode = match get_str(args, "mode") {
130                    Some(m) => m,
131                    None => {
132                        let cache = self.cache.read().await;
133                        crate::tools::ctx_smart_read::select_mode_with_task(&cache, &path, task_ref)
134                    }
135                };
136                let fresh = get_bool(args, "fresh").unwrap_or(false);
137                let start_line = get_int(args, "start_line");
138                if let Some(sl) = start_line {
139                    let sl = sl.max(1_i64);
140                    mode = format!("lines:{sl}-999999");
141                }
142                let stale = self.is_prompt_cache_stale().await;
143                let effective_mode = LeanCtxServer::upgrade_mode_if_stale(&mode, stale).to_string();
144                let mut cache = self.cache.write().await;
145                let output = if fresh {
146                    crate::tools::ctx_read::handle_fresh_with_task(
147                        &mut cache,
148                        &path,
149                        &effective_mode,
150                        self.crp_mode,
151                        task_ref,
152                    )
153                } else {
154                    crate::tools::ctx_read::handle_with_task(
155                        &mut cache,
156                        &path,
157                        &effective_mode,
158                        self.crp_mode,
159                        task_ref,
160                    )
161                };
162                let stale_note = if effective_mode != mode {
163                    format!("[cache stale, {mode}→{effective_mode}]\n")
164                } else {
165                    String::new()
166                };
167                let original = cache.get(&path).map_or(0, |e| e.original_tokens);
168                let output_tokens = crate::core::tokens::count_tokens(&output);
169                let saved = original.saturating_sub(output_tokens);
170                let is_cache_hit = output.contains(" cached ");
171                let output = format!("{stale_note}{output}");
172                let file_ref = cache.file_ref_map().get(&path).cloned();
173                drop(cache);
174                {
175                    let mut session = self.session.write().await;
176                    session.touch_file(&path, file_ref.as_deref(), &effective_mode, original);
177                    if is_cache_hit {
178                        session.record_cache_hit();
179                    }
180                    if session.project_root.is_none() {
181                        if let Some(root) = detect_project_root(&path) {
182                            session.project_root = Some(root.clone());
183                            let mut current = self.agent_id.write().await;
184                            if current.is_none() {
185                                let mut registry =
186                                    crate::core::agents::AgentRegistry::load_or_create();
187                                registry.cleanup_stale(24);
188                                let id = registry.register("mcp", None, &root);
189                                let _ = registry.save();
190                                *current = Some(id);
191                            }
192                        }
193                    }
194                }
195                self.record_call("ctx_read", original, saved, Some(mode.clone()))
196                    .await;
197                {
198                    let sig =
199                        crate::core::mode_predictor::FileSignature::from_path(&path, original);
200                    let density = if output_tokens > 0 {
201                        original as f64 / output_tokens as f64
202                    } else {
203                        1.0
204                    };
205                    let outcome = crate::core::mode_predictor::ModeOutcome {
206                        mode: mode.clone(),
207                        tokens_in: original,
208                        tokens_out: output_tokens,
209                        density: density.min(1.0),
210                    };
211                    let mut predictor = crate::core::mode_predictor::ModePredictor::new();
212                    predictor.record(sig, outcome);
213                    predictor.save();
214
215                    let ext = std::path::Path::new(&path)
216                        .extension()
217                        .and_then(|e| e.to_str())
218                        .unwrap_or("")
219                        .to_string();
220                    let thresholds = crate::core::adaptive_thresholds::thresholds_for_path(&path);
221                    let cache = self.cache.read().await;
222                    let stats = cache.get_stats();
223                    let feedback_outcome = crate::core::feedback::CompressionOutcome {
224                        session_id: format!("{}", std::process::id()),
225                        language: ext,
226                        entropy_threshold: thresholds.bpe_entropy,
227                        jaccard_threshold: thresholds.jaccard,
228                        total_turns: stats.total_reads as u32,
229                        tokens_saved: saved as u64,
230                        tokens_original: original as u64,
231                        cache_hits: stats.cache_hits as u32,
232                        total_reads: stats.total_reads as u32,
233                        task_completed: true,
234                        timestamp: chrono::Local::now().to_rfc3339(),
235                    };
236                    drop(cache);
237                    let mut store = crate::core::feedback::FeedbackStore::load();
238                    store.record_outcome(feedback_outcome);
239                }
240                output
241            }
242            "ctx_multi_read" => {
243                let paths = get_str_array(args, "paths")
244                    .ok_or_else(|| ErrorData::invalid_params("paths array is required", None))?
245                    .into_iter()
246                    .map(|p| crate::hooks::normalize_tool_path(&p))
247                    .collect::<Vec<_>>();
248                let mode = get_str(args, "mode").unwrap_or_else(|| "full".to_string());
249                let current_task = {
250                    let session = self.session.read().await;
251                    session.task.as_ref().map(|t| t.description.clone())
252                };
253                let mut cache = self.cache.write().await;
254                let output = crate::tools::ctx_multi_read::handle_with_task(
255                    &mut cache,
256                    &paths,
257                    &mode,
258                    self.crp_mode,
259                    current_task.as_deref(),
260                );
261                let mut total_original: usize = 0;
262                for path in &paths {
263                    total_original = total_original
264                        .saturating_add(cache.get(path).map(|e| e.original_tokens).unwrap_or(0));
265                }
266                let tokens = crate::core::tokens::count_tokens(&output);
267                drop(cache);
268                self.record_call(
269                    "ctx_multi_read",
270                    total_original,
271                    total_original.saturating_sub(tokens),
272                    Some(mode),
273                )
274                .await;
275                output
276            }
277            "ctx_tree" => {
278                let path = crate::hooks::normalize_tool_path(
279                    &get_str(args, "path").unwrap_or_else(|| ".".to_string()),
280                );
281                let depth = get_int(args, "depth").unwrap_or(3) as usize;
282                let show_hidden = get_bool(args, "show_hidden").unwrap_or(false);
283                let (result, original) = crate::tools::ctx_tree::handle(&path, depth, show_hidden);
284                let sent = crate::core::tokens::count_tokens(&result);
285                let saved = original.saturating_sub(sent);
286                self.record_call("ctx_tree", original, saved, None).await;
287                let savings_note = if saved > 0 {
288                    format!("\n[saved {saved} tokens vs native ls]")
289                } else {
290                    String::new()
291                };
292                format!("{result}{savings_note}")
293            }
294            "ctx_shell" => {
295                let command = get_str(args, "command")
296                    .ok_or_else(|| ErrorData::invalid_params("command is required", None))?;
297                let raw = get_bool(args, "raw").unwrap_or(false)
298                    || std::env::var("LEAN_CTX_DISABLED").is_ok();
299                let cmd_clone = command.clone();
300                let (output, real_exit_code) =
301                    tokio::task::spawn_blocking(move || execute_command(&cmd_clone))
302                        .await
303                        .unwrap_or_else(|e| (format!("ERROR: shell task failed: {e}"), 1));
304
305                if raw {
306                    let original = crate::core::tokens::count_tokens(&output);
307                    self.record_call("ctx_shell", original, 0, None).await;
308                    output
309                } else {
310                    let result = crate::tools::ctx_shell::handle(&command, &output, self.crp_mode);
311                    let original = crate::core::tokens::count_tokens(&output);
312                    let sent = crate::core::tokens::count_tokens(&result);
313                    let saved = original.saturating_sub(sent);
314                    self.record_call("ctx_shell", original, saved, None).await;
315
316                    let cfg = crate::core::config::Config::load();
317                    let tee_hint = match cfg.tee_mode {
318                        crate::core::config::TeeMode::Always => {
319                            crate::shell::save_tee(&command, &output)
320                                .map(|p| format!("\n[full output: {p}]"))
321                                .unwrap_or_default()
322                        }
323                        crate::core::config::TeeMode::Failures
324                            if !output.trim().is_empty() && output.contains("error")
325                                || output.contains("Error")
326                                || output.contains("ERROR") =>
327                        {
328                            crate::shell::save_tee(&command, &output)
329                                .map(|p| format!("\n[full output: {p}]"))
330                                .unwrap_or_default()
331                        }
332                        _ => String::new(),
333                    };
334
335                    let savings_note = if saved > 0 {
336                        format!("\n[saved {saved} tokens vs native Shell]")
337                    } else {
338                        String::new()
339                    };
340
341                    // Bug Memory: detect errors / resolve pending
342                    {
343                        let sess = self.session.read().await;
344                        let root = sess.project_root.clone();
345                        let sid = sess.id.clone();
346                        let files: Vec<String> = sess
347                            .files_touched
348                            .iter()
349                            .map(|ft| ft.path.clone())
350                            .collect();
351                        drop(sess);
352
353                        if let Some(ref root) = root {
354                            let mut store = crate::core::gotcha_tracker::GotchaStore::load(root);
355
356                            if real_exit_code != 0 {
357                                store.detect_error(&output, &command, real_exit_code, &files, &sid);
358                            } else {
359                                // Success: check if any injected gotchas prevented a repeat
360                                let relevant = store.top_relevant(&files, 7);
361                                let relevant_ids: Vec<String> =
362                                    relevant.iter().map(|g| g.id.clone()).collect();
363                                for gid in &relevant_ids {
364                                    store.mark_prevented(gid);
365                                }
366
367                                if store.try_resolve_pending(&command, &files, &sid).is_some() {
368                                    store.cross_session_boost();
369                                }
370
371                                // Promote mature gotchas to ProjectKnowledge
372                                let promotions = store.check_promotions();
373                                if !promotions.is_empty() {
374                                    let mut knowledge =
375                                        crate::core::knowledge::ProjectKnowledge::load_or_create(
376                                            root,
377                                        );
378                                    for (cat, trigger, resolution, conf) in &promotions {
379                                        knowledge.remember(
380                                            &format!("gotcha-{cat}"),
381                                            trigger,
382                                            resolution,
383                                            &sid,
384                                            *conf,
385                                        );
386                                    }
387                                    let _ = knowledge.save();
388                                }
389                            }
390
391                            let _ = store.save(root);
392                        }
393                    }
394
395                    format!("{result}{savings_note}{tee_hint}")
396                }
397            }
398            "ctx_search" => {
399                let pattern = get_str(args, "pattern")
400                    .ok_or_else(|| ErrorData::invalid_params("pattern is required", None))?;
401                let path = crate::hooks::normalize_tool_path(
402                    &get_str(args, "path").unwrap_or_else(|| ".".to_string()),
403                );
404                let ext = get_str(args, "ext");
405                let max = get_int(args, "max_results").unwrap_or(20) as usize;
406                let no_gitignore = get_bool(args, "ignore_gitignore").unwrap_or(false);
407                let crp = self.crp_mode;
408                let respect = !no_gitignore;
409                let search_result = tokio::time::timeout(
410                    std::time::Duration::from_secs(30),
411                    tokio::task::spawn_blocking(move || {
412                        crate::tools::ctx_search::handle(
413                            &pattern,
414                            &path,
415                            ext.as_deref(),
416                            max,
417                            crp,
418                            respect,
419                        )
420                    }),
421                )
422                .await;
423                let (result, original) = match search_result {
424                    Ok(Ok(r)) => r,
425                    Ok(Err(e)) => {
426                        return Err(ErrorData::internal_error(
427                            format!("search task failed: {e}"),
428                            None,
429                        ))
430                    }
431                    Err(_) => {
432                        let msg = "ctx_search timed out after 30s. Try narrowing the search:\n\
433                                   • Use a more specific pattern\n\
434                                   • Specify ext= to limit file types\n\
435                                   • Specify a subdirectory in path=";
436                        self.record_call("ctx_search", 0, 0, None).await;
437                        return Ok(CallToolResult::success(vec![Content::text(msg)]));
438                    }
439                };
440                let sent = crate::core::tokens::count_tokens(&result);
441                let saved = original.saturating_sub(sent);
442                self.record_call("ctx_search", original, saved, None).await;
443                let savings_note = if saved > 0 {
444                    format!("\n[saved {saved} tokens vs native Grep]")
445                } else {
446                    String::new()
447                };
448                format!("{result}{savings_note}")
449            }
450            "ctx_compress" => {
451                let include_sigs = get_bool(args, "include_signatures").unwrap_or(true);
452                let cache = self.cache.read().await;
453                let result =
454                    crate::tools::ctx_compress::handle(&cache, include_sigs, self.crp_mode);
455                drop(cache);
456                self.record_call("ctx_compress", 0, 0, None).await;
457                result
458            }
459            "ctx_benchmark" => {
460                let path = get_str(args, "path")
461                    .map(|p| crate::hooks::normalize_tool_path(&p))
462                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
463                let action = get_str(args, "action").unwrap_or_default();
464                let result = if action == "project" {
465                    let fmt = get_str(args, "format").unwrap_or_default();
466                    let bench = crate::core::benchmark::run_project_benchmark(&path);
467                    match fmt.as_str() {
468                        "json" => crate::core::benchmark::format_json(&bench),
469                        "markdown" | "md" => crate::core::benchmark::format_markdown(&bench),
470                        _ => crate::core::benchmark::format_terminal(&bench),
471                    }
472                } else {
473                    crate::tools::ctx_benchmark::handle(&path, self.crp_mode)
474                };
475                self.record_call("ctx_benchmark", 0, 0, None).await;
476                result
477            }
478            "ctx_metrics" => {
479                let cache = self.cache.read().await;
480                let calls = self.tool_calls.read().await;
481                let result = crate::tools::ctx_metrics::handle(&cache, &calls, self.crp_mode);
482                drop(cache);
483                drop(calls);
484                self.record_call("ctx_metrics", 0, 0, None).await;
485                result
486            }
487            "ctx_analyze" => {
488                let path = get_str(args, "path")
489                    .map(|p| crate::hooks::normalize_tool_path(&p))
490                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
491                let result = crate::tools::ctx_analyze::handle(&path, self.crp_mode);
492                self.record_call("ctx_analyze", 0, 0, None).await;
493                result
494            }
495            "ctx_discover" => {
496                let limit = get_int(args, "limit").unwrap_or(15) as usize;
497                let history = crate::cli::load_shell_history_pub();
498                let result = crate::tools::ctx_discover::discover_from_history(&history, limit);
499                self.record_call("ctx_discover", 0, 0, None).await;
500                result
501            }
502            "ctx_smart_read" => {
503                let path = get_str(args, "path")
504                    .map(|p| crate::hooks::normalize_tool_path(&p))
505                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
506                let mut cache = self.cache.write().await;
507                let output = crate::tools::ctx_smart_read::handle(&mut cache, &path, self.crp_mode);
508                let original = cache.get(&path).map_or(0, |e| e.original_tokens);
509                let tokens = crate::core::tokens::count_tokens(&output);
510                drop(cache);
511                self.record_call(
512                    "ctx_smart_read",
513                    original,
514                    original.saturating_sub(tokens),
515                    Some("auto".to_string()),
516                )
517                .await;
518                output
519            }
520            "ctx_delta" => {
521                let path = get_str(args, "path")
522                    .map(|p| crate::hooks::normalize_tool_path(&p))
523                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
524                let mut cache = self.cache.write().await;
525                let output = crate::tools::ctx_delta::handle(&mut cache, &path);
526                let original = cache.get(&path).map_or(0, |e| e.original_tokens);
527                let tokens = crate::core::tokens::count_tokens(&output);
528                drop(cache);
529                {
530                    let mut session = self.session.write().await;
531                    session.mark_modified(&path);
532                }
533                self.record_call(
534                    "ctx_delta",
535                    original,
536                    original.saturating_sub(tokens),
537                    Some("delta".to_string()),
538                )
539                .await;
540                output
541            }
542            "ctx_edit" => {
543                let path = get_str(args, "path")
544                    .map(|p| crate::hooks::normalize_tool_path(&p))
545                    .ok_or_else(|| ErrorData::invalid_params("path is required", None))?;
546                let old_string = get_str(args, "old_string").unwrap_or_default();
547                let new_string = get_str(args, "new_string")
548                    .ok_or_else(|| ErrorData::invalid_params("new_string is required", None))?;
549                let replace_all = args
550                    .as_ref()
551                    .and_then(|a| a.get("replace_all"))
552                    .and_then(|v| v.as_bool())
553                    .unwrap_or(false);
554                let create = args
555                    .as_ref()
556                    .and_then(|a| a.get("create"))
557                    .and_then(|v| v.as_bool())
558                    .unwrap_or(false);
559
560                let mut cache = self.cache.write().await;
561                let output = crate::tools::ctx_edit::handle(
562                    &mut cache,
563                    crate::tools::ctx_edit::EditParams {
564                        path: path.clone(),
565                        old_string,
566                        new_string,
567                        replace_all,
568                        create,
569                    },
570                );
571                drop(cache);
572
573                {
574                    let mut session = self.session.write().await;
575                    session.mark_modified(&path);
576                }
577                self.record_call("ctx_edit", 0, 0, None).await;
578                output
579            }
580            "ctx_dedup" => {
581                let action = get_str(args, "action").unwrap_or_default();
582                if action == "apply" {
583                    let mut cache = self.cache.write().await;
584                    let result = crate::tools::ctx_dedup::handle_action(&mut cache, &action);
585                    drop(cache);
586                    self.record_call("ctx_dedup", 0, 0, None).await;
587                    result
588                } else {
589                    let cache = self.cache.read().await;
590                    let result = crate::tools::ctx_dedup::handle(&cache);
591                    drop(cache);
592                    self.record_call("ctx_dedup", 0, 0, None).await;
593                    result
594                }
595            }
596            "ctx_fill" => {
597                let paths = get_str_array(args, "paths")
598                    .ok_or_else(|| ErrorData::invalid_params("paths array is required", None))?
599                    .into_iter()
600                    .map(|p| crate::hooks::normalize_tool_path(&p))
601                    .collect::<Vec<_>>();
602                let budget = get_int(args, "budget")
603                    .ok_or_else(|| ErrorData::invalid_params("budget is required", None))?
604                    as usize;
605                let mut cache = self.cache.write().await;
606                let output =
607                    crate::tools::ctx_fill::handle(&mut cache, &paths, budget, self.crp_mode);
608                drop(cache);
609                self.record_call("ctx_fill", 0, 0, Some(format!("budget:{budget}")))
610                    .await;
611                output
612            }
613            "ctx_intent" => {
614                let query = get_str(args, "query")
615                    .ok_or_else(|| ErrorData::invalid_params("query is required", None))?;
616                let root = get_str(args, "project_root").unwrap_or_else(|| ".".to_string());
617                let mut cache = self.cache.write().await;
618                let output =
619                    crate::tools::ctx_intent::handle(&mut cache, &query, &root, self.crp_mode);
620                drop(cache);
621                {
622                    let mut session = self.session.write().await;
623                    session.set_task(&query, Some("intent"));
624                }
625                self.record_call("ctx_intent", 0, 0, Some("semantic".to_string()))
626                    .await;
627                output
628            }
629            "ctx_response" => {
630                let text = get_str(args, "text")
631                    .ok_or_else(|| ErrorData::invalid_params("text is required", None))?;
632                let output = crate::tools::ctx_response::handle(&text, self.crp_mode);
633                self.record_call("ctx_response", 0, 0, None).await;
634                output
635            }
636            "ctx_context" => {
637                let cache = self.cache.read().await;
638                let turn = self.call_count.load(std::sync::atomic::Ordering::Relaxed);
639                let result = crate::tools::ctx_context::handle_status(&cache, turn, self.crp_mode);
640                drop(cache);
641                self.record_call("ctx_context", 0, 0, None).await;
642                result
643            }
644            "ctx_graph" => {
645                let action = get_str(args, "action")
646                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
647                let path = get_str(args, "path").map(|p| crate::hooks::normalize_tool_path(&p));
648                let root = crate::hooks::normalize_tool_path(
649                    &get_str(args, "project_root").unwrap_or_else(|| ".".to_string()),
650                );
651                let mut cache = self.cache.write().await;
652                let result = crate::tools::ctx_graph::handle(
653                    &action,
654                    path.as_deref(),
655                    &root,
656                    &mut cache,
657                    self.crp_mode,
658                );
659                drop(cache);
660                self.record_call("ctx_graph", 0, 0, Some(action)).await;
661                result
662            }
663            "ctx_cache" => {
664                let action = get_str(args, "action")
665                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
666                let mut cache = self.cache.write().await;
667                let result = match action.as_str() {
668                    "status" => {
669                        let entries = cache.get_all_entries();
670                        if entries.is_empty() {
671                            "Cache empty — no files tracked.".to_string()
672                        } else {
673                            let mut lines = vec![format!("Cache: {} file(s)", entries.len())];
674                            for (path, entry) in &entries {
675                                let fref = cache
676                                    .file_ref_map()
677                                    .get(*path)
678                                    .map(|s| s.as_str())
679                                    .unwrap_or("F?");
680                                lines.push(format!(
681                                    "  {fref}={} [{}L, {}t, read {}x]",
682                                    crate::core::protocol::shorten_path(path),
683                                    entry.line_count,
684                                    entry.original_tokens,
685                                    entry.read_count
686                                ));
687                            }
688                            lines.join("\n")
689                        }
690                    }
691                    "clear" => {
692                        let count = cache.clear();
693                        format!("Cache cleared — {count} file(s) removed. Next ctx_read will return full content.")
694                    }
695                    "invalidate" => {
696                        let path = get_str(args, "path")
697                            .map(|p| crate::hooks::normalize_tool_path(&p))
698                            .ok_or_else(|| {
699                                ErrorData::invalid_params("path is required for invalidate", None)
700                            })?;
701                        if cache.invalidate(&path) {
702                            format!(
703                                "Invalidated cache for {}. Next ctx_read will return full content.",
704                                crate::core::protocol::shorten_path(&path)
705                            )
706                        } else {
707                            format!(
708                                "{} was not in cache.",
709                                crate::core::protocol::shorten_path(&path)
710                            )
711                        }
712                    }
713                    _ => "Unknown action. Use: status, clear, invalidate".to_string(),
714                };
715                drop(cache);
716                self.record_call("ctx_cache", 0, 0, Some(action)).await;
717                result
718            }
719            "ctx_session" => {
720                let action = get_str(args, "action")
721                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
722                let value = get_str(args, "value");
723                let sid = get_str(args, "session_id");
724                let mut session = self.session.write().await;
725                let result = crate::tools::ctx_session::handle(
726                    &mut session,
727                    &action,
728                    value.as_deref(),
729                    sid.as_deref(),
730                );
731                drop(session);
732                self.record_call("ctx_session", 0, 0, Some(action)).await;
733                result
734            }
735            "ctx_knowledge" => {
736                let action = get_str(args, "action")
737                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
738                let category = get_str(args, "category");
739                let key = get_str(args, "key");
740                let value = get_str(args, "value");
741                let query = get_str(args, "query");
742                let pattern_type = get_str(args, "pattern_type");
743                let examples = get_str_array(args, "examples");
744                let confidence: Option<f32> = args
745                    .as_ref()
746                    .and_then(|a| a.get("confidence"))
747                    .and_then(|v| v.as_f64())
748                    .map(|v| v as f32);
749
750                let session = self.session.read().await;
751                let session_id = session.id.clone();
752                let project_root = session.project_root.clone().unwrap_or_else(|| {
753                    std::env::current_dir()
754                        .map(|p| p.to_string_lossy().to_string())
755                        .unwrap_or_else(|_| "unknown".to_string())
756                });
757                drop(session);
758
759                if action == "gotcha" {
760                    let trigger = get_str(args, "trigger").unwrap_or_default();
761                    let resolution = get_str(args, "resolution").unwrap_or_default();
762                    let severity = get_str(args, "severity").unwrap_or_default();
763                    let cat = category.as_deref().unwrap_or("convention");
764
765                    if trigger.is_empty() || resolution.is_empty() {
766                        self.record_call("ctx_knowledge", 0, 0, Some(action)).await;
767                        return Ok(CallToolResult::success(vec![Content::text(
768                            "ERROR: trigger and resolution are required for gotcha action",
769                        )]));
770                    }
771
772                    let mut store = crate::core::gotcha_tracker::GotchaStore::load(&project_root);
773                    let gotcha =
774                        store.report_gotcha(&trigger, &resolution, cat, &severity, &session_id);
775                    let conf = (gotcha.confidence * 100.0) as u32;
776                    let label = gotcha.category.short_label();
777                    let msg = format!("Gotcha recorded: [{label}] {trigger} (confidence: {conf}%)");
778                    let _ = store.save(&project_root);
779                    self.record_call("ctx_knowledge", 0, 0, Some(action)).await;
780                    return Ok(CallToolResult::success(vec![Content::text(msg)]));
781                }
782
783                let result = crate::tools::ctx_knowledge::handle(
784                    &project_root,
785                    &action,
786                    category.as_deref(),
787                    key.as_deref(),
788                    value.as_deref(),
789                    query.as_deref(),
790                    &session_id,
791                    pattern_type.as_deref(),
792                    examples,
793                    confidence,
794                );
795                self.record_call("ctx_knowledge", 0, 0, Some(action)).await;
796                result
797            }
798            "ctx_agent" => {
799                let action = get_str(args, "action")
800                    .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
801                let agent_type = get_str(args, "agent_type");
802                let role = get_str(args, "role");
803                let message = get_str(args, "message");
804                let category = get_str(args, "category");
805                let to_agent = get_str(args, "to_agent");
806                let status = get_str(args, "status");
807
808                let session = self.session.read().await;
809                let project_root = session.project_root.clone().unwrap_or_else(|| {
810                    std::env::current_dir()
811                        .map(|p| p.to_string_lossy().to_string())
812                        .unwrap_or_else(|_| "unknown".to_string())
813                });
814                drop(session);
815
816                let current_agent_id = self.agent_id.read().await.clone();
817                let result = crate::tools::ctx_agent::handle(
818                    &action,
819                    agent_type.as_deref(),
820                    role.as_deref(),
821                    &project_root,
822                    current_agent_id.as_deref(),
823                    message.as_deref(),
824                    category.as_deref(),
825                    to_agent.as_deref(),
826                    status.as_deref(),
827                );
828
829                if action == "register" {
830                    if let Some(id) = result.split(':').nth(1) {
831                        let id = id.split_whitespace().next().unwrap_or("").to_string();
832                        if !id.is_empty() {
833                            *self.agent_id.write().await = Some(id);
834                        }
835                    }
836                }
837
838                self.record_call("ctx_agent", 0, 0, Some(action)).await;
839                result
840            }
841            "ctx_overview" => {
842                let task = get_str(args, "task");
843                let path = get_str(args, "path").map(|p| crate::hooks::normalize_tool_path(&p));
844                let cache = self.cache.read().await;
845                let result = crate::tools::ctx_overview::handle(
846                    &cache,
847                    task.as_deref(),
848                    path.as_deref(),
849                    self.crp_mode,
850                );
851                drop(cache);
852                self.record_call("ctx_overview", 0, 0, Some("overview".to_string()))
853                    .await;
854                result
855            }
856            "ctx_preload" => {
857                let task = get_str(args, "task").unwrap_or_default();
858                let path = get_str(args, "path").map(|p| crate::hooks::normalize_tool_path(&p));
859                let mut cache = self.cache.write().await;
860                let result = crate::tools::ctx_preload::handle(
861                    &mut cache,
862                    &task,
863                    path.as_deref(),
864                    self.crp_mode,
865                );
866                drop(cache);
867                self.record_call("ctx_preload", 0, 0, Some("preload".to_string()))
868                    .await;
869                result
870            }
871            "ctx_wrapped" => {
872                let period = get_str(args, "period").unwrap_or_else(|| "week".to_string());
873                let result = crate::tools::ctx_wrapped::handle(&period);
874                self.record_call("ctx_wrapped", 0, 0, Some(period)).await;
875                result
876            }
877            "ctx_semantic_search" => {
878                let query = get_str(args, "query")
879                    .ok_or_else(|| ErrorData::invalid_params("query is required", None))?;
880                let path = crate::hooks::normalize_tool_path(
881                    &get_str(args, "path").unwrap_or_else(|| ".".to_string()),
882                );
883                let top_k = get_int(args, "top_k").unwrap_or(10) as usize;
884                let action = get_str(args, "action").unwrap_or_default();
885                let result = if action == "reindex" {
886                    crate::tools::ctx_semantic_search::handle_reindex(&path)
887                } else {
888                    crate::tools::ctx_semantic_search::handle(&query, &path, top_k, self.crp_mode)
889                };
890                self.record_call("ctx_semantic_search", 0, 0, Some("semantic".to_string()))
891                    .await;
892                result
893            }
894            _ => {
895                return Err(ErrorData::invalid_params(
896                    format!("Unknown tool: {name}"),
897                    None,
898                ));
899            }
900        };
901
902        let mut result_text = result_text;
903
904        if let Some(ctx) = auto_context {
905            result_text = format!("{ctx}\n\n{result_text}");
906        }
907
908        if name == "ctx_read" {
909            let read_path =
910                crate::hooks::normalize_tool_path(&get_str(args, "path").unwrap_or_default());
911            let project_root = {
912                let session = self.session.read().await;
913                session.project_root.clone()
914            };
915            let mut cache = self.cache.write().await;
916            let enrich = crate::tools::autonomy::enrich_after_read(
917                &self.autonomy,
918                &mut cache,
919                &read_path,
920                project_root.as_deref(),
921            );
922            if let Some(hint) = enrich.related_hint {
923                result_text = format!("{result_text}\n{hint}");
924            }
925
926            crate::tools::autonomy::maybe_auto_dedup(&self.autonomy, &mut cache);
927        }
928
929        if name == "ctx_shell" {
930            let cmd = get_str(args, "command").unwrap_or_default();
931            let output_tokens = crate::core::tokens::count_tokens(&result_text);
932            let calls = self.tool_calls.read().await;
933            let last_original = calls.last().map(|c| c.original_tokens).unwrap_or(0);
934            drop(calls);
935            if let Some(hint) = crate::tools::autonomy::shell_efficiency_hint(
936                &self.autonomy,
937                &cmd,
938                last_original,
939                output_tokens,
940            ) {
941                result_text = format!("{result_text}\n{hint}");
942            }
943        }
944
945        let skip_checkpoint = matches!(
946            name,
947            "ctx_compress"
948                | "ctx_metrics"
949                | "ctx_benchmark"
950                | "ctx_analyze"
951                | "ctx_cache"
952                | "ctx_discover"
953                | "ctx_dedup"
954                | "ctx_session"
955                | "ctx_knowledge"
956                | "ctx_agent"
957                | "ctx_wrapped"
958                | "ctx_overview"
959                | "ctx_preload"
960        );
961
962        if !skip_checkpoint && self.increment_and_check() {
963            if let Some(checkpoint) = self.auto_checkpoint().await {
964                let combined = format!(
965                    "{result_text}\n\n--- AUTO CHECKPOINT (every {} calls) ---\n{checkpoint}",
966                    self.checkpoint_interval
967                );
968                return Ok(CallToolResult::success(vec![Content::text(combined)]));
969            }
970        }
971
972        let tool_duration_ms = tool_start.elapsed().as_millis() as u64;
973        if tool_duration_ms > 100 {
974            LeanCtxServer::append_tool_call_log(
975                name,
976                tool_duration_ms,
977                0,
978                0,
979                None,
980                &chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
981            );
982        }
983
984        let current_count = self.call_count.load(std::sync::atomic::Ordering::Relaxed);
985        if current_count > 0 && current_count.is_multiple_of(100) {
986            std::thread::spawn(crate::cloud_sync::cloud_background_tasks);
987        }
988
989        Ok(CallToolResult::success(vec![Content::text(result_text)]))
990    }
991}
992
993pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
994    crate::instructions::build_instructions(crp_mode)
995}
996
997fn get_str_array(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<Vec<String>> {
998    let arr = args.as_ref()?.get(key)?.as_array()?;
999    let mut out = Vec::with_capacity(arr.len());
1000    for v in arr {
1001        let s = v.as_str()?.to_string();
1002        out.push(s);
1003    }
1004    Some(out)
1005}
1006
1007fn get_str(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<String> {
1008    args.as_ref()?.get(key)?.as_str().map(|s| s.to_string())
1009}
1010
1011fn get_int(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<i64> {
1012    args.as_ref()?.get(key)?.as_i64()
1013}
1014
1015fn get_bool(args: &Option<serde_json::Map<String, Value>>, key: &str) -> Option<bool> {
1016    args.as_ref()?.get(key)?.as_bool()
1017}
1018
1019fn execute_command(command: &str) -> (String, i32) {
1020    let (shell, flag) = crate::shell::shell_and_flag();
1021    let output = std::process::Command::new(&shell)
1022        .arg(&flag)
1023        .arg(command)
1024        .env("LEAN_CTX_ACTIVE", "1")
1025        .output();
1026
1027    match output {
1028        Ok(out) => {
1029            let code = out.status.code().unwrap_or(1);
1030            let stdout = String::from_utf8_lossy(&out.stdout);
1031            let stderr = String::from_utf8_lossy(&out.stderr);
1032            let text = if stdout.is_empty() {
1033                stderr.to_string()
1034            } else if stderr.is_empty() {
1035                stdout.to_string()
1036            } else {
1037                format!("{stdout}\n{stderr}")
1038            };
1039            (text, code)
1040        }
1041        Err(e) => (format!("ERROR: {e}"), 1),
1042    }
1043}
1044
1045fn detect_project_root(file_path: &str) -> Option<String> {
1046    let mut dir = std::path::Path::new(file_path).parent()?;
1047    loop {
1048        if dir.join(".git").exists() {
1049            return Some(dir.to_string_lossy().to_string());
1050        }
1051        dir = dir.parent()?;
1052    }
1053}
1054
1055pub fn tool_descriptions_for_test() -> Vec<(&'static str, &'static str)> {
1056    crate::tool_defs::list_all_tool_defs()
1057        .into_iter()
1058        .map(|(name, desc, _)| (name, desc))
1059        .collect()
1060}
1061
1062pub fn tool_schemas_json_for_test() -> String {
1063    crate::tool_defs::list_all_tool_defs()
1064        .iter()
1065        .map(|(name, _, schema)| format!("{}: {}", name, schema))
1066        .collect::<Vec<_>>()
1067        .join("\n")
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072    #[test]
1073    fn test_unified_tool_count() {
1074        let tools = crate::tool_defs::unified_tool_defs();
1075        assert_eq!(tools.len(), 5, "Expected 5 unified tools");
1076    }
1077
1078    #[test]
1079    fn test_granular_tool_count() {
1080        let tools = crate::tool_defs::granular_tool_defs();
1081        assert!(tools.len() >= 25, "Expected at least 25 granular tools");
1082    }
1083}