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 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}