edit-tools.ts•10.4 kB
import * as vscode from 'vscode';
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from 'zod';
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
/**
* Creates a new file in the VS Code workspace using WorkspaceEdit
* @param workspacePath The path within the workspace to the file
* @param content The content to write to the file
* @param overwrite Whether to overwrite if the file exists
* @param ignoreIfExists Whether to ignore if the file exists
* @returns Promise that resolves when the edit operation completes
*/
export async function createWorkspaceFile(
workspacePath: string,
content: string,
overwrite: boolean = false,
ignoreIfExists: boolean = false
): Promise<void> {
console.log(`[createWorkspaceFile] Starting with path: ${workspacePath}, overwrite: ${overwrite}, ignoreIfExists: ${ignoreIfExists}`);
if (!vscode.workspace.workspaceFolders) {
throw new Error('No workspace folder is open');
}
const workspaceFolder = vscode.workspace.workspaceFolders[0];
const workspaceUri = workspaceFolder.uri;
// Create URI for the target file
const fileUri = vscode.Uri.joinPath(workspaceUri, workspacePath);
console.log(`[createWorkspaceFile] File URI: ${fileUri.fsPath}`);
try {
// Create a WorkspaceEdit
const workspaceEdit = new vscode.WorkspaceEdit();
// Convert content to Uint8Array
const contentBuffer = new TextEncoder().encode(content);
// Add createFile operation to the edit
workspaceEdit.createFile(fileUri, {
contents: contentBuffer,
overwrite: overwrite,
ignoreIfExists: ignoreIfExists
});
// Apply the edit
const success = await vscode.workspace.applyEdit(workspaceEdit);
if (success) {
console.log(`[createWorkspaceFile] File created successfully: ${fileUri.fsPath}`);
// Open the document to trigger linting
const document = await vscode.workspace.openTextDocument(fileUri);
await vscode.window.showTextDocument(document);
console.log(`[createWorkspaceFile] File opened in editor`);
} else {
throw new Error(`Failed to create file: ${fileUri.fsPath}`);
}
} catch (error) {
console.error('[createWorkspaceFile] Error:', error);
throw error;
}
}
/**
* Replaces specific lines in a file in the VS Code workspace
* @param workspacePath The path within the workspace to the file
* @param startLine The start line number (0-based, inclusive)
* @param endLine The end line number (0-based, inclusive)
* @param content The new content to replace the lines with
* @param originalCode The original code for validation
* @returns Promise that resolves when the edit operation completes
*/
export async function replaceWorkspaceFileLines(
workspacePath: string,
startLine: number,
endLine: number,
content: string,
originalCode: string
): Promise<void> {
console.log(`[replaceWorkspaceFileLines] Starting with path: ${workspacePath}, lines: ${startLine}-${endLine}`);
if (!vscode.workspace.workspaceFolders) {
throw new Error('No workspace folder is open');
}
const workspaceFolder = vscode.workspace.workspaceFolders[0];
const workspaceUri = workspaceFolder.uri;
// Create URI for the target file
const fileUri = vscode.Uri.joinPath(workspaceUri, workspacePath);
console.log(`[replaceWorkspaceFileLines] File URI: ${fileUri.fsPath}`);
try {
// Open the document (or get it if already open)
const document = await vscode.workspace.openTextDocument(fileUri);
// Validate line numbers
if (startLine < 0 || startLine >= document.lineCount) {
throw new Error(`Start line ${startLine + 1} is out of range (1-${document.lineCount})`);
}
if (endLine < startLine || endLine >= document.lineCount) {
throw new Error(`End line ${endLine + 1} is out of range (${startLine + 1}-${document.lineCount})`);
}
// Get the current content of the lines
const currentLines = [];
for (let i = startLine; i <= endLine; i++) {
currentLines.push(document.lineAt(i).text);
}
const currentContent = currentLines.join('\n');
// Compare with the provided original code
if (currentContent !== originalCode) {
throw new Error(`Original code validation failed. The current content does not match the provided original code.`);
}
// Create a range for the lines to replace
const startPos = new vscode.Position(startLine, 0);
const endPos = new vscode.Position(endLine, document.lineAt(endLine).text.length);
const range = new vscode.Range(startPos, endPos);
// Get the active text editor or show the document
let editor = vscode.window.activeTextEditor;
if (!editor || editor.document.uri.toString() !== fileUri.toString()) {
editor = await vscode.window.showTextDocument(document);
}
// Apply the edit
const success = await editor.edit((editBuilder) => {
editBuilder.replace(range, content);
});
if (success) {
console.log(`[replaceWorkspaceFileLines] Lines replaced successfully`);
// Save the document to persist changes
await document.save();
console.log(`[replaceWorkspaceFileLines] Document saved`);
} else {
throw new Error(`Failed to replace lines in file: ${fileUri.fsPath}`);
}
} catch (error) {
console.error('[replaceWorkspaceFileLines] Error:', error);
throw error;
}
}
/**
* Registers MCP edit-related tools with the server
* @param server MCP server instance
*/
export function registerEditTools(server: McpServer): void {
// Add create_file tool
server.tool(
'create_file_code',
`Creates new files or completely rewrites existing files.
WHEN TO USE: New files, large modifications (>10 lines), complete file rewrites.
USE replace_lines_code instead for: small edits ≤10 lines where you have exact original content.
File handling: Use overwrite=true to replace existing files, ignoreIfExists=true to skip if file exists.
Always check with list_files_code first unless you specifically want to overwrite.`,
{
path: z.string().describe('The path to the file to create'),
content: z.string().describe('The content to write to the file'),
overwrite: z.boolean().optional().default(false).describe('Whether to overwrite if the file exists'),
ignoreIfExists: z.boolean().optional().default(false).describe('Whether to ignore if the file exists')
},
async ({ path, content, overwrite = false, ignoreIfExists = false }): Promise<CallToolResult> => {
console.log(`[create_file] Tool called with path=${path}, overwrite=${overwrite}, ignoreIfExists=${ignoreIfExists}`);
try {
console.log('[create_file] Creating file');
await createWorkspaceFile(path, content, overwrite, ignoreIfExists);
const result: CallToolResult = {
content: [
{
type: 'text',
text: `File ${path} created successfully`
}
]
};
console.log('[create_file] Successfully completed');
return result;
} catch (error) {
console.error('[create_file] Error in tool:', error);
throw error;
}
}
);
// Add replace_lines_code tool
server.tool(
'replace_lines_code',
`Replaces specific lines in existing files with exact content validation.
WHEN TO USE: Modifications ≤10 lines where you have exact original text, or inserts of any size.
USE create_file_code instead for: new files, large modifications (>10 lines, hard to match exact content), or when original text is uncertain.
CRITICAL: originalCode parameter must match current file content exactly or tool fails.
If tool fails: run read_file_code on target lines to get current content, then retry.
Parameters use 1-based line numbers. Always verify line numbers with read_file_code if unsure.`,
{
path: z.string().describe('The path to the file to modify'),
startLine: z.number().describe('The start line number (1-based, inclusive)'),
endLine: z.number().describe('The end line number (1-based, inclusive)'),
content: z.string().describe('The new content to replace the lines with'),
originalCode: z.string().describe('The original code for validation - must match exactly')
},
async ({ path, startLine, endLine, content, originalCode }): Promise<CallToolResult> => {
console.log(`[replace_lines_code] Tool called with path=${path}, startLine=${startLine}, endLine=${endLine}`);
// Convert 1-based input to 0-based for VS Code API
const zeroBasedStartLine = startLine - 1;
const zeroBasedEndLine = endLine - 1;
try {
console.log('[replace_lines_code] Replacing lines');
await replaceWorkspaceFileLines(path, zeroBasedStartLine, zeroBasedEndLine, content, originalCode);
const result: CallToolResult = {
content: [
{
type: 'text',
text: `Lines ${startLine}-${endLine} in file ${path} replaced successfully`
}
]
};
console.log('[replace_lines_code] Successfully completed');
return result;
} catch (error) {
console.error('[replace_lines_code] Error in tool:', error);
throw error;
}
}
);
}