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