apply_patch
Apply unified diff patches to modify files, supporting validation, fuzzy matching, and line ending conversion for reliable file updates.
Instructions
Apply a unified diff patch to one or more files. Single-file: throws on failure. Multi-file: best-effort per file with results[]. Workflow: diff_files → apply_patch(dryRun:true) → apply_patch. On failure, regenerate the patch from current file content.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | Path to file to patch | |
| patch | Yes | Unified diff with @@ hunk headers. Generate with `diff_files`. | |
| fuzzFactor | No | Maximum fuzzy mismatches per hunk | |
| autoConvertLineEndings | No | Auto-convert line endings to match target file | |
| dryRun | No | Validate patch without writing. Check `applied` before committing. |
Implementation Reference
- src/tools/apply-patch.ts:253-308 (handler)The core logic function 'handleApplyPatch' that parses the patch content and applies it using the 'diff' library, handling both single and multi-file scenarios.
async function handleApplyPatch( args: z.infer<typeof ApplyPatchInputSchema>, signal?: AbortSignal ): Promise<ToolResponse<z.infer<typeof ApplyPatchOutputSchema>>> { if (!args.patch.trim()) { throw new McpError(ErrorCode.E_INVALID_INPUT, 'Patch content is empty.'); } const fuzzFactor = args.fuzzFactor ?? 0; const parsed = parsePatch(args.patch); const hasHunks = parsed.some((p) => p.hunks.length > 0); if (!hasHunks) { throw new McpError( ErrorCode.E_INVALID_INPUT, 'Patch must include unified hunk headers (e.g., @@ -1,2 +1,2 @@).' ); } const options: PatchOptions = { dryRun: args.dryRun, fuzzFactor, autoConvertLineEndings: args.autoConvertLineEndings, }; if (parsed.length > 1) { return processMultiFilePatch(args.path, parsed, options, signal); } const diff = parsed[0]; if (!diff) { throw new McpError(ErrorCode.E_INVALID_INPUT, 'No patch content found.'); } const result = await applyDiff(args.path, diff, options, signal); if (!result.applied) { throw new McpError( ErrorCode.E_INVALID_INPUT, result.error?.message === 'Patch had no effect' ? 'Patch had no effect \u2014 the file content is unchanged after applying. The patch may not match the current file content. Generate a fresh patch via diff_files and retry.' : 'Patch application failed. The file content may have changed or patch context is insufficient. Generate a fresh patch via diff_files against the current file, then retry. If differences are minor, enable fuzzy matching with the fuzzFactor parameter.' ); } const text = args.dryRun ? 'Dry run successful. Patch can be applied.' : `Successfully patched ${args.path}`; return buildToolResponse(text, { ok: true, path: result.path, applied: true, hunksApplied: result.hunksApplied, linesAdded: result.linesAdded, linesRemoved: result.linesRemoved, }); } - src/schemas.ts:791-833 (schema)Input/output schemas for the 'apply_patch' tool.
export const ApplyPatchInputSchema = z.strictObject({ path: RequiredPathSchema.describe('Path to file to patch'), patch: z .string() .min(1, 'Patch content required') .refine((val) => /@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/u.test(val), { error: 'Patch must include hunk headers (e.g., @@ -1,2 +1,2 @@)', }) .describe('Unified diff with @@ hunk headers. Generate with `diff_files`.'), fuzzFactor: z .int({ error: 'Must be integer' }) .min(0, 'Min: 0') .max(20, 'Max: 20') .optional() .describe('Maximum fuzzy mismatches per hunk'), autoConvertLineEndings: z .boolean() .optional() .default(true) .describe('Auto-convert line endings to match target file'), dryRun: z .boolean() .optional() .default(false) .describe( 'Validate patch without writing. Check `applied` before committing.' ), }); export const ApplyPatchOutputSchema = z.strictObject({ ok: z.boolean(), path: z.string().optional(), applied: z.boolean().optional(), hunksApplied: NonNegativeIntegerSchema.optional().describe('Hunks applied'), linesAdded: NonNegativeIntegerSchema.optional().describe('Lines added'), linesRemoved: NonNegativeIntegerSchema.optional().describe('Lines removed'), results: z .array( z.strictObject({ path: z.string().describe('File path'), applied: z.boolean().describe('Patch applied successfully'), hunksApplied: NonNegativeIntegerSchema.optional().describe('Hunks applied'), - src/tools/apply-patch.ts:310-370 (registration)Registration function 'registerApplyPatchTool' that binds the tool to the MCP server.
export function registerApplyPatchTool( server: McpServer, options: ToolRegistrationOptions = {} ): void { const handler = ( args: z.infer<typeof ApplyPatchInputSchema>, extra: ToolExtra ): Promise<ToolResult<z.infer<typeof ApplyPatchOutputSchema>>> => executeToolWithDiagnostics({ toolName: 'apply_patch', extra, outputSchema: ApplyPatchOutputSchema, timedSignal: {}, context: { path: args.path }, run: (signal) => handleApplyPatch(args, signal), onError: (error) => buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path), }); const wrappedHandler = wrapToolHandler(handler, { guard: options.isInitialized, progressMessage: (args) => { const name = path.basename(args.path); return args.dryRun ? `🛠 patch: ${name} [dry run]` : `🛠 patch: ${name}`; }, completionMessage: (args, result) => { const name = path.basename(args.path); if (result.isError) return `🛠 patch: ${name} • failed`; const sc = result.structuredContent; if (!sc.ok) return `🛠 patch: ${name} • failed`; const added = sc.linesAdded ?? 0; const removed = sc.linesRemoved ?? 0; const dry = args.dryRun ? 'dry run ' : ''; if (added > 0 || removed > 0) return `🛠 patch: ${name} • ${dry} +${added} -${removed}`; return `🛠 patch: ${name} • ${dry}no changes`; }, }); const validatedHandler = withValidatedArgs( ApplyPatchInputSchema, wrappedHandler ); if ( registerToolTaskIfAvailable( server, 'apply_patch', APPLY_PATCH_TOOL, validatedHandler, options.iconInfo, options.isInitialized ) ) return; server.registerTool( 'apply_patch', withDefaultIcons({ ...APPLY_PATCH_TOOL }, options.iconInfo), validatedHandler ); }