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