patch_file Macro Tool
Status: ✅ Implemented in v0.7.0 Issue: #26Commit:
c8ee5dc—feat(tools): patch_file macro tool for unified read-diff-write operationsBranch:feat/v0.7.0-improvements
Problem
To edit a file, LLMs must execute three separate tool calls: read_text_file → compute diff in context → edit_file. This is slow (3 round-trips), wastes tokens (the full file content passes through the model twice), and is error-prone (the file can change between read and write).
Design
Goal
A single tool call that reads a file, applies a diff, and writes the result — atomically and with optional concurrency safety.
Tool Schema
{
name: "patch_file",
inputSchema: {
path: string,
patch: string,
format?: "unified" | "line_replace", // default: "unified"
expected_etag?: string // optional concurrency check
}
}Patch Formats
Unified diff (default) — Standard @@ hunk syntax:
@@ -2,1 +2,1 @@
- "port": 3000
+ "port": 8080Multi-hunk diffs are supported. File header lines (---, +++, diff, index) are skipped.
Line replace — Simpler format for line-range replacements:
2:2
"port": 8080Multiple replacement blocks can be specified. Applied in reverse order to preserve line indices.
Architecture
src/tools/patch.ts → [NEW] parseUnifiedDiff, applyUnifiedDiff,
parseLineReplace, applyLineReplace,
handlePatchFile, registerPatchTools
src/server.ts → [MODIFY] register patch tool
src/auth/scopes.ts → [MODIFY] map patch_file to WRITE scopeHandler Flow
async function handlePatchFile(args, ctx) {
// 1. Read current file
const original = await ctx.vfs.get(root, key);
// 2. Concurrency check (optional)
if (args.expected_etag) {
const stat = await ctx.vfs.stat(root, key);
if (stat.etag !== args.expected_etag) return err("Conflict: ...");
}
// 3. Parse and apply patch
const result = format === "unified"
? applyUnifiedDiff(original, parseUnifiedDiff(args.patch))
: applyLineReplace(original, parseLineReplace(args.patch));
// 4. Write result
await ctx.vfs.put(root, key, Buffer.from(result, "utf8"));
// 5. Return summary with new ETag
return ok(`Patched: ${oldLines} → ${newLines} lines (etag: ${newEtag})`);
}Unified Diff Parser
The parser extracts hunks by matching @@ -old,count +new,count @@ headers, then collects lines starting with (context), - (removal), or + (addition). Application applies hunks sequentially, tracking an accumulated line offset.
Line Replace Parser
Scans for startLine:endLine headers (plain integers separated by :), collects subsequent lines as replacement content until the next header. Applied in reverse order via splice().
Key Decisions
- Two formats: Unified diff is natural for LLMs trained on code. Line replace is simpler for cases where the LLM knows exact line numbers (e.g., from
read_file_range). - Atomic: The full read-transform-write happens in one handler — no intermediate state exposure.
- ETag integration: Leverages the v0.7.0 ETag system for concurrency safety without reimplementing conflict detection.
Implementation Plan
- Implement
parseUnifiedDiff()andapplyUnifiedDiff(). - Implement
parseLineReplace()andapplyLineReplace(). - Create
handlePatchFilewith concurrency check and format dispatch. - Register tool in
createMcpServer(). - Map to
cloud-fs:writescope. - Unit tests:
src/tools/patch.test.ts— 20 tests covering both formats, multi-hunk, edge cases, ETag conflicts.
Acceptance Criteria
- [x] Unified diff format parses
@@hunks correctly - [x] Multi-hunk diffs apply with correct offset tracking
- [x] File header lines (
---,+++) are skipped - [x] Line replace format parses
startLine:endLineblocks - [x] Multiple replacement blocks apply in correct order
- [x]
expected_etagmismatch returns conflict error - [x] Response includes new ETag and line count summary
- [x] Mapped to
cloud-fs:writescope - [x] 20 unit tests passing
