Skip to content

Commit d5cb2af

Browse files
authored
Merge pull request #13 from secondcircle/add-file-editing-capability
feat: Add file editing capability to rename_symbol tools
2 parents f9494d8 + 9a1e2da commit d5cb2af

14 files changed

+1877
-26
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Thumbs.db
5757
# Temporary files
5858
tmp/
5959
temp/
60+
test-tmp/
6061

6162
# MCP specific
6263
mcp/*/storage.json

README.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -374,25 +374,32 @@ Find all references to a symbol by name and kind in a file. Returns references f
374374

375375
### `rename_symbol`
376376

377-
Rename a symbol by name and kind in a file. If multiple symbols match, returns candidate positions and suggests using rename_symbol_strict.
377+
Rename a symbol by name and kind in a file. **This tool now applies the rename to all affected files by default.** If multiple symbols match, returns candidate positions and suggests using rename_symbol_strict.
378378

379379
**Parameters:**
380380

381381
- `file_path`: The path to the file
382382
- `symbol_name`: The name of the symbol
383383
- `symbol_kind`: The kind of symbol (function, class, variable, method, etc.) (optional)
384384
- `new_name`: The new name for the symbol
385+
- `dry_run`: If true, only preview the changes without applying them (optional, default: false)
386+
387+
**Note:** When `dry_run` is false (default), the tool will:
388+
- Apply the rename to all affected files
389+
- Create backup files with `.bak` extension
390+
- Return the list of modified files
385391

386392
### `rename_symbol_strict`
387393

388-
Rename a symbol at a specific position in a file. Use this when rename_symbol returns multiple candidates.
394+
Rename a symbol at a specific position in a file. Use this when rename_symbol returns multiple candidates. **This tool now applies the rename to all affected files by default.**
389395

390396
**Parameters:**
391397

392398
- `file_path`: The path to the file
393399
- `line`: The line number (1-indexed)
394400
- `character`: The character position in the line (1-indexed)
395401
- `new_name`: The new name for the symbol
402+
- `dry_run`: If true, only preview the changes without applying them (optional, default: false)
396403

397404
### `get_diagnostics`
398405

@@ -439,13 +446,28 @@ Results: Found 5 references:
439446

440447
### Renaming Symbols
441448

442-
Safe refactoring across the entire codebase:
449+
Safe refactoring across the entire codebase (now with actual file modification!):
443450

444451
```
445452
Claude: I'll rename `getUserData` to `fetchUserProfile`
446453
> Using cclsp.rename_symbol with symbol_name="getUserData", new_name="fetchUserProfile"
447454
448-
Result: Successfully renamed getUserData (function) to "fetchUserProfile":
455+
Result: Successfully renamed getUserData (function) to "fetchUserProfile".
456+
457+
Modified files:
458+
- src/api/user.ts
459+
- src/services/auth.ts
460+
- src/components/UserProfile.tsx
461+
... (12 files total)
462+
```
463+
464+
Preview changes before applying (using dry_run):
465+
466+
```
467+
Claude: Let me first preview what will be renamed
468+
> Using cclsp.rename_symbol with symbol_name="getUserData", new_name="fetchUserProfile", dry_run=true
469+
470+
Result: [DRY RUN] Would rename getUserData (function) to "fetchUserProfile":
449471
File: src/api/user.ts
450472
- Line 55, Column 10 to Line 55, Column 21: "fetchUserProfile"
451473
File: src/services/auth.ts
@@ -466,7 +488,10 @@ Result: Multiple symbols found matching "data". Please use rename_symbol_strict
466488
467489
> Using cclsp.rename_symbol_strict with line=45, character=10, new_name="userData"
468490
469-
Result: Successfully renamed symbol at line 45, character 10 to "userData"
491+
Result: Successfully renamed symbol at line 45, character 10 to "userData".
492+
493+
Modified files:
494+
- src/utils/parser.ts
470495
```
471496

472497
### Checking File Diagnostics

bun.lock

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@biomejs/biome": "^1.9.4",
1515
"@types/bun": "latest",
1616
"@types/node": "^24.0.1",
17+
"memfs": "^4.36.3",
1718
},
1819
"peerDependencies": {
1920
"typescript": "^5.8.3",
@@ -67,6 +68,18 @@
6768

6869
"@inquirer/type": ["@inquirer/[email protected]", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA=="],
6970

71+
"@jsonjoy.com/base64": ["@jsonjoy.com/[email protected]", "", { "peerDependencies": { "tslib": "2" } }, "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA=="],
72+
73+
"@jsonjoy.com/buffers": ["@jsonjoy.com/[email protected]", "", { "peerDependencies": { "tslib": "2" } }, "sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q=="],
74+
75+
"@jsonjoy.com/codegen": ["@jsonjoy.com/[email protected]", "", { "peerDependencies": { "tslib": "2" } }, "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g=="],
76+
77+
"@jsonjoy.com/json-pack": ["@jsonjoy.com/[email protected]", "", { "dependencies": { "@jsonjoy.com/base64": "^1.1.2", "@jsonjoy.com/buffers": "^1.0.0", "@jsonjoy.com/codegen": "^1.0.0", "@jsonjoy.com/json-pointer": "^1.0.1", "@jsonjoy.com/util": "^1.9.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-nLqSTAYwpk+5ZQIoVp7pfd/oSKNWlEdvTq2LzVA4r2wtWZg6v+5u0VgBOaDJuUfNOuw/4Ysq6glN5QKSrOCgrA=="],
78+
79+
"@jsonjoy.com/json-pointer": ["@jsonjoy.com/[email protected]", "", { "dependencies": { "@jsonjoy.com/util": "^1.3.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-tJpwQfuBuxqZlyoJOSZcqf7OUmiYQ6MiPNmOv4KbZdXE/DdvBSSAwhos0zIlJU/AXxC8XpuO8p08bh2fIl+RKA=="],
80+
81+
"@jsonjoy.com/util": ["@jsonjoy.com/[email protected]", "", { "dependencies": { "@jsonjoy.com/buffers": "^1.0.0", "@jsonjoy.com/codegen": "^1.0.0" }, "peerDependencies": { "tslib": "2" } }, "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ=="],
82+
7083
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/[email protected]", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-DyVYSOafBvk3/j1Oka4z5BWT8o4AFmoNyZY9pALOm7Lh3GZglR71Co4r4dEUoqDWdDazIZQHBe7J2Nwkg6gHgQ=="],
7184

7285
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
@@ -173,6 +186,8 @@
173186

174187
"http-errors": ["[email protected]", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
175188

189+
"hyperdyperid": ["[email protected]", "", {}, "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A=="],
190+
176191
"iconv-lite": ["[email protected]", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
177192

178193
"ignore": ["[email protected]", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
@@ -195,6 +210,8 @@
195210

196211
"media-typer": ["[email protected]", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
197212

213+
"memfs": ["[email protected]", "", { "dependencies": { "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "thingies": "^2.5.0", "tree-dump": "^1.0.3", "tslib": "^2.0.0" } }, "sha512-rZIVsNPGdZDPls/ckWhIsod2zRNsI2f2kEru0gMldkrEve+fPn7CVBTvfKLNyHQ9rZDWwzVBF8tPsZivzDPiZQ=="],
214+
198215
"merge-descriptors": ["[email protected]", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
199216

200217
"mime-db": ["[email protected]", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
@@ -271,10 +288,14 @@
271288

272289
"strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
273290

291+
"thingies": ["[email protected]", "", { "peerDependencies": { "tslib": "^2" } }, "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw=="],
292+
274293
"tmp": ["[email protected]", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="],
275294

276295
"toidentifier": ["[email protected]", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
277296

297+
"tree-dump": ["[email protected]", "", { "peerDependencies": { "tslib": "2" } }, "sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg=="],
298+
278299
"tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
279300

280301
"type-fest": ["[email protected]", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],

index.ts

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { resolve } from 'node:path';
44
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
55
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
66
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
7+
import { applyWorkspaceEdit } from './src/file-editor.js';
78
import { LSPClient } from './src/lsp-client.js';
89
import { uriToPath } from './src/utils.js';
910

@@ -97,7 +98,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
9798
{
9899
name: 'rename_symbol',
99100
description:
100-
'Rename a symbol by name and kind in a file. If multiple symbols match, returns candidate positions and suggests using rename_symbol_strict.',
101+
'Rename a symbol by name and kind in a file. If multiple symbols match, returns candidate positions and suggests using rename_symbol_strict. By default, this will apply the rename to the files. Use dry_run to preview changes without applying them.',
101102
inputSchema: {
102103
type: 'object',
103104
properties: {
@@ -117,14 +118,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
117118
type: 'string',
118119
description: 'The new name for the symbol',
119120
},
121+
dry_run: {
122+
type: 'boolean',
123+
description:
124+
'If true, only preview the changes without applying them (default: false)',
125+
},
120126
},
121127
required: ['file_path', 'symbol_name', 'new_name'],
122128
},
123129
},
124130
{
125131
name: 'rename_symbol_strict',
126132
description:
127-
'Rename a symbol at a specific position in a file. Use this when rename_symbol returns multiple candidates.',
133+
'Rename a symbol at a specific position in a file. Use this when rename_symbol returns multiple candidates. By default, this will apply the rename to the files. Use dry_run to preview changes without applying them.',
128134
inputSchema: {
129135
type: 'object',
130136
properties: {
@@ -144,6 +150,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
144150
type: 'string',
145151
description: 'The new name for the symbol',
146152
},
153+
dry_run: {
154+
type: 'boolean',
155+
description:
156+
'If true, only preview the changes without applying them (default: false)',
157+
},
147158
},
148159
required: ['file_path', 'line', 'character', 'new_name'],
149160
},
@@ -361,11 +372,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
361372
}
362373

363374
if (name === 'rename_symbol') {
364-
const { file_path, symbol_name, symbol_kind, new_name } = args as {
375+
const {
376+
file_path,
377+
symbol_name,
378+
symbol_kind,
379+
new_name,
380+
dry_run = false,
381+
} = args as {
365382
file_path: string;
366383
symbol_name: string;
367384
symbol_kind?: string;
368385
new_name: string;
386+
dry_run?: boolean;
369387
};
370388
const absolutePath = resolve(file_path);
371389

@@ -430,9 +448,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
430448
}
431449
}
432450

451+
// Apply changes if not in dry run mode
452+
if (!dry_run) {
453+
const editResult = await applyWorkspaceEdit(workspaceEdit, { lspClient });
454+
455+
if (!editResult.success) {
456+
return {
457+
content: [
458+
{
459+
type: 'text',
460+
text: `Failed to apply rename: ${editResult.error}`,
461+
},
462+
],
463+
};
464+
}
465+
466+
const responseText = warning
467+
? `${warning}\n\nSuccessfully renamed ${match.name} (${lspClient.symbolKindToString(match.kind)}) to "${new_name}".\n\nModified files:\n${editResult.filesModified.map((f) => `- ${f}`).join('\n')}`
468+
: `Successfully renamed ${match.name} (${lspClient.symbolKindToString(match.kind)}) to "${new_name}".\n\nModified files:\n${editResult.filesModified.map((f) => `- ${f}`).join('\n')}`;
469+
470+
return {
471+
content: [
472+
{
473+
type: 'text',
474+
text: responseText,
475+
},
476+
],
477+
};
478+
}
479+
// Dry run mode - show preview
433480
const responseText = warning
434-
? `${warning}\n\nSuccessfully renamed ${match.name} (${lspClient.symbolKindToString(match.kind)}) to "${new_name}":\n${changes.join('\n')}`
435-
: `Successfully renamed ${match.name} (${lspClient.symbolKindToString(match.kind)}) to "${new_name}":\n${changes.join('\n')}`;
481+
? `${warning}\n\n[DRY RUN] Would rename ${match.name} (${lspClient.symbolKindToString(match.kind)}) to "${new_name}":\n${changes.join('\n')}`
482+
: `[DRY RUN] Would rename ${match.name} (${lspClient.symbolKindToString(match.kind)}) to "${new_name}":\n${changes.join('\n')}`;
436483

437484
return {
438485
content: [
@@ -468,11 +515,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
468515
}
469516

470517
if (name === 'rename_symbol_strict') {
471-
const { file_path, line, character, new_name } = args as {
518+
const {
519+
file_path,
520+
line,
521+
character,
522+
new_name,
523+
dry_run = false,
524+
} = args as {
472525
file_path: string;
473526
line: number;
474527
character: number;
475528
new_name: string;
529+
dry_run?: boolean;
476530
};
477531
const absolutePath = resolve(file_path);
478532

@@ -496,11 +550,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
496550
}
497551
}
498552

553+
// Apply changes if not in dry run mode
554+
if (!dry_run) {
555+
const editResult = await applyWorkspaceEdit(workspaceEdit, { lspClient });
556+
557+
if (!editResult.success) {
558+
return {
559+
content: [
560+
{
561+
type: 'text',
562+
text: `Failed to apply rename: ${editResult.error}`,
563+
},
564+
],
565+
};
566+
}
567+
568+
return {
569+
content: [
570+
{
571+
type: 'text',
572+
text: `Successfully renamed symbol at line ${line}, character ${character} to "${new_name}".\n\nModified files:\n${editResult.filesModified.map((f) => `- ${f}`).join('\n')}`,
573+
},
574+
],
575+
};
576+
}
577+
// Dry run mode - show preview
499578
return {
500579
content: [
501580
{
502581
type: 'text',
503-
text: `Successfully renamed symbol at line ${line}, character ${character} to "${new_name}":\n${changes.join('\n')}`,
582+
text: `[DRY RUN] Would rename symbol at line ${line}, character ${character} to "${new_name}":\n${changes.join('\n')}`,
504583
},
505584
],
506585
};

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
"devDependencies": {
5656
"@biomejs/biome": "^1.9.4",
5757
"@types/bun": "latest",
58-
"@types/node": "^24.0.1"
58+
"@types/node": "^24.0.1",
59+
"memfs": "^4.36.3"
5960
},
6061
"peerDependencies": {
6162
"typescript": "^5.8.3"

0 commit comments

Comments
 (0)