planRenameSymbol
Plan renaming a TypeScript symbol at a specified position (file, line, character) by computing edit suggestions without modifying files. Optionally include strings and comments.
Instructions
Compute edits to rename a TypeScript symbol at a specific position. Returns edit plans without modifying the filesystem.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| projectRoot | No | Optional base directory for resolving relative paths and limiting tsconfig discovery | |
| workspaceRoot | No | Optional monorepo root used to search multiple tsconfig.json files | |
| tsconfigPath | No | Optional explicit tsconfig.json path | |
| filePath | Yes | Absolute path or path relative to projectRoot/workspaceRoot | |
| line | Yes | 0-based line number of the symbol | |
| character | Yes | 0-based character position of the symbol | |
| newName | Yes | The new name for the symbol | |
| findInStrings | No | Whether to find occurrences in strings (default: false) | |
| findInComments | No | Whether to find occurrences in comments (default: false) |
Implementation Reference
- src/tools/planRenameSymbol.ts:25-150 (handler)Main handler function: resolves file path, creates TS service, collects rename locations across workspace, and returns edit plans. Uses TypeScript LanguageService's getRenameInfo and findRenameLocations to compute rename edits without modifying the filesystem.
export function planRenameSymbol( params: PlanRenameSymbolParams ): PlanRenameSymbolResult { const baseDir = path.resolve( params.workspaceRoot ?? params.projectRoot ?? process.cwd() ); const absFilePath = resolveInputPath(params.filePath, baseDir); if (!fs.existsSync(absFilePath)) { if ( params.projectRoot !== undefined || params.workspaceRoot !== undefined || params.tsconfigPath !== undefined ) { try { createTsService({ projectRoot: params.projectRoot, workspaceRoot: params.workspaceRoot, tsconfigPath: params.tsconfigPath, }); } catch (error) { return { canRename: false, reason: error instanceof Error ? error.message : "Failed to create TS service", }; } } return { canRename: false, reason: `File not found: ${absFilePath}`, }; } // 1. 対象ファイルの所属 TS プロジェクトを解決 let primaryContext: ReturnType<typeof createTsService>; try { primaryContext = createTsService({ projectRoot: params.projectRoot, workspaceRoot: params.workspaceRoot, tsconfigPath: params.tsconfigPath, filePath: absFilePath, }); } catch (error) { return { canRename: false, reason: error instanceof Error ? error.message : "Failed to create TS service", }; } // 2. ファイル内容を読み込み const fileText = fs.readFileSync(absFilePath, "utf8"); const sourceFile = primaryContext.tsModule.createSourceFile( absFilePath, fileText, primaryContext.tsModule.ScriptTarget.Latest, true ); // 3. ts.getPositionOfLineAndCharacter で位置を計算 const pos = primaryContext.tsModule.getPositionOfLineAndCharacter( sourceFile, params.line, params.character ); // 4. rename 可能な TS プロジェクトを集めて location をマージ const candidateContexts = collectWorkspaceTsServices([absFilePath], { projectRoot: params.projectRoot, workspaceRoot: params.workspaceRoot, tsconfigPath: params.tsconfigPath, primaryFilePath: absFilePath, }); const editBucket = createEditBucket(); let renameErrorMessage: string | undefined; for (const context of candidateContexts) { const program = context.service.getProgram(); const contextSourceFile = program?.getSourceFile(absFilePath); if (!contextSourceFile) { continue; } const renameInfo = context.service.getRenameInfo(absFilePath, pos); if (!renameInfo.canRename) { renameErrorMessage = renameErrorMessage ?? renameInfo.localizedErrorMessage ?? "Cannot rename this symbol"; continue; } const locations = context.service.findRenameLocations( absFilePath, pos, params.findInStrings ?? false, params.findInComments ?? false, false ) ?? []; addRenameLocations( editBucket, context.tsModule, locations, params.newName ); } const fileTextEdits = toFileTextEdits(editBucket); if (fileTextEdits.length === 0) { return { canRename: false, reason: renameErrorMessage ?? "Cannot rename this symbol", }; } return { canRename: true, edits: fileTextEdits, }; } - src/types.ts:47-72 (schema)Input and output type definitions for planRenameSymbol tool. PlanRenameSymbolParams includes filePath, line, character, newName, and optional findInStrings/findInComments. PlanRenameSymbolResult is a discriminated union: canRename:false (with reason) or canRename:true (with edits array).
* planRenameSymbol の入力パラメータ */ export type PlanRenameSymbolParams = { projectRoot?: string; // 旧互換: 相対パスの解決基準 / ワークスペース境界 workspaceRoot?: string; // monorepo 全体を探索するためのルート tsconfigPath?: string; // 使用する tsconfig を明示したい場合 filePath: string; // 絶対 or projectRoot からの相対 line: number; // 0-based character: number; // 0-based newName: string; findInStrings?: boolean; // デフォルト false findInComments?: boolean; // デフォルト false }; /** * planRenameSymbol の出力結果 */ export type PlanRenameSymbolResult = | { canRename: false; reason: string; } | { canRename: true; edits: FileTextEdits[]; }; - src/index.ts:28-190 (registration)Tool registration in MCP server: defines the tool name 'planRenameSymbol' with description and JSON Schema input schema. Dispatches calls from the MCP request handler (case 'planRenameSymbol' at line 179) to the planRenameSymbol function.
const TOOLS: Tool[] = [ { name: "planRenameSymbol", description: "Compute edits to rename a TypeScript symbol at a specific position. Returns edit plans without modifying the filesystem.", inputSchema: { type: "object", properties: { projectRoot: { type: "string", description: "Optional base directory for resolving relative paths and limiting tsconfig discovery", }, workspaceRoot: { type: "string", description: "Optional monorepo root used to search multiple tsconfig.json files", }, tsconfigPath: { type: "string", description: "Optional explicit tsconfig.json path", }, filePath: { type: "string", description: "Absolute path or path relative to projectRoot/workspaceRoot", }, line: { type: "number", description: "0-based line number of the symbol", }, character: { type: "number", description: "0-based character position of the symbol", }, newName: { type: "string", description: "The new name for the symbol", }, findInStrings: { type: "boolean", description: "Whether to find occurrences in strings (default: false)", }, findInComments: { type: "boolean", description: "Whether to find occurrences in comments (default: false)", }, }, required: ["filePath", "line", "character", "newName"], }, }, { name: "planFileMove", description: "Plan file move/rename with import path updates. Returns edit plans and file move suggestions without modifying the filesystem.", inputSchema: { type: "object", properties: { projectRoot: { type: "string", description: "Optional base directory for resolving relative paths and limiting tsconfig discovery", }, workspaceRoot: { type: "string", description: "Optional monorepo root used to search multiple tsconfig.json files", }, tsconfigPath: { type: "string", description: "Optional explicit tsconfig.json path for the primary project", }, oldPath: { type: "string", description: "Absolute path or path relative to projectRoot/workspaceRoot of the file to move", }, newPath: { type: "string", description: "Absolute path or path relative to projectRoot/workspaceRoot of the destination", }, }, required: ["oldPath", "newPath"], }, }, { name: "planDirectoryMove", description: "Plan directory move with recursive import updates for all contained files. Returns edit plans and file move suggestions without modifying the filesystem.", inputSchema: { type: "object", properties: { projectRoot: { type: "string", description: "Optional base directory for resolving relative paths and limiting tsconfig discovery", }, workspaceRoot: { type: "string", description: "Optional monorepo root used to search multiple tsconfig.json files", }, tsconfigPath: { type: "string", description: "Optional explicit tsconfig.json path for the primary project", }, oldDir: { type: "string", description: "Absolute path or path relative to projectRoot/workspaceRoot of the directory to move", }, newDir: { type: "string", description: "Absolute path or path relative to projectRoot/workspaceRoot of the destination", }, }, required: ["oldDir", "newDir"], }, }, ]; /** * MCP サーバーの起動 */ async function main(): Promise<void> { // Server インスタンスを作成 const server = new Server( { name: "ts-rename-helper-mcp", version: "0.1.0", }, { capabilities: { tools: {}, }, } ); // ツールリストのリクエストをハンドル server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: TOOLS, }; }); // ツール呼び出しのリクエストをハンドル server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "planRenameSymbol": { const params = args as unknown as PlanRenameSymbolParams; const result = planRenameSymbol(params); return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } - src/editUtils.ts:92-104 (helper)addRenameLocations helper: converts TypeScript RenameLocation objects (with TextSpan) into TextEdit entries in the EditBucket. Called by planRenameSymbol to accumulate rename edits across multiple TS projects.
export function addRenameLocations( bucket: EditBucket, tsModule: TypeScriptModule, locations: readonly ts.RenameLocation[], newText: string ): void { for (const location of locations) { addTextEdit(bucket, location.fileName, { range: textSpanToRange(tsModule, location.fileName, location.textSpan), newText, }); } } - src/tsService.ts:414-453 (helper)collectWorkspaceTsServices helper: discovers all tsconfig.json files within the workspace root and creates TypeScript LanguageService contexts for each. Used by planRenameSymbol to find rename locations across all projects in a monorepo.
export function collectWorkspaceTsServices( anchorPaths: string[], options: CreateTsServiceOptions & { primaryFilePath?: string; } = {} ): TsServiceContext[] { const dedupedContexts = new Map<string, TsServiceContext>(); if (options.primaryFilePath) { const primaryContext = createTsService({ ...options, filePath: options.primaryFilePath, }); dedupedContexts.set(primaryContext.tsconfigPath, primaryContext); } const workspaceRoot = inferWorkspaceRoot( anchorPaths, options.workspaceRoot ?? options.projectRoot ); if (!workspaceRoot) { return Array.from(dedupedContexts.values()); } for (const tsconfigPath of findWorkspaceTsconfigPaths(workspaceRoot)) { if (dedupedContexts.has(tsconfigPath)) { continue; } try { const context = createTsService({ tsconfigPath }); dedupedContexts.set(tsconfigPath, context); } catch { // ワークスペース配下の壊れた sibling tsconfig は無視して継続する } } return Array.from(dedupedContexts.values()); }