Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/spokospace/wordpress-content-mcp/archive/refs/heads/main.zip
Spoko Content MCP
A WordPress plugin that lets AI agents (Claude, Cursor, any MCP client) read and edit posts and their Gutenberg blocks, through the official MCP Adapter and the Abilities API.
An agent that rewrites a whole post to change one paragraph can break blocks it never meant to touch, or overwrite an edit someone made a minute ago. This plugin gives it smaller, safer steps:
- Edit one block, not the whole post. Blocks are addressed by a path (
12.0.3), so a change replaces, inserts, moves or deletes exactly that block and leaves the rest byte for byte. - No lost edits. Every read returns a
content_hash; every write needs it. If the post changed in the meantime, the write is refused. - Look before you save. Every write takes
dry_run. - Always a way back. Every save is a normal post update with a revision (
revision_id). - Valid blocks only. Before saving, the content is checked for broken block structure; if the check fails, nothing is saved.
Requirements
- WordPress 6.9+ (Abilities API in core), PHP 7.4+
- The MCP Adapter plugin
- A WordPress user for the agent (an application password). The tools run with that user's own
capabilities:
edit_postfor the post,upload_filesforfind-media.
Install
- Download
spoko-content-mcp.zipfrom the latest release. - Plugins → Add New → Upload Plugin, activate it.
- Turn on Enable auto-updates for it: new releases install themselves (see Updates).
- Point your MCP client at
https://your-site/wp-json/spoko/content, e.g. with@automattic/mcp-wordpress-remote:
{
"mcpServers": {
"spoko-content": {
"command": "npx",
"args": ["-y", "@automattic/mcp-wordpress-remote@latest"],
"env": {
"WP_API_URL": "https://your-site/wp-json/spoko/content",
"WP_API_USERNAME": "agent-user",
"WP_API_PASSWORD": "application password"
}
}
}
}
MCP clients list the tools as spoko-content-<name>, e.g. spoko-content-get-outline. Most
clients read the tool list once when they start, so restart the client after an update that adds
tools.
Tools
| Tool | Writes | What it does |
|---|---|---|
find-posts |
no | Search posts and pages by title text, slug or ID. |
get-outline |
no | Block tree with paths, names and text previews, plus content_hash. |
get-blocks |
no | Serialized markup of blocks by path, plus content_hash. |
validate-post |
no | Report broken block structure. |
find-media |
no | Search the media library: IDs, URLs, image sizes. |
replace-block |
yes | Replace the block at a path with new markup. |
insert-blocks |
yes | Insert blocks before or after a path. |
delete-block |
yes | Delete the block at a path. |
move-blocks |
yes | Move consecutive sibling blocks next to another block, markup unchanged. |
unwrap-block |
yes | Replace a container (e.g. a group) with its inner blocks. |
link-images |
yes | Link every unlinked image of a post to its full-size file. |
A block's path is its index in the parsed block tree, joined with dots: 12 is the 13th
top-level block, 12.0.3 the 4th inner block of its 1st inner block. Blank separators between
blocks take an index too, so top-level blocks are usually 0, 2, 4… Get paths from get-outline.
Typical edit:
get-outline→ paths andcontent_hash.- The write tool with
expected_hashanddry_run: true→ check the result. - The same call without
dry_run→ saved;revision_idis the backup.
link-images
Lightboxes usually open only images that have a link (<a href> around the <img>). Images
added with "Link to: None" have none, so the lightbox skips them. For every core/image block in
the post, galleries and columns included, link-images:
- links it the way the editor does for "Link to: Media file":
linkDestination: "media"and<a href="…">around the<img>. The file is the attachment's full URL (the-scaledfile for big uploads); without an attachment, thesrcwithout its-WxHsize suffix; - leaves it alone when it already has a link outside the caption (never a double
<a>); - skips it, with a reason in
skipped, when the figure has the classno-lightbox(the opt-out), the full file cannot be told, or the markup is unexpected.
Nothing else in the post changes, and a second run saves nothing. Images outside core/image
blocks (tables, classic content, core/html) are out of scope.
Headless sites: if the frontend serves the uploads under its own address, set it, and links point there instead of at WordPress:
// wp-config.php
define( 'SPOKO_CONTENT_MCP_MEDIA_BASE', 'https://example.com/images' );
// or in code, plus old media hosts to rewrite as well:
add_filter( 'spoko_content_mcp_media_base', fn () => 'https://example.com/images' );
add_filter( 'spoko_content_mcp_media_aliases', fn ( array $bases ) => [ ...$bases, 'https://img.example.com' ] );
URLs under the uploads base URL (and under the aliases) are rewritten; other hosts are left as they are. Without a media base, links use the URLs WordPress gives.
Roadmap
Tools for the rest of a content workflow, with the same hash, dry-run and revision rules:
- creating and updating posts and pages, moving them to the trash;
- categories and tags;
- Polylang translations: pairing posts as translations of each other (only when Polylang is active).
Updates and releases
There are two builds of the same code:
- GitHub build (
dist/spoko-content-mcp.zip, the GitHub release asset) updates itself from this repo's GitHub releases (updater.php): the Plugins screen shows the new version, and with Enable auto-updates on, WordPress installs it by itself. A release counts when its tag is a higher version (v2.0.1), it is not a draft or a prerelease, and it has the assetspoko-content-mcp.zip. - WordPress.org build (
dist/wporg/spoko-content-mcp.zip) has noupdater.php, since the plugin directory does not allow plugins to update themselves; WordPress.org delivers its updates. The plugin loads the updater only when the file is there.
Releasing (no CI needed):
- Bump
Version:in the plugin header,VERSIONin the class andStable taginreadme.txt(the script refuses if they differ), add changelog entries toREADME.mdandreadme.txt, merge tomain. - On
main, runbin/release.sh --publish. It builds both zips withgit archive(top folderspoko-content-mcp/) and creates the GitHub releasev<version>with the changelog entry as notes. Without--publishit only builds the zips. On Windows, run it from Git Bash:bashstarted from PowerShell may be WSL, whose git sees the checkout as modified and the script refuses. - WordPress.org (once the plugin is approved): copy the WordPress.org build into the SVN
checkout's
trunk/, copytrunk/totags/<version>/, andsvn commit. The directory assets (banner, icon) areart/wporg/*.jpg|pngand go to SVNassets/; repaint them withpython art/wporg/render.py.
Before a WordPress.org release, run Plugin Check
on the WordPress.org build (wp plugin check spoko-content-mcp); 2.0.1 passes with no errors or
warnings, experimental checks included.
Tests
No WordPress install or database needed:
php tests/link-images.php /path/to/wordpress spoko-content-mcp.php
php tests/updater.php
link-images.php runs link-images against WordPress's real block parser and serializer (from
the given WordPress checkout) with stubs for the rest. updater.php checks the updater against
stubbed GitHub responses. Both exit 0 when everything passes.
Changelog
- 2.0.1: one endpoint only,
/wp-json/spoko/content. - 2.0.0: first public release. Block tools (
find-posts,get-outline,get-blocks,validate-post,find-media,replace-block,insert-blocks,delete-block,move-blocks,unwrap-block,link-images) at/wp-json/spoko/content, a configurable public media address forlink-images(SPOKO_CONTENT_MCP_MEDIA_BASE, filters), updates from GitHub releases. MIT license.
Made by spoko.space
Spoko Content MCP is built and maintained by spoko.space: websites, online shops and web apps, headless WordPress included. Questions and ideas are welcome as issues.