Skip to main content

lean_ctx/tools/registered/
ctx_refactor.rs

1use 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        // name_path edits resolve their own path; only require/resolve `path`
82        // when actually provided (read actions + position-fallback edits).
83        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}