Skip to content

Add targeted partial content edits#2

Merged
jack-arturo merged 3 commits into
mainfrom
feature/partial-content-edits
Jun 10, 2026
Merged

Add targeted partial content edits#2
jack-arturo merged 3 commits into
mainfrom
feature/partial-content-edits

Conversation

@jack-arturo

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

Copy link
Copy Markdown
Member

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

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown

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.

jack-arturo and others added 2 commits March 25, 2026 03:31
…hema fix.

Includes uncommitted WIP that was in the working tree: site_id param via
siteManager, DANGEROUS_PATTERNS / multi-statement guards, and calling the
/mcp/v1/query endpoint directly with axios (makeWordPressRequest prepends
/wp-json/wp/v2/ so it can't reach it).

Also fixes the published parameter schema: server.ts rebuilds each tool
schema with z.object(inputSchema.properties), so properties must be zod
shapes; the WIP passed raw JSON Schema, collapsing the published schema
to {} and causing clients to strip all params.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 10, 2026 22:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces targeted, substring-level editing for WordPress content to reduce token/network cost in MCP/LLM workflows, and adds an opt-in raw-content read path to make exact matching reliable against content.raw (WordPress context=edit).

Changes:

  • Add content_edit to update_content and find_content_by_url.update_fields to support partial operations (append, prepend, insert_before, insert_after, replace) against raw stored content.
  • Add include_raw_content to get_content / find_content_by_url and expose content_raw for exact target_text discovery.
  • Adjust SQL query tool schema publishing and add site_id support for multi-site SQL queries.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/types/wordpress-types.ts Expand WP REST typings to include optional raw fields for edit-context responses.
src/tools/unified-content.ts Implement content_edit, raw-content fetching (context=edit), and response content_raw aliasing.
src/tools/sql-query.ts Add site_id, fix schema publishing approach, and build SQL endpoint requests directly from site config.
README.md Document targeted content edits and the include_raw_content / content_raw workflow.
CLAUDE.md Update tool documentation to reflect targeted updates and raw-content retrieval.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/tools/sql-query.ts
Comment on lines +89 to +93
const site = siteManager.getSite(params.site_id);
const sqlPath = process.env.WORDPRESS_SQL_ENDPOINT || '/mcp/v1/query';
const siteBase = site.url.replace(/\/$/, '');
const url = `${siteBase}/wp-json${sqlPath}`;

Comment on lines +455 to +457
matches.push(matchIndex);
fromIndex = matchIndex + targetText.length;
}
Comment on lines +1151 to +1154
await makeWordPressRequest('POST', `${endpoint}/${content.id}`, updateData, { siteId: params.site_id });
const responseContent = params.include_raw_content
? await fetchContentById(endpoint, content.id, params.site_id, true)
: await fetchContentById(endpoint, content.id, params.site_id, false);
Comment on lines +1209 to +1212
await makeWordPressRequest('POST', `${endpoint}/${content.id}`, updateData, { siteId: params.site_id });
const responseContent = params.include_raw_content
? await fetchContentById(endpoint, content.id, params.site_id, true)
: await fetchContentById(endpoint, content.id, params.site_id, false);
@jack-arturo jack-arturo changed the base branch from feature/media-tool-cleanup-file-upload to main June 10, 2026 22:57
@jack-arturo jack-arturo merged commit a32afde into main Jun 10, 2026
1 check passed
@jack-arturo jack-arturo deleted the feature/partial-content-edits branch June 10, 2026 22:58
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.

2 participants