lean_ctx/tools/registered/
ctx_refactor.rs1use rmcp::model::Tool;
2use rmcp::ErrorData;
3use serde_json::{json, Map, Value};
4
5use crate::server::tool_trait::{get_str, require_resolved_path, McpTool, ToolContext, ToolOutput};
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 "LSP/IDE refactoring. action=one pipe-delimited value below. \
19 Reads (references/definition/implementations/declaration/type_hierarchy/\
20 symbols_overview/inspections) need a language server or the JetBrains \
21 backend. Symbol edits (replace/insert_before/insert_after_symbol) are \
22 name_path-addressed, IDE-first with a lossless headless fallback. Two-Phase \
23 ops (rename/move/safe_delete/inline _preview+_apply) need a JetBrains IDE \
24 (else BACKEND_REQUIRED) with a stateless plan_hash TOCTOU guard. \
25 rename/move/safe_delete block conflicts unless force=true; inline cannot be \
26 forced (→ UNSUPPORTED). reformat is Single-Phase, by name_path | path | path+line.",
27 json!({
28 "type": "object",
29 "properties": {
30 "action": {
31 "type": "string",
32 "description": "rename|references|definition|implementations|declaration|type_hierarchy|\
33 symbols_overview|inspections|replace_symbol_body|insert_before_symbol|\
34 insert_after_symbol|rename_preview|rename_apply|move_preview|move_apply|\
35 safe_delete_preview|safe_delete_apply|inline_preview|inline_apply|reformat"
36 },
37 "path": { "type": "string", "description": "File path" },
38 "line": { "type": "integer", "description": "1-indexed line number" },
39 "column": { "type": "integer", "description": "0-indexed character offset" },
40 "new_name": { "type": "string", "description": "New name (only for rename action)" },
41 "scope": {
42 "type": "string",
43 "enum": ["project", "all"],
44 "description": "Search scope for references/implementations/type_hierarchy (JetBrains backend). 'project' = project sources only (default); 'all' = include libraries/SDK."
45 },
46 "direction": {
47 "type": "string",
48 "enum": ["supertypes", "subtypes"],
49 "description": "type_hierarchy direction (JetBrains backend). 'supertypes' (default) = parents; 'subtypes' = children/implementors."
50 },
51 "mode": {
52 "type": "string",
53 "enum": ["run", "list"],
54 "description": "inspections mode (JetBrains backend). 'run' (default) = diagnostics for the given file; 'list' = enabled inspections of the current project profile."
55 },
56 "name_path": { "type": "string", "description": "Symbol path for body edits: 'Class/method' (qualified) or bare 'name'. Resolved via the symbol index; ambiguous → AMBIGUOUS_SYMBOL with candidates." },
57 "new_body": { "type": "string", "description": "Full replacement declaration text (replace_symbol_body)." },
58 "text": { "type": "string", "description": "Sibling text to insert (insert_before_symbol/insert_after_symbol); indentation is applied automatically." },
59 "end_line": { "type": "integer", "description": "1-based last line of the symbol (only for the path+line fallback when name_path is omitted)." },
60 "expected_hash": { "type": "string", "description": "Optional BLAKE3-hex of the current range content; mismatch → CONFLICT (no blind overwrite)." },
61 "plan_hash": { "type": "string", "description": "Required for rename_apply: the BLAKE3 plan hash returned by rename_preview (stateless TOCTOU guard; mismatch → CONFLICT)." },
62 "force": { "type": "boolean", "description": "rename_apply only: override blocking refactoring conflicts (default false → CONFLICT when conflicts exist)." },
63 "search_comments": { "type": "boolean", "description": "rename: also rename matches inside comments/strings (default false)." },
64 "search_text_occurrences": { "type": "boolean", "description": "rename: also rename non-code text occurrences (default false)." },
65 "target_path": { "type": "string", "description": "move only: destination directory/file (project-relative). Set EXACTLY ONE of target_path/target_parent. Out-of-jail or both/neither set → INVALID_TARGET." },
66 "target_parent": { "type": "string", "description": "move only: destination parent symbol (name_path, e.g. 'OtherClass') for a member move. Set EXACTLY ONE of target_path/target_parent." },
67 "propagate": { "type": "boolean", "description": "safe_delete_apply only: also delete dependencies that become unreferenced (Serena 'propagate', default false)." },
68 "keep_definition": { "type": "boolean", "description": "inline only: inline at all call sites but keep the declaration (default false)." },
69 "optimize_imports": { "type": "boolean", "description": "reformat only: also remove unused imports (default false)." }
70 },
71 "required": ["action"]
72 }),
73 )
74 }
75
76 fn handle(
77 &self,
78 args: &Map<String, Value>,
79 ctx: &ToolContext,
80 ) -> Result<ToolOutput, ErrorData> {
81 let has_path = args.get("path").and_then(Value::as_str).is_some();
84 let abs_path = if has_path {
85 require_resolved_path(ctx, args, "path")?
86 } else {
87 String::new()
88 };
89
90 let args_value = Value::Object(args.clone());
91 let result = crate::tools::ctx_refactor::handle(&args_value, &ctx.project_root, &abs_path);
92
93 let action = get_str(args, "action").unwrap_or_default();
94 Ok(ToolOutput {
95 text: result,
96 original_tokens: 0,
97 saved_tokens: 0,
98 mode: Some(action.clone()),
99 path: get_str(args, "path"),
100 changed: matches!(
101 action.as_str(),
102 "replace_symbol_body"
103 | "insert_before_symbol"
104 | "insert_after_symbol"
105 | "rename_apply"
106 | "move_apply"
107 | "safe_delete_apply"
108 | "inline_apply"
109 | "reformat"
110 ),
111 shell_outcome: None,
112 })
113 }
114}
115
116#[cfg(test)]
117mod schema_tests {
118 use super::*;
119 use crate::server::tool_trait::McpTool;
120
121 #[test]
122 fn schema_advertises_declaration_and_scope() {
123 let tool = CtxRefactorTool;
124 let def = tool.tool_def();
125 let schema = serde_json::to_string(&def).unwrap();
126 for needle in [
127 "declaration",
128 "\"scope\"",
129 "type_hierarchy",
130 "symbols_overview",
131 "\"direction\"",
132 "supertypes",
133 "subtypes",
134 "inspections",
135 "\"mode\"",
136 "replace_symbol_body",
137 "insert_before_symbol",
138 "insert_after_symbol",
139 "name_path",
140 "new_body",
141 "expected_hash",
142 "rename_preview",
143 "rename_apply",
144 "plan_hash",
145 "force",
146 "search_comments",
147 "search_text_occurrences",
148 "move_preview",
149 "move_apply",
150 "safe_delete_preview",
151 "safe_delete_apply",
152 "target_path",
153 "target_parent",
154 "propagate",
155 "inline_preview",
156 "inline_apply",
157 "reformat",
158 "keep_definition",
159 "optimize_imports",
160 ] {
161 assert!(schema.contains(needle), "schema missing {needle}: {schema}");
162 }
163 }
164}