lean_ctx/tools/registered/
ctx_callgraph.rs1use crate::core::ocla::cache_types::{CacheKeyBuilder, SearchQueryKey};
2use rmcp::ErrorData;
3use rmcp::model::Tool;
4use serde_json::{Map, Value, json};
5
6use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_int, get_str};
7use crate::tool_defs::tool_def;
8
9pub struct CtxCallgraphTool;
10
11impl McpTool for CtxCallgraphTool {
12 fn name(&self) -> &'static str {
13 "ctx_callgraph"
14 }
15
16 fn tool_def(&self) -> Tool {
17 tool_def(
18 "ctx_callgraph",
19 "Callers/callees for one symbol (function call edges, not const/var refs).\n\
20 action=callers|callees symbol='fn' → every call site with file:line.\n\
21 action=trace from→to finds the path between two symbols (depth=N).\n\
22 For end-to-end flow understanding use ctx_compose FIRST.",
23 json!({
24 "type": "object",
25 "properties": {
26 "action": {
27 "type": "string",
28 "enum": ["callers", "callees", "trace", "risk"]
29 },
30 "symbol": { "type": "string" },
31 "file": { "type": "string", "description": "Scope results to file" },
32 "depth": { "type": "integer", "minimum": 1, "maximum": 5 },
33 "from": { "type": "string" },
34 "to": { "type": "string" }
35 },
36 "required": ["action"],
37 "allOf": [
38 { "if": { "properties": { "action": { "enum": ["callers", "callees", "risk"] } }, "required": ["action"] }, "then": { "required": ["symbol"] } },
39 { "if": { "properties": { "action": { "const": "trace" } }, "required": ["action"] }, "then": { "required": ["from", "to"] } }
40 ]
41 }),
42 )
43 }
44
45 fn handle(
46 &self,
47 args: &Map<String, Value>,
48 ctx: &ToolContext,
49 ) -> Result<ToolOutput, ErrorData> {
50 let action = get_str(args, "action").unwrap_or_else(|| "callers".to_string());
51
52 let action_normalized = match action.to_lowercase().as_str() {
53 "callers" | "caller" => "callers",
54 "callees" | "callee" => "callees",
55 "trace" => "trace",
56 "risk" => "risk",
57 _ => action.as_str(),
58 }
59 .to_string();
60
61 let symbol = get_str(args, "symbol");
62 let file = get_str(args, "file");
63 let depth = get_int(args, "depth").unwrap_or(1).clamp(1, 5) as usize;
64 let from = get_str(args, "from");
65 let to = get_str(args, "to");
66
67 let cache_input = format!(
68 "callgraph:{action_normalized}:{}:{}:{depth}",
69 symbol.as_deref().unwrap_or(""),
70 file.as_deref().unwrap_or("")
71 );
72 let builder = SearchQueryKey {
73 path: ctx.project_root.clone(),
74 index_rev: String::new(),
75 pattern: cache_input,
76 include: String::new(),
77 exclude: String::new(),
78 };
79 let key = builder.cache_key();
80 let validator = builder.validator();
81
82 if let Some(entry) =
83 crate::core::ocla::cache_delivery::check(&key, &validator, "ctx_callgraph")
84 {
85 let stub = crate::core::ocla::cache_delivery::stub(&entry, "callgraph");
86 return Ok(ToolOutput {
87 text: stub,
88 original_tokens: entry.token_count as usize,
89 saved_tokens: entry.token_count as usize,
90 mode: Some(action_normalized),
91 path: None,
92 changed: false,
93 shell_outcome: None,
94 content_blocks: None,
95 });
96 }
97
98 let result = crate::tools::ctx_callgraph::handle(
99 &action_normalized,
100 symbol.as_deref(),
101 file.as_deref(),
102 &ctx.project_root,
103 depth,
104 from.as_deref(),
105 to.as_deref(),
106 );
107
108 crate::core::ocla::cache_delivery::record(
109 key,
110 crate::core::ocla::cache_types::DeliveryKind::SearchQuery,
111 validator,
112 None,
113 &result,
114 "ctx_callgraph",
115 );
116
117 Ok(ToolOutput {
118 text: result,
119 original_tokens: 0,
120 saved_tokens: 0,
121 mode: Some(action_normalized),
122 path: None,
123 changed: false,
124 shell_outcome: None,
125 content_blocks: None,
126 })
127 }
128}