lean_ctx/tools/registered/
ctx_outline.rs1use rmcp::ErrorData;
2use rmcp::model::Tool;
3use serde_json::{Map, Value, json};
4
5use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_str, require_resolved_path};
6use crate::tool_defs::tool_def;
7use crate::tools::ctx_outline::OutlineOpts;
8
9pub struct CtxOutlineTool;
10
11impl McpTool for CtxOutlineTool {
12 fn name(&self) -> &'static str {
13 "ctx_outline"
14 }
15
16 fn tool_def(&self) -> Tool {
17 tool_def(
18 "ctx_outline",
19 "WORKFLOW: call BEFORE ctx_read to map code structure (a syntax-aware table of contents).\n\
20 Accepts a FILE or a DIRECTORY (folder surface — per-file symbols). Symbols come from\n\
21 tree-sitter (27 languages, real line spans); a conservative regex fallback covers the rest.\n\
22 kind=fn|struct|class|trait|enum|impl|all filters by kind; match=<substr> filters by name\n\
23 (case-insensitive); format=json emits deterministic JSON labelling the backend per file.\n\
24 ANTIPATTERN: NOT for file content (use ctx_read) or deep understanding (use ctx_compose).",
25 json!({
26 "type": "object",
27 "properties": {
28 "path": { "type": "string", "description": "File or directory" },
29 "kind": { "type": "string", "description": "Filter by kind: fn|struct|class|trait|enum|impl|all" },
30 "match": { "type": "string", "description": "Keep only symbols whose name contains this (case-insensitive)" },
31 "format": { "type": "string", "description": "Output format: text (default) | json (deterministic)" }
32 },
33 "required": ["path"]
34 }),
35 )
36 }
37
38 fn handle(
39 &self,
40 args: &Map<String, Value>,
41 ctx: &ToolContext,
42 ) -> Result<ToolOutput, ErrorData> {
43 let path = require_resolved_path(ctx, args, "path")?;
44 let kind = get_str(args, "kind");
45 let name_match = get_str(args, "match");
46 let as_json = get_str(args, "format").as_deref() == Some("json");
47
48 let (result, original) = crate::tools::ctx_outline::run(
49 &path,
50 &OutlineOpts {
51 kind: kind.as_deref(),
52 name_match: name_match.as_deref(),
53 as_json,
54 },
55 );
56 let sent = crate::core::tokens::count_tokens(&result);
57 let saved = original.saturating_sub(sent);
58
59 Ok(ToolOutput {
60 text: result,
61 original_tokens: original,
62 saved_tokens: saved,
63 mode: kind,
64 path: Some(path),
65 changed: false,
66 shell_outcome: None,
67 })
68 }
69
70 fn produces_machine_readable(&self, args: Option<&Map<String, Value>>) -> bool {
74 args.and_then(|a| a.get("format")).and_then(Value::as_str) == Some("json")
75 }
76}