Skip to main content

lean_ctx/tools/registered/
ctx_refactor.rs

1use rmcp::ErrorData;
2use rmcp::model::Tool;
3use serde_json::{Map, Value, json};
4
5use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_str, require_resolved_path};
6use crate::tool_defs::tool_def;
7
8pub struct CtxRefactorTool;
9
10impl McpTool for CtxRefactorTool {
11    fn name(&self) -> &'static str {
12        "ctx_refactor"
13    }
14
15    fn tool_def(&self) -> Tool {
16        tool_def(
17            "ctx_refactor",
18            "Rename, move, safe_delete, inline, read-only analyses via LSP/IDE.\n\
19             WORKFLOW: use action=references first to find usages before refactoring.\n\
20             ANTIPATTERN: not for symbol discovery — use ctx_symbol/ctx_compose.\n\
21             Single-phase edits (replace_symbol_body, reformat) work headless via name_path.\n\
22             Two-phase ops (_preview+_apply) need JetBrains IDE (else BACKEND_REQUIRED).\n\
23             Conflicts blocked unless force=true. See `action` parameter for full list.",
24            json!({
25                "type": "object",
26                "properties": {
27                    "action": {
28                        "type": "string",
29                        "description": "rename|references|definition|implementations|declaration|type_hierarchy|symbols_overview|inspections|replace_symbol_body|insert_before_symbol|insert_after_symbol|rename_preview|rename_apply|move_preview|move_apply|safe_delete_preview|safe_delete_apply|inline_preview|inline_apply|reformat"
30                    },
31                    "path": { "type": "string", "description": "Path" },
32                    "line": { "type": "integer", "description": "1-indexed line" },
33                    "column": { "type": "integer", "description": "0-indexed column" },
34                    "new_name": { "type": "string", "description": "New symbol name" },
35                    "scope": {
36                        "type": "string",
37                        "enum": ["project", "all"],
38                        "description": "project|all"
39                    },
40                    "direction": {
41                        "type": "string",
42                        "enum": ["supertypes", "subtypes"],
43                        "description": "supertypes|subtypes"
44                    },
45                    "mode": {
46                        "type": "string",
47                        "enum": ["run", "list"],
48                        "description": "run|list"
49                    },
50                    "name_path": { "type": "string", "description": "Symbol path for body edits (qualified or bare)" },
51                    "file": { "type": "string", "description": "Scope name_path resolution to this file (avoids AMBIGUOUS_SYMBOL for common names)" },
52                    "new_text": { "type": "string", "description": "Full replacement declaration text (replace_symbol_body)" },
53                    "text": { "type": "string", "description": "Sibling text to insert (auto-indented)" },
54                    "end_line": { "type": "integer", "description": "1-based last line (path+line fallback)" },
55                    "expected_hash": { "type": "string", "description": "BLAKE3 hex of current range (TOCTOU guard)" },
56                    "plan_hash": { "type": "string", "description": "BLAKE3 plan hash from rename_preview" },
57                    "force": { "type": "boolean", "description": "Override refactoring conflicts" },
58                    "search_comments": { "type": "boolean", "description": "Rename in comments/strings" },
59                    "search_text_occurrences": { "type": "boolean", "description": "Rename in non-code text" },
60                    "target_path": { "type": "string", "description": "Destination directory/file (project-relative)" },
61                    "target_parent": { "type": "string", "description": "Destination parent symbol for member move" },
62                    "propagate": { "type": "boolean", "description": "Delete unreferenced dependencies" },
63                    "keep_definition": { "type": "boolean", "description": "Keep declaration after inline" },
64                    "optimize_imports": { "type": "boolean", "description": "Remove unused imports" }
65                },
66                "allOf": [
67                    {
68                        "if": { "properties": { "action": { "const": "rename" } }, "required": ["action"] },
69                        "then": { "required": ["action", "path", "new_name"] }
70                    },
71                    {
72                        "if": { "properties": { "action": { "const": "references" } }, "required": ["action"] },
73                        "then": { "required": ["action", "path"] }
74                    },
75                    {
76                        "if": { "properties": { "action": { "const": "definition" } }, "required": ["action"] },
77                        "then": { "required": ["action", "path"] }
78                    },
79                    {
80                        "if": { "properties": { "action": { "const": "implementations" } }, "required": ["action"] },
81                        "then": { "required": ["action", "path"] }
82                    },
83                    {
84                        "if": { "properties": { "action": { "const": "declaration" } }, "required": ["action"] },
85                        "then": { "required": ["action", "path"] }
86                    },
87                    {
88                        "if": { "properties": { "action": { "const": "type_hierarchy" } }, "required": ["action"] },
89                        "then": { "required": ["action", "path"] }
90                    },
91                    {
92                        "if": { "properties": { "action": { "const": "symbols_overview" } }, "required": ["action"] },
93                        "then": { "required": ["action", "path"] }
94                    },
95                    {
96                        "if": { "properties": { "action": { "const": "inspections" } }, "required": ["action"] },
97                        "then": { "required": ["action", "path"] }
98                    },
99                    {
100                        "if": { "properties": { "action": { "const": "replace_symbol_body" } }, "required": ["action"] },
101                        "then": { "required": ["action", "new_text"] }
102                    },
103                    {
104                        "if": { "properties": { "action": { "const": "insert_before_symbol" } }, "required": ["action"] },
105                        "then": { "required": ["action", "text"] }
106                    },
107                    {
108                        "if": { "properties": { "action": { "const": "insert_after_symbol" } }, "required": ["action"] },
109                        "then": { "required": ["action", "text"] }
110                    },
111                    {
112                        "if": { "properties": { "action": { "const": "rename_preview" } }, "required": ["action"] },
113                        "then": { "required": ["action", "new_name"] }
114                    },
115                    {
116                        "if": { "properties": { "action": { "const": "rename_apply" } }, "required": ["action"] },
117                        "then": { "required": ["action", "new_name", "plan_hash"] }
118                    },
119                    {
120                        "if": { "properties": { "action": { "const": "safe_delete_apply" } }, "required": ["action"] },
121                        "then": { "required": ["action", "plan_hash"] }
122                    },
123                    {
124                        "if": { "properties": { "action": { "const": "move_apply" } }, "required": ["action"] },
125                        "then": { "required": ["action", "plan_hash"] }
126                    },
127                    {
128                        "if": { "properties": { "action": { "const": "inline_apply" } }, "required": ["action"] },
129                        "then": { "required": ["action", "plan_hash"] }
130                    }
131                ],
132                "required": ["action"]
133            }),
134        )
135    }
136
137    fn handle(
138        &self,
139        args: &Map<String, Value>,
140        ctx: &ToolContext,
141    ) -> Result<ToolOutput, ErrorData> {
142        // name_path edits resolve their own path; only require/resolve `path`
143        // when actually provided (read actions + position-fallback edits).
144        let has_path = args.get("path").and_then(Value::as_str).is_some();
145        let abs_path = if has_path {
146            require_resolved_path(ctx, args, "path")?
147        } else {
148            String::new()
149        };
150
151        let args_value = Value::Object(args.clone());
152        let result = crate::tools::ctx_refactor::handle(&args_value, &ctx.project_root, &abs_path);
153
154        let action = get_str(args, "action").unwrap_or_default();
155        Ok(ToolOutput {
156            text: result,
157            original_tokens: 0,
158            saved_tokens: 0,
159            mode: Some(action.clone()),
160            path: get_str(args, "path"),
161            changed: matches!(
162                action.as_str(),
163                "replace_symbol_body"
164                    | "insert_before_symbol"
165                    | "insert_after_symbol"
166                    | "rename_apply"
167                    | "move_apply"
168                    | "safe_delete_apply"
169                    | "inline_apply"
170                    | "reformat"
171            ),
172            shell_outcome: None,
173            content_blocks: None,
174        })
175    }
176}
177
178#[cfg(test)]
179mod schema_tests {
180    use super::*;
181    use crate::server::tool_trait::McpTool;
182
183    #[test]
184    fn schema_advertises_declaration_and_scope() {
185        let tool = CtxRefactorTool;
186        let def = tool.tool_def();
187        let schema = serde_json::to_string(&def).unwrap();
188        for needle in [
189            "declaration",
190            "\"scope\"",
191            "type_hierarchy",
192            "symbols_overview",
193            "\"direction\"",
194            "supertypes",
195            "subtypes",
196            "inspections",
197            "\"mode\"",
198            "replace_symbol_body",
199            "insert_before_symbol",
200            "insert_after_symbol",
201            "name_path",
202            "new_text",
203            "expected_hash",
204            "rename_preview",
205            "rename_apply",
206            "plan_hash",
207            "force",
208            "search_comments",
209            "search_text_occurrences",
210            "move_preview",
211            "move_apply",
212            "safe_delete_preview",
213            "safe_delete_apply",
214            "target_path",
215            "target_parent",
216            "propagate",
217            "inline_preview",
218            "inline_apply",
219            "reformat",
220            "keep_definition",
221            "optimize_imports",
222        ] {
223            assert!(schema.contains(needle), "schema missing {needle}: {schema}");
224        }
225    }
226}