lean_ctx/tools/registered/
ctx_graph.rs1use rmcp::ErrorData;
2use rmcp::model::Tool;
3use serde_json::{Map, Value, json};
4
5use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_str, get_usize};
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 "Graph queries — find dependencies, relationships, and symbols.\n\
19 action=symbol path=\"file.rs::fnName\" returns the source (NOT usages).\n\
20 action=neighbors path=\"file.rs\" shows import neighbors with direction & confidence.\n\
21 action=impact path=\"file.rs\" shows reverse dependency tree (blast radius).\n\
22 action=path from→to shows shortest dependency chain between two files.\n\
23 action=diff since=HEAD~1 for git change impact.\n\
24 action=diagram kind=deps|calls renders a Mermaid diagram.\n\
25 For understanding code, use ctx_compose FIRST. Use ctx_graph for targeted structural queries.\n\
26 ANTIPATTERN: symbol returns only the DEFINITION — not usages. For REFERENCES use grep or ctx_compose.",
27 json!({
28 "type": "object",
29 "properties": {
30 "action": {
31 "type": "string",
32 "description": "build|related|symbol|impact|status|enrich|context|diagram|neighbors|path|explain|diff"
33 },
34 "path": {
35 "type": "string",
36 "description": "Path; file::symbol for symbol action"
37 },
38 "to": { "type": "string", "description": "Target file (action=path)" },
39 "depth": { "type": "integer", "description": "Traversal depth" },
40 "kind": { "type": "string", "description": "diagram: deps|calls" },
41 "format": { "type": "string", "description": "text|json" },
42 "since": { "type": "string", "description": "Git ref for action=diff (default HEAD~1)" },
43 "project_root": { "type": "string", "description": "Project root" }
44 },
45 "required": ["action"]
46 }),
47 )
48 }
49
50 fn handle(
51 &self,
52 args: &Map<String, Value>,
53 ctx: &ToolContext,
54 ) -> Result<ToolOutput, ErrorData> {
55 let action = get_str(args, "action")
56 .ok_or_else(|| ErrorData::invalid_params("action is required", None))?;
57
58 let path = if action == "diagram" {
59 get_str(args, "path")
60 } else if let Some(p) = ctx.resolved_path("path") {
61 Some(p.to_string())
62 } else if let Some(err) = ctx
63 .path_error("path")
64 .filter(|_| get_str(args, "path").is_some())
65 {
66 return Err(ErrorData::invalid_params(format!("path: {err}"), None));
67 } else {
68 None
69 };
70
71 let root = if let Some(p) = ctx.resolved_path("project_root") {
72 p.to_string()
73 } else if let Some(err) = ctx.path_error("project_root") {
74 return Err(ErrorData::invalid_params(
75 format!("project_root: {err}"),
76 None,
77 ));
78 } else {
79 ctx.project_root.clone()
80 };
81 let depth = get_usize(args, "depth").map(|d| d.min(64));
82 let kind = get_str(args, "kind");
83 let format = get_str(args, "format");
84 let since = get_str(args, "since");
86 let to = if let Some(p) = ctx.resolved_path("to") {
87 Some(p.to_string())
88 } else if let Some(err) = ctx
89 .path_error("to")
90 .filter(|_| get_str(args, "to").is_some())
91 {
92 return Err(ErrorData::invalid_params(format!("to: {err}"), None));
93 } else {
94 None
95 };
96
97 let cache = ctx
98 .cache
99 .as_ref()
100 .ok_or_else(|| ErrorData::internal_error("cache not available", None))?;
101 let Some(mut guard) = crate::server::bounded_lock::write(cache, "ctx_graph") else {
102 return Ok(ToolOutput::simple(
103 "[graph cache temporarily unavailable — retry in a moment]".to_string(),
104 ));
105 };
106 let result = crate::tools::ctx_graph::handle(
107 &action,
108 path.as_deref(),
109 &root,
110 &mut guard,
111 ctx.crp_mode,
112 depth,
113 kind.as_deref(),
114 to.as_deref(),
115 format.as_deref(),
116 since.as_deref(),
117 );
118
119 Ok(ToolOutput {
120 text: result,
121 original_tokens: 0,
122 saved_tokens: 0,
123 mode: Some(action),
124 path: None,
125 changed: false,
126 shell_outcome: None,
127 })
128 }
129}