Exposes a focused set of WordPress content-management tools over the Model Context Protocol (MCP) so AI assistants can read and write posts, pages, ACF fields, options, menus, and media.
The canonical reference for each tool is its PHP class in inc/Abilities/ — read the get_description(), get_input_schema(), and get_output_schema() methods. Each tool also returns a hint field in its output payload that points to likely next actions or common failure causes.
- Few, sharp tools. No overlapping primitives. Block-level mutation tools were removed in favor of one path: read full content, modify the markup string, write full content back.
- Soft-fail with hints. Where useful, tools return
null/empty results plus ahintstring instead of erroring — so the agent can chain follow-up actions without a round-trip through error handling. - Schema-first. Always call
block-schemafor each Gutenberg block before composing markup. Wrong attributes are silently dropped.
All tools register under the xfive-mcp server with REST namespace xfive-mcp/v1 and route mcp. Each is grouped by category.
Tools follow a read/write-symmetry rule: anything a write tool can set, a read tool can return — so content can be migrated between sites without silently dropping fields. IDs of attachments and terms are site-local; tool hints flag them so the agent remaps when migrating.
| Tool | Purpose |
|---|---|
block-schema |
Get the registration schema for a block: attributes, supports, renderMode/seedAs (how to seed the comment), and nesting info (usesInnerBlocks, allowedBlocks, parent). Call BEFORE writing block markup. |
block-tree |
Parse a post's post_content into a Gutenberg block tree. attrs_only drops innerHTML/innerContent; depth summarizes blocks past the limit as {blockName, childCount, truncated}. |
| Tool | Purpose |
|---|---|
post-list |
Enumerate posts of one or more post types, with status, search, taxonomy + meta_query filters, pagination, ordering, and a fields mode (ids / summary / full). |
post-by-title |
Look up a post by exact title + post_type (optional post_status). Returns post_id: null (not an error) when nothing matches, plus the matched title/slug/status/type. |
post-get-content |
Read the raw post_content markup string (+ post_id, length). |
post-get-meta |
Read post meta plus the full post object for migration (title, slug, status, type, excerpt, parent, menu_order, author, date, modified, comment/ping status, password, featured_image_id). |
post-create |
Create a post / page / CPT in one call: every wp_insert_post field plus _thumbnail_id (featured image), meta_input, and tax_input (assign terms). Returns the new post_id. |
post-update |
Update post-level fields (pass-through to wp_update_post) plus _thumbnail_id, meta_input, tax_input. |
post-update-content |
Replace post_content with new Gutenberg markup. The primary tool for ALL block edits. |
post-meta-update |
Set/delete arbitrary core post meta (update_post_meta; pass null to delete). For ACF use acf-field-update. |
post-trash |
Move a post to the trash, or restore it (action: "trash" | "untrash"). Permanent deletion is intentionally not supported. |
| Tool | Purpose |
|---|---|
media-upload |
Bring an EXTERNAL file into the library from a remote URL or local filesystem path. Optionally set alt, title, caption, description, post_parent. Returns attachment id + url. |
media-migrate |
Push an existing library file from THIS site to a remote WordPress site (e.g. local → staging), server-to-server. The source attachment's alt/title/caption/description are carried over (override-able). The agent passes only attachment_id — file bytes never travel through the agent. Returns the remote attachment id + url. See Media migration below for required config. |
| Tool | Purpose |
|---|---|
nav-menu-list |
List all menus (or one) with their items, theme locations, and per-item object_id/type/parent/classes/target. Round-trips with nav-menu-create. |
nav-menu-create |
Create a nav menu, optionally assign to a theme location, optionally seed items (custom links, posts, taxonomies, nested via parent_index). |
| Tool | Purpose |
|---|---|
acf-field-get |
Read ACF field values for a post / term / user / options page. Reverses textarea render markup (raw); expands image/file/gallery fields to {id,url,filename} in a media map (expand_media, default on). |
acf-field-update |
Update one or more ACF fields (object_id: numeric post ID, "term_{id}", "user_{id}", or "option"). Returns read-after-write values; an already-equal value counts as updated, not failed. |
acf-field-schema |
Read the STRUCTURE (not values) of ACF field groups for a target: field names/keys/types, nested sub_fields/layouts (repeater/group/flexible), and choices. Use before writing complex fields. |
| Tool | Purpose |
|---|---|
options-get |
Read wp_options rows (type: "option") or theme mods (type: "theme_mod"); for theme mods, omit names to return all. Read counterpart to options-update. |
options-update |
Bulk-update wp_options rows (type: "option") or theme mods (type: "theme_mod"). |
| Tool | Purpose |
|---|---|
widgets-list |
List all sidebars and the widgets assigned to each (widget_id, type, settings). |
widget-add |
Add a widget to a sidebar (type + settings, optional position). |
widget-update |
Update a widget's settings (merge, or replace). |
widget-remove |
Remove a widget from a sidebar (and delete its stored settings). |
There are no partial-block mutation tools (block-add / update / replace / move / remove were removed; index-based mutation was fragile). To edit any block content:
- Optional:
block-tree(inspect current structure) orpost-get-content(raw markup string). block-schemafor every block type you're about to write (validates attribute shape).post-update-contentwith the full new Gutenberg markup.
Round-trip cost is small in practice and produces predictable, debuggable diffs.
media-migrate follows the WP Migrate DB Pro push model: it runs on the source site (e.g. local), reads an attachment from disk, and POSTs it server-to-server to the remote site's built-in wp/v2/media REST endpoint. The agent passes only attachment_id, so file bytes never pass through the agent's context.
Because the source site makes the outbound HTTP call itself (it is not the authenticated MCP caller), it must hold the remote's credentials. Configure them once on the source site's wp-config.php:
define( 'XFIVE_MCP_REMOTE_URL', 'https://staging.example.com' );
define( 'XFIVE_MCP_REMOTE_USER', 'your-remote-username' );
define( 'XFIVE_MCP_REMOTE_APP_PASSWORD', 'xxxx xxxx xxxx xxxx xxxx xxxx' );The application password is generated on the remote site at Users → Profile → Application Passwords; that user needs the upload_files capability. The remote site must be reachable over HTTP from the source server.
Notes:
- Allowed types: jpeg, png, gif, webp, svg, mp4, webm, ogg, mov.
- The file is sent in a single request, so large files (video) are bound by the remote's
upload_max_filesize,post_max_size, andmemory_limit(HTTP 413 indicates the limit was hit). - The remote regenerates its own thumbnail/size variants; only the original file is transferred.
The MCP REST endpoint uses HTTP Basic Auth with WordPress application passwords. For local development, define MCP_OPEN truthy in wp-config.php to bypass auth and run as the first administrator user (already configured in this site).
define( 'MCP_OPEN', true );The server is reached over HTTP via the @automattic/mcp-wordpress-remote proxy, which bridges stdio MCP clients to the WordPress REST endpoint.
Drop a .mcp.json at the project root:
{
"mcpServers": {
"xfive-mcp-chisel": {
"type": "http",
"url": "http://chisel-ai-mcp-test.test/wp-json/xfive-mcp/mcp"
}
}
}WP_API_URL points at this plugin's route (xfive-mcp/v1 namespace → mcp route).
-
MCP_OPENmode (local dev). Whendefine( 'MCP_OPEN', true )is set inwp-config.php, the endpoint accepts unauthenticated requests and runs as the first admin. OmitWP_API_USERNAME/WP_API_PASSWORD— they're not needed. -
Application passwords (staging / shared). Generate one at Users → Profile → Application Passwords and add:
"env": { "WP_API_URL": "http://your-site.test/wp-json/xfive-mcp/mcp", "WP_API_USERNAME": "your-username", "WP_API_PASSWORD": "your-application-password" }
Other MCP-capable clients (Codex, Cursor, Windsurf, Cline, Zed, etc.) use the same proxy command / args / env block, but each reads it from its own config file and may wrap it in a different top-level schema. Check the tool's MCP docs for the exact filename and structure — the inner server definition is portable.
inc/
├── Abilities/ # One class per MCP tool, all extend AbilitiesBase
│ ├── AbilitiesBase.php
│ ├── BlockSchema.php
│ ├── BlockTree.php
│ ├── PostList.php
│ ├── PostByTitle.php
│ ├── PostCreate.php
│ ├── PostUpdate.php
│ ├── PostUpdateContent.php
│ ├── PostGetContent.php
│ ├── PostGetMeta.php
│ ├── PostMetaUpdate.php
│ ├── PostTrash.php
│ ├── MediaUpload.php
│ ├── MediaMigrate.php
│ ├── NavMenuList.php
│ ├── NavMenuCreate.php
│ ├── AcfFieldGet.php
│ ├── AcfFieldUpdate.php
│ ├── AcfFieldSchema.php
│ ├── OptionsGet.php
│ ├── OptionsUpdate.php
│ └── Widget{sList,Add,Update,Remove}.php
├── WP/
│ ├── Plugin.php # Bootstrap (singleton)
│ ├── Abilities.php # Registers each ability with wp_register_ability()
│ └── MCP.php # Creates the MCP server via mcp_adapter_init
└── Trait/
├── Singleton.php
└── Config.php # Inventory of which tools live in which categoryTool registration is data-driven from Trait/Config.php: add an entry there, create the matching class in inc/Abilities/, and Abilities::register_abilities() picks it up on wp_abilities_api_init.