Codebase refactoring as MCP tools. Move modules, rename symbols, validate imports — across languages, preview-first.
claude --plugin-dir ~/dev/refactoryInstead of manually hunting down every import after moving a file, Claude calls move_module and gets it done in one tool call.
Refactory exposes four shared MCP tools plus Python-only Rope extras that Claude Code can call directly during refactoring sessions:
| Tool | Description |
|---|---|
move_module |
Move a file to a new location, rewrite all imports pointing to it, and optionally overwrite an existing target |
move_symbol |
Extract a function, class, or variable to another module, update all references, and fail closed on unsafe moves |
rename_symbol |
Rename an identifier across the entire codebase, including parameters when you disambiguate with line / column if needed |
validate_imports |
Run a narrow refactor-safety scan for broken imports and unresolved exported names after restructuring |
Python-only tools:
| Tool | Description |
|---|---|
organize_imports |
Organize imports in a Python module using Rope |
extract_variable |
Extract a selected Python expression into a variable |
extract_function |
Extract selected Python statements into a function |
inline_symbol |
Inline a selected Python local variable, parameter, or function |
Mutating tools preview by default. Claude must pass apply: true to write files. Python previews use Rope's native change descriptions instead of copying the whole repo to a temporary project.
When you ask Claude to move src/utils.py to src/core/utils.py without tooling, it:
- Reads dozens of files to find imports
- Edits each one manually, spending tokens on boilerplate
- Can miss dynamic imports, re-exports, or barrel files
With refactory, Claude calls one tool:
move_module("src/utils.py", "src/core/utils.py")
→ { success: true, affected_files: ["src/api.py", "src/db.py", "tests/test_utils.py", ...] }
Batch operations run in parallel — move three modules in the same turn.
Python — via Rope
- AST-based symbol location (handles decorators, nested classes)
- Automatic
__init__.pycreation in target directories - Circular import detection before
move_symbol - Rope's refactoring engines rewrite relative and absolute imports
TypeScript — via ts-morph
- Respects
tsconfig.jsonpath aliases - Updates ES6 and CommonJS imports
- Handles barrel files and re-exports
- Monorepo-aware
Language is detected from file extension. Both backends validate that resolved paths stay within the project root.
claude --plugin-dir ~/dev/refactoryDependencies (rope, ts-morph) are auto-installed on first session start via the SessionStart hook.
Manual install if needed:
pip install rope mcp
cd server/tsmorph && pnpm installClaude calls tools directly during a session. Typical flow:
# Preview a move
move_module("src/utils.py", "src/core/utils.py")
→ shows diff: which files change, which imports get rewritten, plus "call again with apply: true"
# Apply it
move_module("src/utils.py", "src/core/utils.py", apply=True)
→ { success: true, affected_files: ["src/api.py", "tests/test_utils.py"] }
# Replace an existing destination explicitly
move_module("src/db.py", "src/storage/db.py", overwrite=True, apply=True)
# Rename an ambiguous parameter by declaration location
rename_symbol("src/api.py", "name", "account_name", line=12, column=19, apply=True)
# Batch: move multiple modules in one turn (parallel tool calls)
move_module("src/db.py", "src/storage/db.py", apply=True)
move_module("src/cache.py", "src/storage/cache.py", apply=True)
rename_symbol("src/api.py", "getData", "fetchData", apply=True)
# Verify nothing broke
validate_imports("src/")
For complex multi-step reorganizations, use the /refactor command to invoke the refactor-planner agent — it analyzes the codebase structure, drafts a preview plan, and executes stages in order.
server/
├── main.py MCP server, tool routing, language detection
├── backends/
│ ├── python.py Rope-based Python refactoring
│ └── typescript.py ts-morph subprocess wrapper
└── tsmorph/
└── refactor.js Node.js script for TypeScript operations
hooks/
└── hooks.json SessionStart: auto-install dependencies
commands/
└── refactor.md /refactor — invokes refactor-planner agent
skills/
└── refactoring/ Refactoring patterns knowledge base
Tests use hermetic fixtures — each test gets an isolated temporary project, so operations can't interfere with each other.
./scripts/test.sh # all tests
./scripts/test.sh -k python # Python backend only
./scripts/test.sh -k "preview or dry_run" # preview mode only
./scripts/test.sh -v # verboseTest coverage: move_module, move_symbol, rename_symbol, validate_imports — plus preview/apply mode, error cases, and path escaping protection.
validate_importsis a refactor-safety probe, not a full compiler or linter run. It stays conservative when a missing exported name cannot be proven statically. The Python scan honors.gitignoreinside git repos (viagit ls-files) and skips standard build/venv/cache/worktree paths elsewhere, so ignored and vendored code is never flagged.move_symbolnow fails closed on ambiguous or destructive cases such as same-file moves, target binding collisions, multi-declarator variables, and source-local dependencies that are not safely importable after the move.- Python
move_moduleandmove_symbolfail closed on two known Rope hazards rather than silently corrupt consumer files:- Basename collision — if the source file exports a top-level binding with the same name as the module (e.g.
project_service = ProjectService()insideproject_service.py), Rope confuses variable attribute access with module attribute access and rewritesproject_service.method()call sites incorrectly. Rename the binding first, then retry. - Lazy (in-function) imports — Rope hoists in-function
importstatements to module top, breaking circular-import workarounds. If any consumer imports the moved module/symbol from inside a function, refactory refuses and lists every offending site. Un-lazy those imports (or resolve the underlying circular dependency) first.
- Basename collision — if the source file exports a top-level binding with the same name as the module (e.g.
- Writes are transactional to the extent the filesystem allows. The TypeScript backend computes every change in memory (preview and apply share that one plan, so previews list dynamic
import()consumers too), refuses if the result would contain a syntax error, stages temp sibling files first — a staging failure changes nothing — then renames them into place with in-memory backups it restores if a later step fails. The Python backend snapshots every file a Rope change touches and restores those bytes when the change fails mid-apply, then undoes previously applied changes. A power loss mid-commit can still leave a partial state; version control remains the last line of defense. - Moves between language families (
.ts/.tsxto.js/.jsxor back) are refused, because type annotations do not survive the move. Files under build/cache directories (dist,build,.next, ...) are never written, even when a broad tsconfigincludepulls them into the project. - Known gaps: CommonJS
require()resolution handles relative specifiers only (path aliases and NodeNext.js-to-.tsmapping are not resolved for require calls);jsconfig.json-only projects fall back to a filesystem crawl; line/column selectors count UTF-8 bytes, so a non-ASCII character earlier on the same line shifts the column. - All MCP tools require an explicit absolute
project_root. In linked worktrees, pass the worker checkout's import root directly (for example.../worker/backend) so cwd drift cannot silently select the main checkout. - Python
move_symbolpreviews require the target module to already exist on disk. Rope cannot compute an exact import-rewrite preview against a module it cannot read, and refactory refuses to fabricate or stage one. Create the destination file first (an empty file is fine) or run withapply: true. move_symbolis conservative across barrel / re-export boundaries. If the source is a pure re-export (export { X } from "./impl") or if callers reach the symbol through a re-export chain, refactory refuses rather than guess — an honest refusal beats a silent corruption. A hand-rolled bash/ast-grep chain remains the right tool for those refactors.