Documentation
Everything WP MCP does, and exactly how the safety net works.
WP MCP is the open-source plugin wpmcp, built on the official WordPress Abilities API. This section is for developers, agencies, and anyone connecting an AI client over MCP.
Introduction
wpmcp is a WordPress plugin that turns a site into an MCP (Model Context Protocol) server. It lets an AI agent, Claude, Cursor, or any other MCP-compatible client, read and edit a WordPress site directly: create and update posts and pages, manage media, adjust settings, and more.
The core promise
Handing an AI agent write access to a live WordPress site is risky. A bad edit on a client’s production site is the kind of mistake that is hard to walk back, and every other WordPress AI tool asks you to trust the model to get it right.
wpmcp takes the opposite approach: it assumes the agent will get something wrong eventually, and makes sure that nothing it does is permanent.
Nothing an agent does through wpmcp is unrecoverable. Every write goes through a safety engine that snapshots the affected object before the change happens. If the change breaks something, it can be rolled back, either by the agent itself (via a rollback tool) or by a human, with one click, from the WordPress admin screen. You can undo a single operation, or unwind an entire agent session back to where it started.
How this is different
Most AI-for-WordPress tools focus entirely on the building and editing experience. wpmcp treats recoverability as the headline feature, not an afterthought:
- Enforced in code, not by convention. No tool that mutates the database can bypass the safety wrapper. This is a structural guarantee, not a guideline the code happens to follow.
- Works at the WordPress data layer. Snapshots capture the actual post row, meta, terms, and comments, so rollback does not depend on understanding any particular page builder’s internal format.
- Runs inside WordPress. wpmcp is a single WordPress plugin built on the official WordPress Abilities API, not a separate Node process or local proxy. One install, and it works with any MCP client.
- Free and open where it matters. The safety engine and the core content/media/settings tools are free and GPL-2.0 licensed.
Who this is for
- Developers and agencies who want to let an AI agent build or maintain WordPress sites (including client sites) without babysitting every change.
- MCP client builders and power users connecting Claude Code, Claude Desktop, Cursor, or another MCP client to a WordPress install.
- Anyone nervous about giving an AI agent write access to production, and wants an undo button that actually works.
Project status
wpmcp is actively developed and past its feature-parity milestone: the safety engine (snapshot, apply, verify, rollback) and 188 registered abilities (163 free, 25 Pro, including Elementor deep editing, the structural suite, and Bricks/Divi support) are shipped and covered by ~2,000 tests. A few integration paths (Polylang, multisite network calls, Google analytics/Search Console) are verified in production rather than CI, and release notes flag those honestly per release. Where this documentation describes a planned feature, it says so explicitly.
Where to go next
- Getting started: requirements and installation.
- Connecting clients: wiring up Claude Code, Cursor, or Claude Desktop.
- The safety model: how snapshot, rollback, and recovery actually work under the hood.
- Tools reference: every MCP tool wpmcp ships, what it does, and whether it is safety-wrapped.
Getting Started
Requirements
| Dependency | Version |
|---|---|
| WordPress | >= 6.9 (this version bundles the Abilities API that wpmcp registers its tools on) |
| PHP | >= 8.1 |
| Composer | needed to install from source |
| MySQL / MariaDB | whatever your WordPress install already uses |
wpmcp is not yet listed on the wp.org plugin directory (this is planned). For now it is installed from source.
Install from source
Clone the plugin directly into your wp-content/plugins directory and install its dependencies:
git clone https://github.com/wpmcp/wpmcp.git wp-content/plugins/wpmcp
cd wp-content/plugins/wpmcp
composer install --no-dev
--no-dev skips the PHPUnit/test tooling, which you do not need on a production or staging install. If you plan to contribute to the plugin itself, see Contributing and tests for the full dev setup instead.
Activate the plugin
- Log into wp-admin.
- Go to Plugins.
- Find wpmcp and click Activate.
On activation, wpmcp creates its snapshot storage table (wp_wpmcp_snapshots) and registers a wpmcp top-level admin menu item. That screen lists recent agent operations and lets a human restore any of them with one click, the same rollback mechanism the MCP tools use, just exposed to a person instead of an agent.
What activation does not do
Activating the plugin does not, by itself, expose write access to the outside world. An MCP client still needs valid WordPress credentials (an Application Password) to call any tool, and every ability is capability-checked against edit_posts before it runs. See Connecting clients for how to actually wire up an AI agent.
Next steps
- Connecting clients: generate an Application Password and point an MCP client at your site.
- The safety model: understand what happens before and after every write.
Connecting an MCP Client
wpmcp exposes its tools over the Model Context Protocol via the official WordPress Abilities API. Every ability wpmcp registers requires a caller to be authenticated as a WordPress user who can edit_posts, so the first step is generating credentials.
Step 1: create an Application Password
wpmcp authenticates over WordPress’s built-in Application Passwords feature. No separate API key system, no additional plugin.
- In wp-admin, go to Users -> Profile (or edit the specific user the agent should act as).
- Scroll to Application Passwords.
- Give it a name (e.g.
claude-code) and click Add New Application Password. - Copy the generated password immediately. It is shown once.
Use a dedicated user for agent access rather than your own admin account, so you can revoke it independently and see its actions clearly in the operations list.
Step 2: encode the credential
MCP clients that speak HTTP typically expect a Basic auth header, which is the base64 encoding of username:application-password:
echo -n "your-username:xxxx xxxx xxxx xxxx xxxx xxxx" | base64
Keep the spaces in the application password exactly as WordPress generated them before encoding.
Step 3: point your client at the MCP endpoint
Claude Code
Add an entry to your project’s .mcp.json:
{
"mcpServers": {
"wpmcp": {
"type": "http",
"url": "https://your-site.com/wp-json/mcp/wpmcp-server",
"headers": {
"Authorization": "Basic BASE64_OF_username:application-password"
}
}
}
}
Replace your-site.com with your actual domain and the Authorization value with the base64 string from step 2.
Cursor and Claude Desktop
Both are MCP-compatible clients, and the same endpoint and Basic-auth header work in their respective MCP server configuration. The .mcp.json example above will need to be adapted to whatever configuration format the client expects, but the URL and header are identical: no wpmcp-specific setup beyond the Application Password.
Verifying the connection
Once connected, ask your client to call the list-post-types or list-posts tool. If you get a result back, the connection and auth are working. If you get a 401/403, double-check the Application Password encoding and that the WordPress user has at least the edit_posts capability (an Editor or Administrator role satisfies this by default).
See Tools reference for the full list of available tools once you are connected.
The Safety Model
This is the part of wpmcp that the rest of the product exists to support. Every tool that mutates existing data routes through a single orchestrator, Safe_Mutation::run(). No exceptions, and this is enforced in code: a tool cannot write to a post or option except by calling this method.
The snapshot -> apply -> verify -> rollback flow
snapshot (before) -> apply the change -> verify -> ok?
| | |
stored in on failure: return
wp_wpmcp_snapshots auto-rollback operation id
(keyed by operation + session) + raise error
- Snapshot. Before the mutation runs,
Snapshot::capture()records the target’s before-image andSafe_Mutationstores it, gzip-compressed, in thewp_wpmcp_snapshotstable, keyed by a generated operation ID (a UUID) and the calling session’s ID. - Apply. The tool’s actual mutation callback runs (e.g.
wp_update_post(),update_option(),wp_delete_post()). - Verify (optional, tool-specific). Some tools pass a verification callback.
update-blocks, for example, checks that the new block markup still parses as valid Gutenberg blocks. If verification fails,Safe_Mutationimmediately restores the snapshot and throws aMutation_Failedexception, so a bad write never survives past the same request. - Rollback, on demand. Separate from the automatic verify-and-revert above, any successfully-applied operation can be undone later via the
rollback-operationorrollback-sessiontools, or from the wpmcp wp-admin screen.
Every write tool returns an operation_id in its response specifically so a caller (human or agent) can reference it later for rollback.
What gets captured
Snapshot capture is dispatched by object type (src/Safety/Snapshot.php), and now covers five object types: post, option, user, comment, and wc_order. This is what makes product edits, order status changes, comment edits, and menu edits undoable through the same engine as posts and settings, not a separate one-off mechanism per domain.
Posts (and attachments, which are WordPress posts of type attachment):
- The full post row, every column from
get_post($id, ARRAY_A), not a hand-picked subset of fields. This matters for the resurrection path below: a partial capture would mean a force-deleted post comes back missing its original post type, author, parent, slug, dates, or menu order, silently rebuilt fromwp_insert_post()’s defaults. - All post meta (
get_post_meta($id)), every key and value. - Taxonomy term assignments, captured per taxonomy registered on that post type via
wp_get_object_terms(). - Comments and their comment meta, captured only for use by the force-delete resurrection path (see below). WordPress’s
wp_delete_post($id, true)destroys comments and commentmeta with no equivalent in the trash or in-place-update paths, so they have to be captured up front to be restorable.
A WooCommerce product is a post (post_type = product) and a navigation menu item is a post (post_type = nav_menu_item), so update-product, delete-product, update-menu-item, and remove-menu-item all restore exactly through this same full-row-plus-meta-plus-terms path: price, stock, description, title, url, parent, and position all come back as they were.
Options (used by update-settings and assign-menu-to-location):
- The option’s current value.
- Whether the option existed before the write. Options have no trash/soft-delete equivalent: a write either changes an existing value or introduces a brand-new option. Recording
existedlets rollback choose between putting the old value back (update_option()) or removing the option entirely (delete_option()) if it wasn’t there before.
Users (used by update-user):
- The user’s core profile fields and all usermeta, captured before any change so
rollback-operationrestores display name, email, url, nickname, name fields, and description exactly. Role and password are never touched by any wpmcp tool, so neither is part of this capture.
Comments (used by moderate-comment, edit-comment, and delete-comment):
- The comment row and its comment meta.
delete-comment‘s resurrection path reinserts the comment viawp_insert_comment(); the content, author, dates, and post association are restored, but (like force-deleted posts’ comments) the comment gets a new auto-increment ID, since WordPress core does not let a caller choose one.
WooCommerce orders (used by update-order-status):
- The order’s prior status, captured via the
wc_orderobject type so it is HPOS- and CPT-safe (it does not assume orders are stored as posts).rollback-operationrestores the exact prior status.
Snapshots are serialized with gzencode(wp_json_encode($before)) and stored in a LONGBLOB column.
Operation rollback vs session rollback
Two distinct undo scopes, both backed by the same snapshot table:
rollback-operationrestores exactly one snapshot, identified by itsoperation_id. Simple: look up the row, apply that snapshot.rollback-sessionunwinds an entire agent session. It pulls every snapshot recorded under asession_id, walks them oldest-first, and for each distinct object restores only the earliest snapshot seen, that object’s state from before the session touched it at all. If the same post was edited three times in one session, only the first (pre-session) snapshot is applied; the two later ones are skipped once the object’s identity has already been restored. The tool’s return value (restored_count) counts snapshot rows processed, not distinct objects restored, so it can be larger than the number of objects that actually changed.
Object identity for deduplication is object_type:object_id for posts, users, comments, and orders, and option:<option name> for options (the raw database object_id column is always 0 for option rows, since options are identified by name, not a numeric ID; the real name lives inside the serialized blob).
The meta-purge
Rollback is a full restore, not an additive merge. If the mutation being undone added a new meta key that did not exist at snapshot time, a rollback that only restored the snapshotted keys would leave that new key behind as orphaned meta, i.e. the object would not be truly back to its pre-mutation state.
To prevent that, Rollback_Service::apply_snapshot():
- Diffs the object’s current meta against the snapshotted meta.
- Deletes any meta key present now but absent from the snapshot (added by the mutation being undone).
- Deletes and re-adds every snapshotted key/value pair exactly as captured.
This is what makes rollback exact rather than approximate: a restored object matches its pre-mutation state, including the absence of anything the agent added.
Force-delete and resurrection, with ID verification
Trashing a post (delete-post without force: true) is not routed through Safe_Mutation at all: WordPress’s own trash already makes it reversible, so a redundant snapshot would buy nothing. Force-deleting (force: true) permanently removes the post row, so that path is safe-wrapped.
When a force-deleted post needs to be rolled back, the row no longer exists, so a plain wp_update_post() would silently no-op. Instead, Rollback_Service re-inserts the post at its original ID using wp_insert_post()’s import_id parameter, then replays the captured comments on top of it via wp_insert_comment() (comment IDs themselves cannot be preserved, WordPress core always assigns a new auto-increment comment ID, but content, author, dates, and thread association with the post are restored).
Two safety checks guard this resurrection path:
- Identity check before choosing the restore path. If a post already exists at the target ID, wpmcp does not assume it’s safe to just update it in place. It compares
post_date_gmt(immutable after creation) between the live row and the snapshot. If they don’t match, the row at that ID is a different post that has since reclaimed the ID (e.g. someone manually re-imported content after the original was force-deleted), and blindly updating it would silently overwrite an unrelated post. In that case rollback routes through the resurrection path instead, which triggers the next check. - ID collision check after resurrection.
wp_insert_post()’simport_idis only honored if that ID is still free; on a collision it silently falls back to a new auto-increment ID instead of erroring. wpmcp checks the returned ID against the ID it asked for, and if they don’t match, throws aMutation_Failedrather than leaving a “restored” post sitting at the wrong ID with no error. A rollback that silently succeeds at the wrong ID would be worse than one that fails loudly.
Known limitations, stated honestly
Free-tier history retention bounds session rollback. Gate::history_limit() returns 20 for the free tier (unlimited, PHP_INT_MAX, for Pro). After every write, Safe_Mutation::run() calls Snapshot_Store::prune(), which deletes all snapshot rows beyond the most recent N. This pruning is not currently session-aware: it prunes purely by recency across the whole table, not per-session. On a free-tier site, an agent session that performs more than 20 total operations (across any objects) can lose its earliest snapshots before the session ends. In that case, rollback-session restores each object to the earliest surviving snapshot rather than guaranteed to its true pre-session state. This does not affect Pro (unlimited history). Making pruning session-aware, so a snapshot belonging to a still-active session is never pruned within its retention window, is on the roadmap.
Media force-delete does not restore file bytes. Force-deleting an attachment (or deleting one without MEDIA_TRASH defined) permanently unlinks the physical file from disk. Rollback via Safe_Mutation restores the media’s database record faithfully, the post row, all meta, and taxonomy terms, but it cannot restore bytes already deleted from the filesystem. The delete-media tool response signals this explicitly with "files_recoverable": false and a warning field pointing at the tracking issue. Full file-level recovery is tracked as issue #24 and is not yet implemented. Because of this, delete-media is disabled by default: a site must opt in via the wpmcp_enable_delete_media filter, and every call additionally requires confirm: true.
Note on wording: an earlier design-spec draft stated that snapshot capture does not record taxonomy terms. That is now out of date. The current Snapshot::capture() implementation does capture per-taxonomy term assignments for posts, and Rollback_Service::apply_snapshot() restores them via wp_set_object_terms(). What is genuinely not yet captured is a small set of secondary post fields not covered by the full-row capture’s practical use today (the design intentionally captures the entire row, so this gap is narrower than earlier drafts suggested); consult src/Safety/Snapshot.php directly if you need the exact current field list, since this is an area still evolving.
Driving rollback
- From an agent: call the
rollback-operationorrollback-sessionMCP tools directly (see Tools reference). - From a human: open the wpmcp screen in wp-admin, which lists recent operations (via the same
list-operationsdata) with a one-click Restore button per row.
Tools Reference
WP MCP currently registers 188 abilities in the canonical test environment: 163 free and 25 Pro. Every tool below is registered as a WordPress ability named wpmcp/<tool-name> via the official Abilities API, each with its own permission_callback (content tools require edit_posts; sensitive domains are gated by stronger capabilities).
Every mutating tool routes through Safe_Mutation::run(), so it is snapshotted before it runs and can be undone with rollback-operation or rollback-session, except the small set of deliberately risky operations that are disabled by default and honestly say so in their descriptions. See The safety model for how the engine works.
This page is generated from the plugin source and its ability manifest; the descriptions below are the exact ones the AI agent sees.
Safety & history (3 tools)
list-operations: List recent safety snapshot operationsrollback-operation: Undo a single operation by restoring its pre-change snapshotrollback-session: Undo all operations from a session by restoring each object’s pre-session snapshot
Content & pages (13 tools)
create-post: Create a post, page, or custom post typedelete-post: Delete a post, page, or custom post type. Trash by default (reversible). force:true permanently deletes: that path is disabled by default (site must opt in via the wpmcp_enable_delete_post filter) and requires confirm:true. Force-delete is snapshotted so the record can be rolled backget-page: Read a pageget-post: Read a single post, page, or custom post typeget-revision: Read a single post revision’s fieldslist-post-types: List registered post types (posts, pages, custom post types)list-posts: List/search posts, pages, or custom post typeslist-revisions: List a post’s revisions (id, author, date, change excerpt)list-taxonomies: List registered taxonomies (categories, tags, custom taxonomies)restore-revision: Restore a post to a given revisionset-post-terms: Assign taxonomy terms to a post (replace, append, or remove)update-blocks: Update a page’s block contentupdate-post: Partially update a post, page, or custom post type
Gutenberg blocks (5 tools)
convert-html-to-blocks: Convert raw HTML into valid Gutenberg block markup. Maps common top-level elements to core blocks (h1-h6 to core/heading, p to core/paragraph, img to core/image, ul/ol to core/list, blockquote to core/quote, pre/code to core/code, hr to core/separator, table to core/table); anything unrecognized is wrapped in a core/html block so no content is lost. A pure transform, not a database write: it never touches a post. To write the resulting markup to a post use the existing update-blocks toolget-block-type: Return full detail for a single registered block type by name: its attributes schema, declared supports, and block-context wiring (uses_context, provides_context). Read-onlylist-block-types: List the block types registered with WP_Block_Type_Registry: name, title, category, whether the block renders dynamically (is_dynamic), and its declared attribute names. Optional category (exact match) and/or search (substring match on block name) filters narrow the result. Read-onlyparse-blocks: Parse block markup into its block tree via parse_blocks(). Accepts either “blocks” (raw markup) or “id” (an existing post, parses its post_content). Each node reports blockName, attrs, recursively parsed innerBlocks, and an innerHTML summary. Read-onlyserialize-blocks: Serialize a block tree (as produced by parse-blocks, or any array shaped the same way) back into valid block markup via serialize_blocks(). A pure transform, not a database write: it never touches a post. To write the resulting markup to a post use the existing update-blocks tool
Surgical block edits & patterns (7 tools)
add-block: Surgically insert ONE block (given as “” delimited markup) into a post so it lands at “path” (array of zero-based indexes into the parse-blocks tree; the final segment may equal the sibling count to append; nested paths descend innerBlocks). Requires expected_hash (the content_hash from parse-blocks) and refuses stale reads. Snapshot-first; every other block stays byte-identicalduplicate-block: Duplicate the block at “path” (deep copy, inserted immediately after the original within the same parent) and return the copy’s new_path. Requires expected_hash (the content_hash from parse-blocks) and refuses stale reads. Snapshot-firstinsert-pattern: Insert a registered block pattern’s parsed blocks into a post starting at “path” (same path semantics as add-block; pure-whitespace filler nodes are dropped). Requires expected_hash (the content_hash from parse-blocks) and refuses stale reads. Snapshot-first; every pre-existing block stays byte-identicallist-patterns: List the block patterns registered with WP_Block_Patterns_Registry: name, title, description, and categories. Optional search (case-insensitive substring match on name or title) narrows the result. Pattern markup is inserted server-side by insert-pattern, so it is not returned here. Read-onlymove-block: Move the block at “from_path” to position “to_index” among its own siblings (same parent only; compose remove-block + add-block to move across parents). Requires expected_hash (the content_hash from parse-blocks) and refuses stale reads. Snapshot-firstremove-block: Surgically remove ONE block by “path” (array of zero-based indexes into the parse-blocks tree, descending innerBlocks); nested removals keep the container wrapper intact. Requires expected_hash (the content_hash from parse-blocks) and refuses stale reads. Snapshot-first and fully restorable via rollback-operationupdate-block: Surgically update ONE block in place by “path” (array of zero-based indexes into the parse-blocks tree, descending innerBlocks): replace its attributes (“attrs”, full replacement) and/or its inner HTML (“inner_html”, leaf blocks only, target a container’s children by their own paths). Requires expected_hash (the content_hash from parse-blocks) and refuses stale reads. Snapshot-first; every other block stays byte-identical
Composite page builds (1 tool)
build-page: Compose a complete page from ONE declarative spec: title, a recursive sections/blocks tree, media references (existing attachment ids), and optional menu placement. The whole composition is a single atomic, recoverable operation: the spec is strictly validated (node-path-addressed errors, bounded size/nodes/depth) before any write, a mid-build failure automatically removes everything it created, and on success one operation_id is returned whose rollback-operation removes the page and its menu placement entirely. Markup is composed deterministically from the spec; nothing in the spec is evaluated or executed. dialect “gutenberg” (default, free) builds block markup; dialect “elementor” (PRO, requires Elementor) builds an _elementor_data element tree
Media library & stock images (11 tools, 1 Pro)
delete-media: Delete a Media Library attachment. Disabled by default (site must opt in via the wpmcp_enable_delete_media filter) and requires confirm:true. force:true permanently deletes, routed through the safety snapshot so it can be rolled backget-media: Read full detail for a Media Library attachment: title, URL, every registered image size, dimensions, mime type, alt text, caption, and descriptionimport-stock-image: Sideload a stock search result into the Media Library. The fetch is SSRF-guarded: https-only, host allowlist checked before any request (wpmcp_remote_media_allowed_hosts filter), redirects refused, size caps enforced, and the bytes must verify as a real image. Attribution/license metadata is persisted on the attachment; rollback deletes the importinsert-stock-image, Pro: Composite stock-image flow: run the same SSRF-guarded import as import-stock-image, then insert the image into the post’s builder content as a Gutenberg image block. Returns two independently rollbackable operation ids (undo the insert, undo the import)list-media: List Media Library attachments with type (“image” or an exact mime like “image/png”), date-range (after/before), and search filters, paged newest first with a total/pages enveloperesize-media: Regenerate the specified registered image sizes for an attachment from its original file and report each resulting file (name, dimensions, URL). Snapshot-first with a physical-file backup, so the operation is rollbackablesearch-stock-images: Search openly-licensed stock images. Providers: openverse (keyless, Creative Commons results), pexels and unsplash (bring-your-own key via set-stock-key). Results are provider-attributed with license, license_url, attribution, and source_url, ready to pass to import-stock-imageset-stock-key: Store (or clear, by passing an empty api_key) a bring-your-own stock-provider API key for pexels or unsplash. Keys are encrypted at rest with a site-salt-derived key and are never echoed backsideload-image: Download an image from a URL and add it to the Media Library as a new attachmentupdate-media: Update a Media Library attachment’s title, alt text, caption, and/or descriptionupload-svg: Add an SVG to the Media Library from raw markup or an allowlisted URL. Every SVG passes a bundled fail-closed sanitizer (script/foreignObject/event handlers/external references are rejected outright); only the sanitized markup is stored. Rollback deletes the upload
Options & post meta (4 tools)
get-option: Read a single wp_options value by name. Refuses a conservative denylist of sensitive/core option names (auth keys and salts, siteurl, home, active_plugins, and secret/password/token-shaped names)get-post-meta: Read a post’s meta, either the full map or a single key. Protected meta (a leading underscore, or is_protected_meta) is always skippedset-post-meta: Set a single meta key/value on a post. Refuses protected meta keys (a leading underscore, or is_protected_meta). Snapshotted via object_type post; rollback-operation restores the prior valueupdate-option: Update a single wp_options value by name. Refuses the same denylist as get-option, and is disabled by default until a site opts in with the wpmcp_enable_option_write filter. Snapshotted via object_type option; rollback-operation restores the prior value (or removes the option if it did not exist before)
Site settings (2 tools)
get-settings: Read WordPress site settings (general, reading, writing, discussion, media, permalinks), each with its group, type, and whether it is writableupdate-settings: Update WordPress site settings from a strict allowlist. Validates/coerces each value (enum, int range, bool), rejects unsafe permalink structures, skips read-only or non-allowlisted keys, and applies the valid subset even if some keys fail
Users (4 tools)
create-user: Create a new non-admin user. Auto-generates a strong password (never returned) and emails the new user so they can set their own. Rejects admin and unknown roles; defaults to subscriberget-user: Read one user’s profile detail, including an is_admin flag derived from live capabilities. Never returns the password hashlist-users: List WordPress users as safe summary rows (id, username, display name, email, roles, registration date). Never returns password hashes or other secretsupdate-user: Update a non-admin user’s profile fields (display name, email, url, nickname, first/last name, description). Refuses admin-capable users. Never changes role or password. Snapshotted so the change can be rolled back
Comments (5 tools)
delete-comment: Permanently delete a comment. Disabled by default (site must opt in via the wpmcp_enable_delete_comment filter) and requires confirm:true. Routed through the safety snapshot so it can be rolled back, though the resurrected comment gets a new IDedit-comment: Edit a comment’s content and/or author fields (name, email, url). Snapshotted so the change can be rolled backget-comment: Read one comment’s detail (post, parent, author fields, content, status, date)list-comments: List comments as safe summary rows (id, post, author, content, status, date), optionally filtered by post and moderation status, with pagingmoderate-comment: Change a comment’s moderation status: approve, unapprove, spam, trash or untrash. Snapshotted so the change can be rolled back
Navigation menus (9 tools)
add-menu-item: Add an item to a navigation menu (custom link by title and url, or an object link via type, object, object_id). Additive; a mistaken item can be removed with remove-menu-itemassign-menu-to-location: Assign a navigation menu to a registered theme location. The assignment lives in the nav_menu_locations theme_mod, so this is snapshotted via object_type option and rollback-operation restores the prior assignmentcreate-menu: Create a new navigation menu (a nav_menu term). Creation has no prior state to snapshot; a mistaken menu can be removed with delete-menudelete-menu: Delete a navigation menu (a nav_menu term). Disabled by default (site must opt in via the wpmcp_enable_delete_menu filter) and requires confirm:true. This is not automatically reversible: the menu name and its items are returned so it can be rebuilt manuallyget-menu: Read one navigation menu with its ordered items (id, title, url, type, parent, order)list-menu-locations: List the theme’s registered menu locations and the menu (if any) assigned to eachlist-menus: List the site’s navigation menus as safe summary rows (id, name, slug, item count)remove-menu-item: Remove an item from a navigation menu. The item is a post, so this is snapshotted via object_type post and rollback-operation resurrects it at its original id, re-attached to its menuupdate-menu-item: Update a navigation menu item’s title, url, parent, or position. A menu item is a post, so this is snapshotted via object_type post and rollback-operation restores the prior values exactly
Plugins & themes (13 tools)
activate-plugin: Activate an installed plugin. Snapshots the prior active_plugins option so it can be rolled backdeactivate-plugin: Deactivate a plugin. Refuses protected packages (wpmcp, Elementor). Snapshots the prior active_plugins option so it can be rolled backdelete-plugin: Permanently delete an installed plugin’s files. Disabled by default (wpmcp_enable_delete_plugin filter) and requires confirm:true. Refuses protected or active plugins. Not rollback-abledelete-theme: Permanently delete an installed theme’s files. Disabled by default (wpmcp_enable_delete_theme filter) and requires confirm:true. Refuses the active theme (or its active parent). Not rollback-ableget-plugin-info: Fetch full wordpress.org plugin directory info for a slug: version, rating, installs, homepage, download link, and compatibilityinstall-plugin: Install a plugin from wordpress.org by slug, optionally activating it. Additive only; nothing to roll backinstall-theme: Install a theme from wordpress.org by slug, optionally activating it. Additive only; nothing to roll backlist-plugins: List installed plugins with active status, protected-package flag, and pending update infolist-themes: List installed themes with active status, parent theme, and pending update infosearch-plugins: Search the wordpress.org plugin directory by keyword, with optional tag/author filters and a capped per_pageswitch-theme: Activate (switch to) an installed theme. Snapshots the prior template/stylesheet options so it can be rolled backupdate-plugin: Update an installed plugin to the latest wordpress.org version. Disabled by default (wpmcp_enable_update_plugin filter) and requires confirm:true. File changes are not rollback-ableupdate-theme: Update an installed theme to the latest wordpress.org version. Disabled by default (wpmcp_enable_update_theme filter) and requires confirm:true. File changes are not rollback-able
WooCommerce (11 tools)
add-order-note: Add an internal or customer-facing note to a WooCommerce order. Additive only; nothing to roll backcreate-product: Create a simple WooCommerce product via the CRUD layer. Creation has no prior state to snapshot; a mistaken product can be removed with delete-productdelete-product: Delete a WooCommerce product (trash by default, force for permanent). Disabled by default (site must opt in via the wpmcp_enable_delete_product filter) and requires confirm:true. Snapshotted so it can be rolled back: force-delete resurrects the product at its original id with its price, stock, and termsget-order: Read full detail for one WooCommerce order (status, billing email, payment method, line items, customer note). HPOS- and CPT-safeget-product: Read full detail for one WooCommerce product (prices, stock, description, categories, tags)get-sales-report: Read-only sales summary over a date range: order count, gross sales, items sold, and top products by quantity. Aggregated over wc_get_orders() (HPOS- and CPT-safe)list-orders: List WooCommerce orders as safe summary rows (id, number, status, total, currency, date), filterable by status and customer, with paging. HPOS- and CPT-safelist-product-categories: List WooCommerce product categories (the product_cat taxonomy) as summary rows (id, name, slug, parent, count)list-products: List WooCommerce products as safe summary rows (id, name, sku, price, stock status), filterable by search, status, type, or category, with pagingupdate-order-status: Change a WooCommerce order’s status, validated against the store’s registered statuses. Snapshotted via the wc_order object type so rollback-operation restores the prior status exactly. HPOS- and CPT-safeupdate-product: Update a WooCommerce product’s fields (price, stock, description, etc.). A product is a post, so this is snapshotted via object_type post and rollback-operation restores the prior price and stock exactly
SEO metadata (Yoast SEO / Rank Math) (3 tools)
get-seo-meta: Read a post’s SEO title, meta description, focus keyword, canonical URL, and robots flags (noindex/nofollow) via the active SEO plugin’s postmeta keysget-seo-status: Report which SEO plugin (Yoast SEO or Rank Math) is active on this site, by name and versionupdate-seo-meta: Set a post’s SEO title, meta description, focus keyword, canonical URL, and/or robots flags (noindex/nofollow) via the active SEO plugin’s postmeta keys. A field value is ordinary postmeta, so this is snapshotted via object_type post and rollback-operation restores the prior values exactly
Internal linking (3 tools)
find-orphan-posts: List published posts or pages that have zero incoming internal links (orphans), by scanning the most-recent posts for links that resolve to this site’s own contentget-link-map: Summarize the internal-link graph: per-post outgoing and incoming link counts, the orphan list, and the most-linked postssuggest-internal-links: Suggest related published posts a given post should link to, ranked by shared categories/tags and title keyword overlap, excluding posts it already links to
Content analysis (accessibility, SEO, contrast) (4 tools, 4 Pro)
analyze-accessibility, Pro: Scan a post’s stored HTML for common WCAG issues (images missing alt text, heading order jumps, empty or non-descriptive link text, and form controls without labels) and return scored findings with the offending element locations. Read-onlyanalyze-seo, Pro: Score a post’s on-page SEO (0-100) with severity-tagged findings: title and meta-description length, H1 and heading structure, word count, image alt coverage, internal/external link counts, focus-keyword density, and a Flesch reading-ease readability score. Read-onlycheck-contrast, Pro: Compute the WCAG contrast ratio between a foreground and background hex color and report AA/AAA pass/fail for normal and large text. Read-onlyextract-content, Pro: Extract a post’s readable plain text and a structural summary (headings, word count, link and image counts) from its stored content. Read-only
Elementor catalog (free) (2 tools)
get-widget-schema: Return the settings schema for one Elementor widget type: the curated typed params (defaults, enums, responsive hints, required plugin) for cataloged widgets by default, or the full introspected control stack with full:true (also the fallback for non-cataloged widgets). Read-onlylist-widgets: List Elementor registered widget types (name, title, categories, icon, tier, availability), annotated from the curated widget catalog (purpose line, cataloged flag). Filter by tier (free/pro), category, or a case-insensitive search over name/title/catalog keywords. Read-only
Elementor deep editing (7 tools, 7 Pro)
add-widget, Pro: Add a widget to a page’s _elementor_data under parent_id (or top level) at an optional position. Any cataloged widget_type (see list-widgets) takes typed params, validated against the curated schema before anything is written; non-cataloged registered widgets take raw settings. Requires expected_hash from get-elementor-data. Undoable via rollback-operationgenerate-widget, Pro: Generate a widget element of any cataloged type from the curated settings schema (see list-widgets / get-widget-schema) and insert it into a page’s _elementor_data, as a child of parent_id or at the top level when parent_id is omitted, with a deterministic seedable element id. Unknown types and invalid or missing required settings are rejected before anything is written. Undoable via rollback-operationget-elementor-data, Pro: Return a page’s parsed Elementor element tree (id, elType, widgetType, settings, and nested elements for every node), read directly from its _elementor_data postmeta. Read-onlymove-element, Pro: Reparent an element by id: remove it from its current location and append it as a child of a new parent element in the page’s _elementor_data. Refuses moves into the element itself or one of its own descendants. Undoable via rollback-operation since _elementor_data is ordinary postmeta captured by the existing post snapshotremove-element, Pro: Remove an element (and its children) from a page’s _elementor_data by id. Undoable via rollback-operation since _elementor_data is ordinary postmeta captured by the existing post snapshotupdate-element, Pro: Update an Elementor element’s settings by id, merging the given settings into its existing settings. Reads and writes the page’s _elementor_data; undoable via rollback-operation since _elementor_data is ordinary postmeta captured by the existing post snapshotupdate-widget, Pro: Patch a cataloged widget’s settings by element id from typed curated params (same schema add-widget uses; see get-widget-schema), validated and merged into the existing settings. Non-cataloged widgets are refused toward update-element. Requires expected_hash from get-elementor-data. Undoable via rollback-operation
Elementor structural suite (8 tools, 8 Pro)
add-container, Pro: Create an Elementor layout element (container by default, or section/column) at the top level or nested under parent_id, at an optional position among its siblings. Columns require a parent; widgets are never valid parents. Requires expected_hash from get-elementor-data (stale reads are refused with no partial write). Undoable via rollback-operationbatch-update, Pro: Apply N Elementor element settings updates atomically under ONE snapshot: every {element_id, settings} entry is validated before anything is written, one unknown id refuses the whole batch, and any failure rolls the entire batch back. Requires expected_hash from get-elementor-data. Undoable as a single rollback-operationduplicate-element, Pro: Deep-copy an Elementor element (and its whole subtree) with recursively regenerated ids, inserted immediately after the original among its siblings. Fresh ids use Elementor’s 7-char hex format and are checked against every id on the page, so the builder opens the result without warnings. Requires expected_hash from get-elementor-data. Undoable via rollback-operationfind-element, Pro: Search a page’s Elementor element tree by el_type, widget_type, setting_key + setting_value, and/or css_class token (criteria AND-combined; at least one required). Each match reports element_id, types, navigator label, and ancestor id path; the response carries the current data_hash so a structural mutation can be chained without a second read. Read-onlyreorder-elements, Pro: Reorder the children of one Elementor parent element (or the top level when parent_id is omitted) to an explicit id order. The order must be an exact permutation of the current children; anything else is refused before any write. Requires expected_hash from get-elementor-data. Undoable via rollback-operationset-element-label, Pro: Set an Elementor element’s navigator label (stored as the _title setting); an empty label clears the custom name. All other settings survive untouched. Requires expected_hash from get-elementor-data. Undoable via rollback-operationupdate-container, Pro: Merge settings non-destructively into an Elementor layout element (container, section, or column) by id: given keys are overwritten or added, all other settings survive. Widgets are refused (use update-element). Requires expected_hash from get-elementor-data. Undoable via rollback-operationupdate-page-settings, Pro: Merge settings non-destructively into a page’s Elementor page settings (_elementor_page_settings): given keys are overwritten or added, all other settings survive. Post field keys (post_title, post_status, template, …) are refused, use the post tools. Requires expected_hash = the settings_hash from get-elementor-data. Undoable via rollback-operation
Bricks & Divi builders (3 tools, 3 Pro)
detect-builder, Pro: Detect which page builder authored a post (elementor / bricks / divi / gutenberg / classic), by inspecting plain postmeta/post_content markers: Elementor’s _elementor_edit_mode, Bricks’ _bricks_page_content_2, Divi’s _et_pb_use_builder, or Gutenberg block comments in post_content, falling back to classic. Read-onlyget-builder-content, Pro: Return the raw builder structure for a post: for Bricks, the decoded _bricks_page_content_2 postmeta JSON; for Divi, the post_content shortcode string plus the use-builder flag. Returns a WP_Error for posts detected as elementor, gutenberg, or classic. Read-onlyupdate-builder-content, Pro: Replace the builder structure for a post. Bricks: validates the given string is well-formed JSON decoding to an array, then writes _bricks_page_content_2. Divi: validates the given content is a string, then writes post_content and ensures _et_pb_use_builder is on. Undoable via rollback-operation since both are ordinary postmeta/post_content captured by the existing post snapshot
Theme structure (sidebars, shortcodes) (4 tools)
list-shortcodes: List the shortcode tags registered in the global $shortcode_tags array: tag name and a short description of the registered callback where resolvable. Optional search (substring match on tag name) narrows the result. Read-onlylist-sidebar-widgets: List the widgets assigned to a single sidebar (by sidebar_id): widget id and display name, from wp_get_sidebars_widgets() resolved against the registered widgets. Read-onlylist-sidebars: List the sidebars/widget areas registered via register_sidebar(): id, name, description. Read-onlyrender-shortcode: Render a shortcode string (e.g. “[gallery ids="1,2"]”) via do_shortcode() and return the resulting HTML. Only invokes tags already present in the registered shortcode registry; input must contain an opening “[” or it is refused
Content export & import (3 tools)
export-content: Generate a WordPress eXtended RSS (WXR) export of site content via the native WordPress exporter (export_wp()). Optional content (post type: all/post/page/attachment/a custom post type), author, start_date, end_date, and status narrow what is included. Writes the XML to a protected directory under uploads and returns the file path, size, and item count. Read-only: does not mutate the site. WordPress’s own export_wp() can only be safely called once per PHP process (a core limitation, not specific to this tool), so a second call in the same long-lived process is refused with a clear message rather than fatalingimport-content: Import a WordPress eXtended RSS (WXR) file, creating posts via wp_insert_post() (title, content, status, post_type, postmeta). Disabled by default (site must opt in via the wpmcp_enable_import filter) and always requires confirm:true. Content creation at scale has no single object_type/object_id to snapshot, so this honestly reports recoverable:false; every created post id is returned in created_post_ids so a caller can follow up with delete-post for each one. Uses a lightweight built-in WXR parser, not the WordPress Importer pluginlist-exports: List the WXR export files previously generated by export-content: file name, size in bytes, and created timestamp for each. Read-only
Async backups (4 tools)
cancel-backup-job: Cancel a queued backup job: unschedule its WP-Cron event and mark it canceled. Refuses with an error if the job is no longer queued (already running or in a terminal status) or unknownget-backup-status: Return a backup job’s current record (status: queued/running/completed/failed/canceled, result artifact reference or error, timestamps) by job id. Read-onlylist-backup-jobs: List backup jobs, newest first, with an optional status filter (queued/running/completed/failed/canceled). Read-onlytrigger-backup: Queue an asynchronous backup job and schedule a WP-Cron event that produces the backup artifact (a WXR export via export-content) and flips the job’s status to completed or failed. Returns the job id immediately, before the backup itself has run, so a large-site backup does not have to complete within a single request
Database (6 tools)
delete-rows: Delete rows matching a mandatory equality WHERE via $wpdb->delete() (parameterized). Requires confirm:true. Refuses protected tables. Disabled by default (wpmcp_enable_db_writes filter). Snapshot-backed and restorable via rollback-operation (rows reinserted with their original primary-key ids) when the table has a primary key and the WHERE stays under the before-image cap; otherwise reports recoverable:false with a reason and logs the before-image to the write audit logdescribe-table: Return the columns, types, and keys of a database tableinsert-row: Insert a row into a table via $wpdb->insert() (parameterized). Refuses protected tables. Disabled by default (wpmcp_enable_db_writes filter)list-tables: List database tables with estimated row counts and sizesquery: Run a read-only SQL query (SELECT/SHOW/DESCRIBE/EXPLAIN/WITH). Writes, DDL, stacked statements, and file-access SQL are rejected before execution. Results are cappedupdate-rows: Update rows matching a mandatory equality WHERE via $wpdb->update() (parameterized). Requires confirm:true. Refuses protected tables. Disabled by default (wpmcp_enable_db_writes filter). Snapshot-backed and restorable via rollback-operation when the table has a primary key and the WHERE stays under the before-image cap; otherwise reports recoverable:false with a reason and logs the before-image to the write audit log
Filesystem (6 tools)
delete-file: Delete a file inside the WordPress install. Requires confirm:true. Backs up the file first (recoverable via restore). Refuses wp-config.php/.htaccess. Disabled by default (wpmcp_enable_fs_writes filter); requires edit_files and honors DISALLOW_FILE_EDITedit-file: Replace an exact string in a file (must match once unless replace_all). Backs up the original first (recoverable via restore). Refuses wp-config.php/.htaccess. Disabled by default (wpmcp_enable_fs_writes filter); requires edit_files and honors DISALLOW_FILE_EDITlist-directory: List entries (files/dirs with size and mtime) of a directory inside the WordPress install. Optional bounded recursive listingread-file: Read a file inside the WordPress installation (core, plugins, themes, uploads). Path is confined to the WP installsearch-files: Search file contents for a substring across a directory tree inside the WordPress install. Filterable by extension; results are cappedwrite-file: Create or overwrite a file inside the WordPress install. Backs up an existing file first (recoverable via restore). Refuses wp-config.php/.htaccess. Disabled by default (wpmcp_enable_fs_writes filter); requires edit_files and honors DISALLOW_FILE_EDIT
Scanners & cache (4 tools)
analyze-performance: Scan server configuration, WordPress internals (database size, autoloaded options, cron backlog, object cache, OPcache, plugin count), and a target page (defaults to the frontpage; pass “url” or “post_id” for a specific page) for performance issues and bottlenecks. Returns a scored report with severities and ranked, actionable recommendations. Read-only; analyzes this site onlyclear-cache: Flush this site’s caches: the object cache (wp_cache_flush), all transients (per-site and site-wide), OPcache when available and enabled, and any detected page-cache plugin cleared via its own API. Returns a per-layer summary of what was cleared versus not present. Safe and idempotent: clearing a cache has no meaningful before-image to restore, so it is not snapshotted or rolled backget-cache-status: Report which caching layers are active on this site: the persistent object cache backend (external vs internal), OPcache (available and enabled), and any active page-cache plugin (WP Rocket, W3 Total Cache, WP Super Cache, LiteSpeed Cache, WP Fastest Cache) detected by its signature functions or constants. Read-only; inspects this site onlyscan-security: Scan this site for security and malware problems across four areas: PHP malware heuristics (uploads plus active plugins/themes; pass deep=true for the whole tree), WordPress core file integrity (against official wordpress.org checksums), configuration hardening (file editor, debug output, admin username, XML-RPC, version disclosure, HTTPS, security headers), and outdated/abandoned software. Returns a scored report (0-100 plus A-F grade) with severities and ranked, actionable recommendations. Read-only; self-contained; scans this site only
Diagnostics & transients (4 tools)
delete-transient: Delete a single named transient via delete_transient(). Not snapshotted: transients are cache-like data with no meaningful before-image to restore, the same reasoning documented for clear-cacheget-debug-config: Report the debug-related constants (WP_DEBUG, WP_DEBUG_LOG, WP_DEBUG_DISPLAY, SCRIPT_DEBUG, SAVEQUERIES) and, when logging is on, the resolved debug.log path. Read-only, no secretsget-debug-log: Return a bounded tail (at most 200 lines / 64KB) of the WordPress debug log, never the whole file. Defaults to WP_CONTENT_DIR/debug.log or the WP_DEBUG_LOG custom path; any path argument is confined to WP_CONTENT_DIR, refusing traversallist-transients: List transients (name, expiry) from the options table, with an optional search substring filter and a capped limit (default 50, hard cap 500)
Advanced Custom Fields (3 tools)
get-fields: Read a post’s ACF field values, keyed by field name, via get_fields()list-field-groups: List registered ACF (Advanced Custom Fields) field groups: key, title, a flattened summary of their location rules, and whether each is activeupdate-fields: Set one or more ACF field values on a post via update_field(). A field value is ordinary postmeta, so this is snapshotted via object_type post and rollback-operation restores the prior values exactly. Disabled by default (site must opt in via the wpmcp_enable_acf_write filter)
Internationalization (Polylang) (4 tools)
get-post-translations: Read a post’s translations (the translated post id and title, keyed by language code) via the active multilingual plugin (Polylang or WPML)link-post-translations: Link a set of posts as translations of one another, given a list of {language, post_id} pairs, via the active multilingual plugin (Polylang or WPML). The relationship spans multiple posts but only the primary (first) post is snapshotted, so rollback restores only the primary post, not the other linked postslist-languages: List the site’s configured languages (code, human-readable name, and which is the default) via the active multilingual plugin (Polylang or WPML)set-post-language: Assign a post to a language (by code) via the active multilingual plugin (Polylang or WPML). For Polylang the language is a term in the ‘language’ taxonomy, so this is snapshotted via object_type post and rollback-operation restores the prior language assignment exactly
Multisite (4 tools)
get-network-info, requires an optional companion plugin or multisite: Report this network’s id, name, domain, total site count, and main site id, via get_network()/get_main_site_id(). Read-onlyget-site-details, requires an optional companion plugin or multisite: Report a single network site’s details (blog_id, url, name, last_updated) by blog_id, via get_site()/get_blog_details(). Returns an error for an unrecognized blog_idis-multisite: Report whether this WordPress install is part of a multisite network. Always registered, even on single-site installs, so a caller can discover network status before using the rest of the multisite tool grouplist-network-sites, requires an optional companion plugin or multisite: List sites on the network (blog_id, url, name, last_updated) via get_sites(), with optional limit (default 50) and offset for pagination. limit is capped at 500
Analytics & Search Console (5 tools)
get-analytics-connection-status: Report whether an analytics provider (Google Site Kit or explicitly configured credentials) is active and appears connected. Always registered so a caller can discover state before using the rest of the analytics tool group. Read-onlyget-analytics-summary: Read-only sessions/users/pageviews summary over a date range (Y-m-d, defaulting to a trailing 28-day window ending yesterday) via the connected analytics provider. Returns an error when no provider is connectedget-search-console-queries: Read-only list of top search queries by clicks over a date range (Y-m-d, defaulting to a trailing 28-day window ending yesterday) via the connected Search Console provider, with optional limit (default 10, capped at 100). Returns an error when no provider is connectedget-search-console-summary: Read-only clicks/impressions/ctr/position summary over a date range (Y-m-d, defaulting to a trailing 28-day window ending yesterday) via the connected Search Console provider. Returns an error when no provider is connectedget-top-pages: Read-only list of top pages by pageviews over a date range (Y-m-d, defaulting to a trailing 28-day window ending yesterday) via the connected analytics provider, with optional limit (default 10, capped at 100). Returns an error when no provider is connected
Cron (4 tools)
list-cron-events: List the scheduled WP-Cron events (hook, next-run timestamp, recurrence/schedule, interval in seconds, callback args) from the cron array, plus the available schedules from wp_get_schedules(). Optional hook filter. Read-onlyrun-event: Fire a scheduled cron hook now via do_action(), for debugging scheduled jobs. Disabled by default until a site opts in with the wpmcp_enable_run_cron_event filter, and only fires a hook actually present in the cron array (never an arbitrary string). Always replays the stored event args, never caller-supplied ones. Not snapshotted: firing a hook is an irreversible side effectschedule-event: Schedule a recurring event (wp_schedule_event, when a recurrence is given) or a single event (wp_schedule_single_event). The recurrence is validated against wp_get_schedules(). Refuses scheduling core-critical hooks (wp_version_check, wp_update_plugins/themes, wp_scheduled_delete, delete_expired_transients, wp_privacy_delete_old_export_files). Snapshotted via object_type option (the cron option); rollback-operation restores the prior cron arrayunschedule-event: Unschedule a single occurrence (wp_unschedule_event, when a timestamp and matching args are given) or every event for a hook (wp_clear_scheduled_hook). Unrestricted, including core hooks, but made safe by undoability: snapshotted via object_type option (the cron option), so rollback-operation restores the prior cron array
Maintenance mode (3 tools)
disable-maintenance: Turn maintenance mode off: sets enabled=false on the wpmcp_maintenance option (message and retry_after are preserved for a later re-enable). Snapshotted via object_type option (the wpmcp_maintenance option); rollback-operation restores the prior stateenable-maintenance: Turn maintenance mode on: sets the wpmcp_maintenance option (enabled=true, message, retry_after seconds). Front-end visitors who are not logged in as a manage_options user then receive a 503 with the configured message until maintenance mode is disabled again. Snapshotted via object_type option (the wpmcp_maintenance option); rollback-operation restores the prior stateget-maintenance-status: Report whether maintenance mode is on and, when it is, the configured message and Retry-After seconds. Read-only
Site context (1 tool)
get-site-context: Report a single orientation payload for an agent connecting to this site: name, URL, tagline, WordPress and PHP versions, active theme, active plugin count and slugs, registered public post types with counts, public taxonomies, user count, locale, timezone, multisite status, and which integrations (Elementor, WooCommerce, ACF, Yoast, RankMath) are active. Excludes the admin email. Read-only
REST passthrough (2 tools)
call-rest: Perform an internal WP REST API request (rest_do_request) against any route registered on this site and return its HTTP status and body. Authorization is inherited from the REST API itself: the target endpoint’s own permission_callback runs against the current user exactly as it would for a real HTTP request, so this tool cannot grant or widen access beyond what that endpoint already allows. GET/HEAD are always permitted (subject to the endpoint’s own permission check). POST/PUT/PATCH/DELETE are refused unless a site has opted in via the wpmcp_enable_rest_writes filter (disabled by default) AND the caller passes confirm:true; a successful write reports recoverable:false because an arbitrary REST write cannot be generically snapshotted or undonelist-rest-routes: List the routes registered on this site’s WP REST API server (core plus every active plugin’s namespace): route path, allowed HTTP methods, and a short summary of each route’s args. Optional namespace and/or search filters narrow the result by substring match on the route path; limit caps the number of rows returned (default 50, max 200). Read-only: never executes a route
Onboarding & connection info (2 tools)
get-connection-info: Return how to connect an MCP client to this site: the MCP server endpoint URL and ready-to-paste connection snippets for Claude Code, Cursor, and Claude Desktop, each using an Application Password placeholder. Never returns a real credential. Read-onlylist-tool-catalog: List every wpmcp ability registered on this site, grouped by domain, with each entry’s tier (free/pro), operation, required capability, and read-only/destructive hints, plus a per-domain summary count. Optional domain and/or tier filters narrow the result. Read-only
Tool dispatch (compact mode) (3 tools)
call-tool: Invoke any wpmcp-registered tool by name with the given arguments object, the dispatch path for tools hidden from tools/list by compact mode. The target tool’s own permission checks (capability, governance, identity scope, license), rate limit, input validation, and snapshot/rollback safety behavior all apply exactly as if it were called directly; this tool can never widen access. Refuses tools not registered by wpmcp and the meta-tools themselvesget-tool-schema: Read one registered wpmcp tool’s full contract by name: the exact input schema it was registered with, its complete description, MCP annotations, and its domain/operation/tier classification. Read-only. Use wpmcp/list-tools to discover nameslist-tools: List every tool this wpmcp install currently registers: name, a short summary, domain, operation, and tier, sorted by name. Optional domain filter narrows the result; full:true adds complete descriptions and MCP annotations. Schemas stay behind get-tool-schema. Read-only. With compact mode active this is the discovery entry point for every tool not directly listed
Governance & scoped identities (6 tools)
create-identity: Create (or overwrite, by name) a scoped identity: a named restriction that, once active (see the wpmcp_current_identity filter), narrows which abilities are usable on top of the caller’s capability and Governance. Accepts name (required), and optional domains/operations/abilities allowlists plus mode (allow, the default, or deny). Optional exposure (full or compact) sets this identity’s tool-surface mode, overriding the site-wide setting; omit to inheritdelete-identity: Delete a scoped identity by name. Returns an error if no identity with that name existsget-governance-settings: Return the stored governance toggle maps (ability, domain, operation): explicit enable/disable decisions layered on top of the wpmcp_ability_enabled/wpmcp_domain_enabled/wpmcp_operation_enabled filters. Read-onlylist-governance-audit-log: List governance-decision audit log entries (ability, active identity or “none”, allowed/denied, timestamp), newest first. Optional limit (default 20). Read-onlylist-identities: List every registered scoped identity. Read-onlyupdate-governance-settings: Batch-update stored governance toggles across the ability, domain, and operation dimensions, e.g. {ability: {“wpmcp/delete-post”: false}, domain: {“database”: false}, operation: {“delete”: false}}. Invalid individual entries are skipped and reported, not thrown for; only entirely empty input throws
PHP snippet validation (1 tool)
validate-php-snippet: Statically validate a PHP code snippet without executing it: report syntax validity (with error message and line if invalid) and safety findings (severity-tagged warnings for dangerous constructs such as eval, exec, shell_exec, backticks, obfuscation decoders, request-driven execution, and outbound HTTP calls). Read-only, never runs the snippet
Guarded WP-CLI (1 tool, 1 Pro)
run-wp-cli, Pro: Run a guarded, allowlisted wp-cli subcommand (e.g. “core version”, “plugin list”, “option get siteurl”) and return its stdout, stderr, and exit code. Disabled by default (opt in via the WPMCP_ALLOW_WP_CLI constant or wpmcp_allow_wp_cli filter); refuses to run on a production environment unless a separate override is also set; only subcommands on the wpmcp_wp_cli_allowlist filter’s allowlist are permitted; arguments containing shell metacharacters are rejected before anything runs
Guarded PHP execution (1 tool, 1 Pro)
run-php-snippet, Pro: Run a guarded, arbitrary PHP snippet and return its return value, echoed output, and any thrown error. THIS IS REMOTE CODE EXECUTION: disabled by default (opt in via the WPMCP_ALLOW_PHP_EXEC constant or wpmcp_allow_php_exec filter); refuses to run on a production environment or any unrecognized environment unless a separate WPMCP_ALLOW_PHP_EXEC_ON_PRODUCTION override is also set; snippets flagged unsafe by the static validator are rejected before execution as a usability speed-bump only, not a security boundary. Its effects are not captured by this plugin’s snapshot/rollback system and cannot be undone.
Integrations (ACF dispatcher) (2 tools)
acf-read: Dispatch a read operation against Advanced Custom Fields (field groups and per-post field values). Pass operation (use the reserved “list-operations” to discover every operation with its input schema) plus args matching that operation’s schema. Read-onlyacf-write: Dispatch a write operation against Advanced Custom Fields (field groups and per-post field values). Pass operation plus args matching that operation’s schema (discoverable via list-operations on the read half). Every operation with a snapshotable target is snapshotted first via Safe_Mutation and restorable with rollback-operation; destructive operations additionally require confirm:true
Free vs Pro
The split today
The plugin registers 188 abilities: 163 free and 25 Pro. Everything is GPL-2.0 licensed either way, the Pro tier gates which abilities register on your site, not the license of the code.
The free tier includes the entire safety net and the broad site-management surface:
- The full safety engine: snapshot, apply, verify, rollback (both operation- and session-level), plus the wp-admin wpmcp history and one-click restore screen.
- All content, Gutenberg block, and surgical block-editing tools, and the
build-pagecomposite. - Media (including stock-image search/import and sanitized SVG upload), settings, users, comments, navigation menus, plugins and themes.
- The complete WooCommerce surface: products, orders, notes, sales reports.
- SEO metadata through Yoast SEO or Rank Math, internal-link analysis, performance and security scanners.
- Governance, scoped identities, the audit log, onboarding/connection tools, ACF, Polylang, multisite reads, analytics/Search Console, cron, maintenance mode, REST passthrough, and compact tool dispatch.
- Operation history: the last 20 operations.
The 25 Pro abilities are:
- Elementor deep editing (7):
get-elementor-data,add-widget,update-widget,generate-widget,update-element,move-element,remove-element, built on a curated 44-widget catalog with typed, validated settings schemas. - Elementor structural suite (8):
add-container,update-container,duplicate-element,find-element,reorder-elements,set-element-label,update-page-settings,batch-update. - Bricks & Divi builders (3):
detect-builder,get-builder-content,update-builder-content. - Content analysis (4):
analyze-accessibility,analyze-seo,check-contrast,extract-content. - Guarded execution (2):
run-wp-cliandrun-php-snippet, both additionally default-off and restricted to development environments; see the safety model. - Stock placement (1):
insert-stock-image(searching and importing stock images stays free).
Plus, on Pro: unlimited operation history instead of the 20-operation cap.
The Gate
Free/Pro gating lives in WPMCP\Pro\Gate: a single boolean, Gate::is_pro(), backed by the Freemius SDK’s can_use_premium_code__premium_only() when Freemius is active, and falling back safely to false when it isn’t (no fatal error if the SDK is absent).
The MCP\Registrar skips registering any ability tagged 'pro' when Gate::is_pro() is false, so Pro tools do not appear in a free-tier site’s tool list. The other concrete difference is history retention:
public static function history_limit(): int
{
return self::is_pro() ? PHP_INT_MAX : 20;
}
Free tier: 20 operations of history, pruned globally (not per-session) after every write. Pro: unlimited. See Safety model for what the free cap means in practice for rollback-session on large agent runs.
Buying Pro
Pro is licensed through Freemius (annual plans from $26.99/year for a single site). The plugin is registered with Freemius and ships privacy-first defaults; checkout links are being wired up, until then, the free plugin is fully functional and the Pro abilities light up when a license activates.
Privacy default: anonymous_mode
Bootstrap::config() sets 'anonymous_mode' => true by default:
// Privacy-first defaults: wpmcp does not force telemetry opt-in.
// anonymous_mode skips the Freemius connect/opt-in gate on activation,
// matching our "no telemetry by default" positioning.
'anonymous_mode' => true,
This means Freemius’s usual connect/opt-in screen is skipped on activation, no telemetry opt-in gate is forced on the site owner. This is a deliberate privacy-first default consistent with the project’s “no telemetry by default” positioning, not an accidental omission.
WooCommerce Tools
wpmcp treats a WooCommerce store as a first-class surface, not an afterthought bolted onto the content tools. An AI agent can read products, orders, and sales data, and make the day-to-day changes a shop owner actually asks for: updating a price, marking an order as shipped, adjusting stock. Every one of those changes goes through the same snapshot/rollback engine documented in The safety model, so a bad price edit or a wrong order-status change is a one-click undo, not a support ticket.
What a shop owner gets
- Read the whole store.
list-productsandget-productfor the catalog,list-product-categoriesfor how it is organized,list-ordersandget-orderfor what customers have bought,get-sales-reportfor a date-range summary of gross sales, order count, items sold, and top products by quantity. - Change products safely.
update-productedits price, stock, description, SKU, and status. A product is a WordPress post under the hood, so the change is snapshotted the same way an ordinary page edit is:rollback-operationputs the exact prior price and stock quantity back, not an approximation. - Change order status safely.
update-order-statusmoves an order through the store’s registered statuses (processing, completed, refunded, and so on), validated against what the store actually supports. It is snapshotted via a dedicatedwc_orderobject type, sorollback-operationrestores the prior status exactly, whether the store uses WooCommerce’s classic post-based orders or the newer High-Performance Order Storage (HPOS) tables. Every order tool is HPOS- and CPT-safe: it does not assume one storage backend, so the same tools work whether or not a store has migrated to HPOS. - Create products, and add order notes.
create-productandadd-order-noteare additive: a new product with no prior state to snapshot, or a note appended to an order’s history. A mistaken product is removed withdelete-product.
The one-click undo, and why it matters here specifically
Most AI-for-WooCommerce integrations either refuse to touch products and orders at all, or let an agent write directly with no way back. That is a real risk on a live store: an agent that fat-fingers a price from $49 to $4.90, or flips a paid order to “refunded” by mistake, has just cost real money in the minutes before a human notices.
wpmcp’s answer is the same safety engine used everywhere else in the plugin:
update-productandupdate-order-statussnapshot the object before the change applies.- The change happens.
- If it turns out to be wrong,
rollback-operation(from the agent, or a one-click Restore button on the wpmcp wp-admin screen) restores the exact prior state: the old price, the old stock count, the old order status.
This is the detail worth underlining for anyone evaluating AI tools against a live store: product and order-status changes are undoable by construction, not by promise. Competing WooCommerce AI plugins that offer direct database or REST access typically have no equivalent undo path once a write lands; recovering means restoring from a nightly backup (losing every order placed since), or fixing it by hand from memory. wpmcp’s snapshot happens at the moment of the change, so the fix is a single click, seconds later, with no data loss for anything else that happened on the store in between.
Destructive and disabled-by-default tools
delete-productis disabled by default (a site must opt in via thewpmcp_enable_delete_productfilter) and always requiresconfirm: true. Trash is the default path;force: truepermanently deletes, and even that is snapshotted, a force-deleted product resurrects at its original ID with its price, stock, and category terms intact.
Capabilities
Read tools (list-products, get-product, list-product-categories, get-sales-report) and product writes require manage_woocommerce, matching WooCommerce’s own admin gate. Order tools (list-orders, get-order, update-order-status, add-order-note) require edit_shop_orders. There is no wpmcp-specific capability layer here: it defers entirely to the store’s existing WooCommerce roles, so a shop owner does not need to reason about a second permissions model.
See the Tools reference for the full argument-level detail on every WooCommerce tool.
Contributing and Tests
Dev setup
composer install # install dev dependencies (PHPUnit, WP test scaffolding, phpcs)
composer test # run the full PHPUnit + WordPress integration suite
composer test:free # run only the free-tier test suite
composer test:pro # run only the Pro/Freemius test suite
composer lint # PSR-12 with WordPress-idiomatic naming, via phpcs
The suite needs a MySQL or MariaDB database for the WordPress integration harness (tests/bootstrap.php sets it up via the standard WordPress PHPUnit scaffolding). CI installs it with bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 6.9 against a MariaDB 10.6 service container; see .github/workflows/ci.yml for the exact steps if you need to reproduce it locally.
CI runs on every push and pull request across a PHP 8.1 / 8.2 matrix: composer install, composer lint, install the WordPress test scaffolding, then composer test.
Test structure
Tests are split into two PHPUnit test suites, matching the free/Pro split described in Free vs Pro:
tests/free/, autoloaded asWPMCP\Tests\Free\, mirrors thesrc/structure:Admin/,Content/,MCP/,Media/,Safety/,Settings/,Tools/, plus root-level smoke and bootstrap tests. This is where the bulk of the coverage lives, including the safety engine’s heaviest tests (Safety/SafeMutationTest.php,Safety/RollbackServiceTest.php,Safety/SnapshotCaptureTest.php,Safety/SnapshotStoreCrudTest.php,Safety/SnapshotStoreInstallTest.php).tests/pro/, autoloaded asWPMCP\Tests\Pro\, covers the Pro ability surface:Elementor/,Builders/,Analysis/,Cli/,Code/,Compose/,Media/,Freemius/, plusGateTest.phpfor the gate itself.
phpunit.xml.dist wires both suites (free -> tests/free, pro -> tests/pro) and defines the WPMCP_TESTING constant used by the bootstrap files to skip the plugin’s normal ABSPATH guard during tests.
The safety invariant
The project’s core rule, stated plainly in the README’s Contributing section: no tool may write to the database except through Safe_Mutation::run(), and every change ships with a test.
This is not enforced by a single “no bypass” test; it’s enforced structurally and by convention across src/Tools/. Every WordPress write call (wp_update_post, wp_delete_post, wp_insert_post, update_option, etc.) in a tool handler either:
- sits inside a
Safe_Mutation::run()closure, or - has an explicit code comment justifying why it is exempt (only two categories qualify: pure-creation tools like
create-postandsideload-image, where there is nothing pre-existing to snapshot; and WordPress’s own trash fordelete-post’s default path anddelete-media’sMEDIA_TRASH-covered path, which is already reversible without a redundant snapshot).
When you add a new tool that mutates existing state, route the mutation through Safe_Mutation::run() with an object_type, object_id, session_id, and tool_name in its context array, the same pattern every existing write tool follows (see src/Tools/Content/Update_Post.php for a representative example, or src/Tools/Settings/Update_Settings.php for the per-key variant used by batch writes).
If a mutation genuinely creates a brand-new object with nothing to overwrite, or is already covered by WordPress’s own reversible trash, document that exemption inline with a comment, the same way the existing exempt tools do, so the next contributor (or reviewer) understands it was a deliberate decision and not an oversight.
How to document a new feature
When a new tool or test lands in this codebase:
- Add or extend a section in
40-tools-reference.mdfor the new tool: its registered ability name (wpmcp/<name>), purpose, key args, whether it’s safe-wrapped (and why, if not), and any capability or opt-in requirements (disabled-by-default filters,confirmflags, allowlists). - If it changes safety behavior, adds a new object type to snapshot/restore, changes pruning/retention logic, changes the meta-purge, or touches the force-delete/resurrection path, also update
30-safety-model.md. This file exists specifically to describe actual mechanics, so keep it in sync with the code rather than the aspirational design spec. - Prefer describing what the code does over what a design doc says it should do. This documentation set intentionally verified every claim against
src/directly; a design spec drifting out of date from the shipped implementation is a normal part of development, not a bug, but the docs here should track the code, not the spec. - If a feature is Pro-gated or merely planned, say so explicitly (“planned”) rather than implying it ships today. See
50-free-vs-pro.mdfor the current free/Pro boundary.