Skip to main content
Glama
applyPatch.ts5.66 kB
/** * apply_patch tool implementation * Provides file patching capabilities within the workspace */ import { ServerConfig } from '../config.js'; import { resolveSafePath } from '../utils/pathUtils.js'; import { readFileContent, writeFileAtomic } from '../utils/fsUtils.js'; import { createReadOnlyError, createPatchFailedError, classifyError } from '../utils/errors.js'; /** * Input parameters for apply_patch tool */ export interface ApplyPatchInput { path: string; patch: string; } /** * Output from apply_patch tool */ export interface ApplyPatchOutput { path: string; oldSize: number; newSize: number; } /** * Tool metadata for MCP registration */ export const applyPatchTool = { name: 'apply_patch', description: 'Apply a patch to an existing file in the workspace. Uses a simple patch format: <<<OLD\\n...\\n===\\n...\\n>>>NEW', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Relative path to the file to patch', }, patch: { type: 'string', description: 'Patch content in format: <<<OLD\\n...\\n===\\n...\\n>>>NEW', }, }, required: ['path', 'patch'], }, }; /** * Parses a patch string in the format: <<<OLD\n...\n===\n...\n>>>NEW * @param patch - The patch string to parse * @returns Object containing oldContent and newContent * @throws Error if patch format is invalid */ function parsePatch(patch: string): { oldContent: string; newContent: string } { // Check for required markers if (!patch.includes('<<<OLD') || !patch.includes('===') || !patch.includes('>>>NEW')) { throw createPatchFailedError( 'Invalid patch format. Expected format: <<<OLD\\n...\\n===\\n...\\n>>>NEW', { patch: patch.substring(0, 100) } ); } // Find marker positions const oldMarkerIndex = patch.indexOf('<<<OLD'); const separatorIndex = patch.indexOf('===', oldMarkerIndex); const newMarkerIndex = patch.indexOf('>>>NEW', separatorIndex); if (oldMarkerIndex === -1 || separatorIndex === -1 || newMarkerIndex === -1) { throw createPatchFailedError('Invalid patch format. Missing required markers.'); } // Extract content between markers // Skip past "<<<OLD" and any newline let oldStart = oldMarkerIndex + '<<<OLD'.length; if (patch[oldStart] === '\n') { oldStart++; } else if (patch[oldStart] === '\r' && patch[oldStart + 1] === '\n') { oldStart += 2; } // Extract old content (everything before ===) let oldEnd = separatorIndex; // Trim trailing newline before === if (patch[oldEnd - 1] === '\n') { oldEnd--; if (patch[oldEnd - 1] === '\r') { oldEnd--; } } const oldContent = patch.substring(oldStart, oldEnd); // Skip past "===" and any newline let newStart = separatorIndex + '==='.length; if (patch[newStart] === '\n') { newStart++; } else if (patch[newStart] === '\r' && patch[newStart + 1] === '\n') { newStart += 2; } // Extract new content (everything before >>>NEW) let newEnd = newMarkerIndex; // Trim trailing newline before >>>NEW if (patch[newEnd - 1] === '\n') { newEnd--; if (patch[newEnd - 1] === '\r') { newEnd--; } } const newContent = patch.substring(newStart, newEnd); return { oldContent, newContent }; } /** * Applies a patch to file content * @param currentContent - The current file content * @param oldContent - The content to find and replace * @param newContent - The content to replace with * @returns The patched content * @throws Error if old content is not found in current content */ function applyPatchToContent( currentContent: string, oldContent: string, newContent: string ): string { // Check if old content exists in current content const index = currentContent.indexOf(oldContent); if (index === -1) { throw createPatchFailedError( 'Old content not found in file. The patch cannot be applied because the expected content does not match the current file content.', { expectedContent: oldContent.substring(0, 100), actualContentPreview: currentContent.substring(0, 100) } ); } // Replace the old content with new content const patchedContent = currentContent.substring(0, index) + newContent + currentContent.substring(index + oldContent.length); return patchedContent; } /** * Executes the apply_patch tool * @param input - Tool input parameters * @param config - Server configuration * @returns File path, old size, and new size */ export async function executeApplyPatch( input: ApplyPatchInput, config: ServerConfig ): Promise<ApplyPatchOutput> { const requestedPath = input.path; const patch = input.patch; // Check read-only mode if (config.readOnly) { throw createReadOnlyError('Write'); } try { // Validate and resolve the path const resolvedPath = await resolveSafePath(config.workspaceRoot, requestedPath); // Read the current file content const { content: currentContent, size: oldSize } = await readFileContent(resolvedPath); // Parse the patch const { oldContent, newContent } = parsePatch(patch); // Apply the patch const patchedContent = applyPatchToContent(currentContent, oldContent, newContent); // Write the patched content back to the file await writeFileAtomic(resolvedPath, patchedContent, false); const newSize = Buffer.byteLength(patchedContent, 'utf-8'); return { path: requestedPath, oldSize, newSize, }; } catch (error: unknown) { // Classify and re-throw the error throw classifyError(error, 'apply_patch'); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ShayYeffet/mcp_server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server