lean_ctx/tools/registered/
ctx_impact.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 CtxImpactTool;
9
10impl McpTool for CtxImpactTool {
11 fn name(&self) -> &'static str {
12 "ctx_impact"
13 }
14
15 fn tool_def(&self) -> Tool {
16 tool_def(
17 "ctx_impact",
18 "Change impact analysis — assess blast radius before refactoring.\n\
19 action=analyze path=\"file.rs\" maps downstream dependents; action=diff compares git refs;\n\
20 action=chain traces from→to dependency paths. depth controls traversal (default 5).",
21 json!({
22 "type": "object",
23 "properties": {
24 "action": {
25 "type": "string",
26 "enum": ["analyze", "diff", "chain", "build", "update", "status"],
27 "description": "analyze|diff|chain|build|update|status"
28 },
29 "path": {
30 "type": "string",
31 "description": "File path or type name"
32 },
33 "root": {
34 "type": "string",
35 "description": "Project root"
36 },
37 "depth": {
38 "type": "integer",
39 "description": "Max depth (default: 5)"
40 },
41 "format": {
42 "type": "string",
43 "description": "Output format"
44 }
45 },
46 "allOf": [
47 {
48 "if": {
49 "properties": { "action": { "const": "analyze" } },
50 "required": ["action"]
51 },
52 "then": {
53 "required": ["action", "path"]
54 }
55 },
56 {
57 "if": {
58 "properties": { "action": { "const": "chain" } },
59 "required": ["action"]
60 },
61 "then": {
62 "required": ["action", "path"]
63 }
64 }
65 ]
66 }),
67 )
68 }
69
70 fn handle(
71 &self,
72 args: &Map<String, Value>,
73 ctx: &ToolContext,
74 ) -> Result<ToolOutput, ErrorData> {
75 let action = get_str(args, "action").unwrap_or_else(|| "analyze".to_string());
76 let path = get_str(args, "path");
77 let depth = get_usize(args, "depth").map(|d| d.min(64));
78 let format = get_str(args, "format");
79 let root = if let Some(p) = ctx
80 .resolved_path("root")
81 .or(ctx.resolved_path("project_root"))
82 {
83 p
84 } else if let Some(err) = ctx.path_error("root").or(ctx.path_error("project_root")) {
85 return Err(ErrorData::invalid_params(format!("root: {err}"), None));
86 } else {
87 &ctx.project_root
88 };
89
90 let result = crate::tools::ctx_impact::handle(
91 &action,
92 path.as_deref(),
93 root,
94 depth,
95 format.as_deref(),
96 );
97
98 Ok(ToolOutput::simple(result))
99 }
100}