LLMはリネームが苦手?MCP+ts-morphで補えるかも?
私がメインで担当しているプロジェクトでは、コンポーネントや関数の命名規則、ファイル構成に一貫性がなく、恥ずかしながらリファクタリングが追いついていない状態にあります。
作った時期やメンバーによっても違いがあり、スクリプトによるリファクタリングも難しい状態にあります。
このような無秩序なフォルダ構成に方向性を与えて、完成形を考えさせるのにLLMのエージェントは大変便利なように思えます。
このファイルはあの名前にする、このフォルダにする、プランを最初に決めて、機械的に実行させれば上手くいきそうではないでしょうか?
LLMはリネーム作業が苦手?
ところが、これがどうにも全く上手くいかない。
移動対象のファイルだけなら問題なく編集できるが、それの影響箇所の修正するのがとても下手なのです。
人の手を使えばIDEの機能を利用して「シンボルのリネーム」などを行うことができれば、簡単にリファクタリングを行えるのですが、エージェントはその機能を使う事ができません。
影響範囲を見つけるには、プロジェクト全体を把握しなければいけませんし、それだけの量のコードを全てコンテキストにいれることはできません。
ではどうするか?MCPを作りましょう!
ts-morphとMCPを使ってシンボルのリネームを行う
ts-morphはTypescriptのプロジェクトを解析し、その構造を操作するためのライブラリです。
機械的なリファクタリングやメタプログラミングを行うのに便利です。
提供している機能の中にIdentifierノードをリネームする機能があります。この機能をMCPで呼び出せるようにすることを考えて実装を行います。
必要なパラメータ:
-
tsconfigPath
: プロジェクトルートにあるtsconfig.json
へのパス。 -
filePath
: リネームしたいシンボルを含むファイルのパス。 -
oldName
: リネーム前のシンボル名。 -
newName
: リネーム後のシンボル名。 -
position
(オプション): 対象シンボルの正確な位置 (行・列)。同じファイル内に同名シンボルがある場合に特定するため。 -
symbolKind
(オプション): シンボルの種類 (検証用)
コード例 (主要部分):
import { Project, SyntaxKind, Identifier } from "ts-morph";
// MCPから渡されるパラメータの型定義
interface RenameParams {
tsconfigPath: string; // tsconfig.jsonへのパス (必須)
filePath: string; // 対象ファイルパス
oldName: string; // 変更前の名前
newName: string; // 変更後の名前
position?: { line: number; character: number }; // 位置情報 (オプション)
dryRun?: boolean; // 実際に変更を加えるかどうか (オプション)
symbolKind?: string; // シンボルの種類 (オプション、検証用)
}
// リネーム処理を実行する関数(実運用時はエラーハンドリングや symbolKind 検証などをもっと考慮)
export async function renameSymbolViaMCP(params: RenameParams): Promise<string[]> {
// 1. tsconfig.json を指定して Project インスタンスを作成
const project = new Project({
tsConfigFilePath: params.tsconfigPath,
// 必要に応じて他のオプション (skipAddingFilesFromTsConfigなど) を追加
});
// 2. 対象ファイルを取得 (tsconfigに基づいてファイルは読み込まれる想定)
const sourceFile = project.getSourceFile(params.filePath);
if (!sourceFile) {
// tsconfigに含まれていないか、パスが間違っている可能性
throw new Error(`File not found or not included in tsconfig: ${params.filePath}`);
}
// 3. リネーム対象の Identifier ノードを特定
// (位置情報があれば優先、なければ名前でファイル内を検索)
// (実運用ではここで params.symbolKind を使った検証も推奨)
let targetIdentifier: Identifier | undefined;
if (params.position) {
try {
const positionOffset = sourceFile.compilerNode.getPositionOfLineAndCharacter(
params.position.line - 1,
params.position.character // ts-morphの列は0-basedのことが多いが、ツールによるかも
);
const node = sourceFile.getDescendantAtPos(positionOffset);
if (node?.getKind() === SyntaxKind.Identifier && node.getText() === params.oldName) {
targetIdentifier = node as Identifier;
}
} catch (e) {
console.warn(`Position resolution error: ${e instanceof Error ? e.message : e}`);
}
}
if (!targetIdentifier) {
targetIdentifier = sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)
.find(id => id.getText() === params.oldName);
}
if (!targetIdentifier) {
throw new Error(`Symbol "${params.oldName}" not found in ${params.filePath}.`);
}
// 4. rename() メソッドでリネーム実行 (ts-morphが参照箇所を解決)
targetIdentifier.rename(params.newName);
// 5. 変更されたファイルパスのリストを取得 (isSaved()が false = 未保存の変更あり)
const changedFilePaths = project.getSourceFiles()
.filter(f => !f.isSaved())
.map(f => f.getFilePath());
// 6. dryRun でなければ変更をファイルシステムに保存
if (!params.dryRun) {
await project.save();
}
return changedFilePaths;
}
影響のあるファイルパスを返すことで、LLMがリネームの影響範囲を把握しやすくなります。
MCP toolへのつなぎ込み
作成したメソッドをラップするMCPのツールを作成します。
特に珍しいことはしていませんが、https://github.com/awslabs/mcp を参考にプロンプトを作成しました。
ちょっと工夫した点
- descriptionを英語にしておく(翻訳はLLMに任せ)
- パラメーターで渡すパスは絶対パスを渡すようにしておく
- 影響を受けたファイル一覧を返却し、エージェントが変更を把握できるようにする
- dryRunをパラメーターに追加し、実際に変更を加えるかどうかを選択できるようにする
/**
* ts-morph を利用したリファクタリングツール群を MCP サーバーに登録する
*/
export function registerTsMorphTools(server: McpServer): void {
server.tool(
"rename_symbol_by_tsmorph",
`[Uses ts-morph] Renames TypeScript/JavaScript symbols across the project.
Analyzes the AST (Abstract Syntax Tree) to track and update references
throughout the project, not just the definition site.
Useful for cross-file refactoring tasks during Vibe Coding.
## Usage
Use this tool, for example, when you change a function name defined in one file
and want to reflect that change in other files that import and use it.
ts-morph parses the project based on \`tsconfig.json\` to resolve symbol references
and perform the rename.
1. Specify the exact location (file path, line, column) of the symbol
(function name, variable name, class name, etc.) you want to rename.
This is necessary for ts-morph to identify the target Identifier node in the AST.
2. Specify the current symbol name and the new symbol name.
3. Specify the symbol kind (\`function\`, \`variable\`, \`class\`).
This allows additional validation to ensure the node identified by ts-morph
is of the expected type (e.g., an Identifier within a FunctionDeclaration),
preventing unintended renames.
4. It's recommended to first run with \`dryRun: true\` to check which files
ts-morph will modify.
5. If the preview looks correct, run with \`dryRun: false\` (or omit it)
to actually save the changes to the file system.
## Parameters
- tsconfigPath (string, required): Path to the project's root \`tsconfig.json\` file.
Essential for ts-morph to correctly parse the project structure and file references. **Must be an absolute path (relative paths can be misinterpreted).**
- targetFilePath (string, required): Path to the file where the symbol to be renamed
is defined (or first appears). **Must be an absolute path (relative paths can be misinterpreted).**
- position (object, required): The exact position on the symbol to be renamed.
Serves as the starting point for ts-morph to locate the AST node.
- line (number, required): 1-based line number, typically obtained from an editor.
- column (number, required): 1-based column number (position of the first character
of the symbol name), typically obtained from an editor.
- symbolName (string, required): The current name of the symbol before renaming.
Used to verify against the node name found at the specified position.
- newName (string, required): The new name for the symbol after renaming.
- symbolKind (string, required): The kind of the symbol ("function", "variable", "class", etc.).
Used to verify the type of the target by checking the kind of the parent node
identified by ts-morph (e.g., FunctionDeclaration).
- dryRun (boolean, optional): If set to true, prevents ts-morph from making and saving
file changes, returning only the list of files that would be affected.
Useful for verification. Defaults to false.
## Result
- On success: Returns a message containing the list of file paths modified
(or scheduled to be modified if dryRun) by the rename.
- On failure: Returns a message indicating the error. `,
// パラメータの型定義はもっと厳密にすることもできると思う
{
tsconfigPath: z
.string()
targetFilePath: z
.string()
position: z
.object({
line: z.number().describe("1-based line number."),
column: z.number().describe("1-based column number."),
})
symbolName: z.string()
newName: z.string()
symbolKind: z
.string()
dryRun: z
.boolean()
.optional()
.default(false)
},
async (args) => {
// Zod で型チェック済みの引数を分割代入
const {
tsconfigPath,
targetFilePath,
position,
symbolName,
newName,
symbolKind,
dryRun,
} = args;
try {
const result = await renameSymbolViaMCP({
// 受け取ったパスをそのまま渡す
tsconfigPath: tsconfigPath,
targetFilePath: targetFilePath,
position: position,
symbolName: symbolName,
newName: newName,
symbolKind: symbolKind,
dryRun: dryRun,
});
const changedFilesList =
result.changedFiles.length > 0
? result.changedFiles.join("\n - ")
: "(変更なし)";
let message = "";
if (dryRun) {
message = `Dry run 完了: シンボル '${symbolName}' を '${newName}' にリネームすると、以下のファイルが変更される予定です:\n - ${changedFilesList}`;
} else {
message = `リネーム成功: シンボル '${symbolName}' を '${newName}' にリネームし、以下のファイルが変更されました:\n - ${changedFilesList}`;
}
return {
content: [{ type: "text", text: message }],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error("[renameSymbol Tool Error]:", error);
return {
content: [
{
type: "text",
text: `リネーム処理中にエラーが発生しました: ${errorMessage}`,
},
],
};
}
},
);
}
まとめ&動作確認
作成したリポジトリ
実際に動かしたときのスクリーンショットがこちらになります。ツールの利用方法とパラメーターの説明が適切であれば、特に躓くことなく、エージェントが作成したツールを活用してくれるようになりました。
追記内容(2025-04-29)
この記事はMCPの試作完成直後にワーッと勢いで書いた記事なのですが、さらに2日間ほど動かしたところ、相応に実用性の高いMCPツールになりました。
記事の冒頭であったファイル名だけだと不足感があったので、以下の様な機能を更に追加しています。
## シンボル名の変更 (rename_symbol_by_tsmorph)
機能: 指定されたファイル内の特定の位置にあるシンボル (関数、変数、クラス、インターフェースなど) の名前を、プロジェクト全体で一括変更します。
ユースケース: 関数名や変数名を変更したいが、参照箇所が多く手作業での変更が困難な場合。
必要な情報: プロジェクトの tsconfig.json パス、対象ファイルのパス、シンボルの位置 (行・列)、現在のシンボル名、新しいシンボル名、シンボルの種類。
## ファイル/フォルダ名の変更 (rename_filesystem_entry_by_tsmorph)
機能: 指定されたファイルまたはフォルダの名前を変更し、プロジェクト内のすべての import/export 文のパスを自動的に更新します。
ユースケース: ファイル構成を変更し、それに伴って import パスを修正したい場合。
必要な情報: プロジェクトの tsconfig.json パス、変更前のパス、変更後のパス。
注意: パスエイリアスや相対的なインデックスインポートの更新は不完全な場合があります。変更後に手動確認が必要な場合があります。"." や ".." 、@/*等のパスで import している場合、更新されないことがあります。
##参照箇所の検索 (find_references_by_tsmorph)
機能: 指定されたファイル内の特定の位置にあるシンボルの定義箇所と、プロジェクト全体でのすべての参照箇所を検索して一覧表示します。
ユースケース: ある関数や変数がどこで使われているかを把握したい場合。リファクタリングの影響範囲を調査したい場合。
必要な情報: プロジェクトの tsconfig.json パス、対象ファイルのパス、シンボルの位置 (行・列)。
##パスエイリアスの削除 (remove_path_alias_by_tsmorph)
機能: 指定されたファイルまたはディレクトリ内の import/export 文に含まれるパスエイリアス (@/components など) を、相対パス (../../components など) に置換します。
ユースケース: プロジェクトの移植性を高めたい場合や、特定のコーディング規約に合わせたい場合。
必要な情報: プロジェクトの tsconfig.json パス、処理対象のファイルまたはディレクトリのパス。
Discussion