Skip to content

Add targeted partial content edits#2

Open
jack-arturo wants to merge 2 commits intofeature/media-tool-cleanup-file-uploadfrom
feature/partial-content-edits
Open

Add targeted partial content edits#2
jack-arturo wants to merge 2 commits intofeature/media-tool-cleanup-file-uploadfrom
feature/partial-content-edits

Conversation

@jack-arturo
Copy link
Copy Markdown
Member

@jack-arturo jack-arturo commented Mar 25, 2026

Problem

update_content currently treats the post body as an all-or-nothing string. That works for small edits, but it is expensive for MCP/LLM workflows because even a one-line change requires resending the full document. WordPress itself supports partial field updates at the post level, but it does not offer substring-level edits inside content, so the server needs to provide that behavior if we want to reduce token usage.

A related problem showed up immediately in review: even when targeted edits work, callers often copy target_text from rendered HTML. That is unreliable because rendered WordPress output can differ from the stored raw document. Entities may be escaped, marker comments may be wrapped, and image markup can diverge from what was originally saved.

Approach

This PR adds a new content_edit contract to update_content and mirrors the same contract in find_content_by_url.update_fields.

Supported operations:

  • append
  • prepend
  • insert_before
  • insert_after
  • replace

The existing content field remains the full-replacement path. content and content_edit are mutually exclusive so the tool contract stays explicit.

Follow-up from review:

  • add include_raw_content to get_content
  • add include_raw_content to find_content_by_url
  • expose a top-level content_raw field in those responses so callers can copy the exact raw fragment needed for content_edit.target_text

Why This Design

The goal here is to reduce token cost without introducing a fragile document-editing system.

I intentionally kept the edit path deterministic:

  • matching is against exact raw WordPress content, not inferred selectors or AI-generated patches
  • targeted operations require target_text for insert_before, insert_after, and replace
  • ambiguous matches fail unless occurrence is provided
  • the incoming fragment still goes through the existing content-format pipeline, so Markdown/HTML/block handling stays consistent with the rest of the server

That keeps the feature useful for practical marker-based and fragment-based edits while avoiding a much larger HTML or Gutenberg AST editing project.

The read-path follow-up is meant to remove the biggest usability pain in that design. Instead of making the matcher heuristic, the server now exposes the exact stored raw content when requested.

How It Works

When a partial edit is requested, the server:

  1. fetches the current post using context=edit
  2. reads content.raw
  3. processes the new fragment through the existing processContent flow
  4. applies the requested string patch locally
  5. sends the rebuilt content back through the normal WordPress update endpoint

For exact-match discovery, get_content(include_raw_content=true) and find_content_by_url(include_raw_content=true) also fetch with context=edit and include content_raw in the returned payload.

This means callers can now:

  1. resolve the content item
  2. copy content_raw or a fragment from it
  3. pass that exact raw string back into content_edit.target_text

API Surface

New edit object on update_content:

  • content_edit.operation
  • content_edit.value
  • content_edit.target_text
  • content_edit.occurrence
  • content_edit.content_format
  • content_edit.convert_to_blocks

The same object is supported under find_content_by_url.update_fields.content_edit.

New read flags:

  • get_content.include_raw_content
  • find_content_by_url.include_raw_content

New convenience field in raw-read responses:

  • content_raw

Validation added:

  • reject content + content_edit together
  • reject targeted operations without target_text
  • reject zero matches
  • reject multiple matches unless occurrence disambiguates
  • reject partial edits when WordPress does not return content.raw

Implementation Notes

  • the patching logic is centralized in shared helpers inside src/tools/unified-content.ts
  • both update_content and find_content_by_url reuse the same content-update builder so the behavior stays aligned
  • raw-read support uses the same endpoint resolution logic and only switches to context=edit when explicitly requested
  • docs were updated with examples and with a note that rendered HTML can differ from content.raw

Verification

  • npm run build
  • live smoke test against https://drunk.support
  • created a disposable published post via the MCP server
  • verified initial raw content with curl .../wp-json/wp/v2/posts/<id>?context=edit
  • verified update_content with content_edit.replace
  • verified find_content_by_url with content_edit.append
  • verified get_content(include_raw_content=true) returns content_raw
  • verified find_content_by_url(include_raw_content=true) returns content_raw
  • verified raw content after each step with curl
  • deleted the disposable posts and confirmed cleanup with final 404 curl checks

Representative curl check showing why raw content matters:

{
  "id": 576,
  "raw": "<p>Smoke & verify 20260325022944</p>\n<!-- raw-marker -->\n<p>Target & replace 20260325022944</p>\n<!-- /raw-marker -->",
  "rendered": "<p>Smoke &#038; verify 20260325022944</p>\n<p><!-- raw-marker --></p>\n<p>Target &#038; replace 20260325022944</p>\n<p><!-- /raw-marker --></p>\n"
}

Representative cleanup check:

{
  "code": "rest_post_invalid_id",
  "message": "Invalid post ID.",
  "data": {
    "status": 404
  }
}

Stack Note

This PR is intentionally stacked on top of feature/media-tool-cleanup-file-upload, which is upstream PR InstaWP/mcp-wp#13. Once #13 merges, this branch can be rebased onto main and opened upstream as a normal standalone PR.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 25, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ee708d46-f9d0-46dd-b505-56f3fb22f413

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/partial-content-edits

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant