Skip to main content

lean_ctx/tools/registered/
ctx_outline.rs

1use 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    /// `format=json` produces a deterministic, byte-stable JSON document (#498):
71    /// it must be returned verbatim, so the dispatch pipeline skips all prose
72    /// decorations and compression for it (#990).
73    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}