Skip to main content

lean_ctx/tools/registered/
ctx_graph.rs

1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{json, Map, Value};
4
5use crate::server::tool_trait::{get_str, get_usize, McpTool, ToolContext, ToolOutput};
6use crate::tool_defs::tool_def;
7
8pub struct CtxGraphTool;
9
10impl McpTool for CtxGraphTool {
11    fn name(&self) -> &'static str {
12        "ctx_graph"
13    }
14
15    fn tool_def(&self) -> Tool {
16        tool_def(
17            "ctx_graph",
18            "Code graph: dependencies, symbol usages, impact/blast radius, Mermaid diagrams, git-diff impact.",
19            json!({
20                "type": "object",
21                "properties": {
22                    "action": {
23                        "type": "string",
24                        "description": "build|related|symbol|impact|status|enrich|context|diagram|neighbors|path|explain|diff"
25                    },
26                    "path": {
27                        "type": "string",
28                        "description": "File path; file::symbol for action=symbol; FROM file for action=path"
29                    },
30                    "to": { "type": "string", "description": "Target file (action=path)" },
31                    "depth": { "type": "integer", "description": "Traversal depth" },
32                    "kind": { "type": "string", "description": "diagram: deps|calls" },
33                    "format": { "type": "string", "description": "text|json" },
34                    "since": { "type": "string", "description": "Git ref for action=diff (default HEAD~1)" },
35                    "project_root": { "type": "string" }
36                },
37                "required": ["action"]
38            }),
39        )
40    }
41
42    fn handle(
43        &self,
44        args: &Map<String, Value>,
45        ctx: &ToolContext,
46    ) -> Result<ToolOutput, ErrorData> {
47        let action = get_str(args, "action")
48            .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
49
50        // For diagram action, pass the raw path; for others, use the resolved path.
51        let path = if action == "diagram" {
52            get_str(args, "path")
53        } else if let Some(p) = ctx.resolved_path("path") {
54            Some(p.to_string())
55        } else if let Some(err) = ctx
56            .path_error("path")
57            .filter(|_| get_str(args, "path").is_some())
58        {
59            return Err(ErrorData::invalid_params(format!("path: {err}"), None));
60        } else {
61            None
62        };
63
64        let root = if let Some(p) = ctx.resolved_path("project_root") {
65            p.to_string()
66        } else if let Some(err) = ctx.path_error("project_root") {
67            return Err(ErrorData::invalid_params(
68                format!("project_root: {err}"),
69                None,
70            ));
71        } else {
72            ctx.project_root.clone()
73        };
74        let depth = get_usize(args, "depth").map(|d| d.min(64));
75        let kind = get_str(args, "kind");
76        let format = get_str(args, "format");
77        // `since` is a git ref, not a filesystem path — read it raw (no PathJail).
78        let since = get_str(args, "since");
79        let to = if let Some(p) = ctx.resolved_path("to") {
80            Some(p.to_string())
81        } else if let Some(err) = ctx
82            .path_error("to")
83            .filter(|_| get_str(args, "to").is_some())
84        {
85            return Err(ErrorData::invalid_params(format!("to: {err}"), None));
86        } else {
87            None
88        };
89
90        let cache = ctx
91            .cache
92            .as_ref()
93            .ok_or_else(|| ErrorData::internal_error("cache not available", None))?;
94        let Some(mut guard) = crate::server::bounded_lock::write(cache, "ctx_graph") else {
95            return Ok(ToolOutput::simple(
96                "[graph cache temporarily unavailable — retry in a moment]".to_string(),
97            ));
98        };
99        let result = crate::tools::ctx_graph::handle(
100            &action,
101            path.as_deref(),
102            &root,
103            &mut guard,
104            ctx.crp_mode,
105            depth,
106            kind.as_deref(),
107            to.as_deref(),
108            format.as_deref(),
109            since.as_deref(),
110        );
111
112        Ok(ToolOutput {
113            text: result,
114            original_tokens: 0,
115            saved_tokens: 0,
116            mode: Some(action),
117            path: None,
118            changed: false,
119            shell_outcome: None,
120        })
121    }
122}