wpmcp
The AI agent that builds and edits your WordPress site, and physically cannot wreck it.
An MCP server for WordPress with snapshot-before-every-write and one-click rollback baked into the core.
Give an AI agent (Claude, Cursor, or any MCP client) the keys to your WordPress site and one thing becomes terrifying: a bad edit on a live client site that you cannot take back. Every other WordPress AI tool asks you to trust the model. wpmcp does the opposite: it assumes the agent will get something wrong, and makes sure nothing it does is permanent.
Every write is snapshotted before it happens. If a change breaks the page, it rolls back automatically. If you do not like what the agent did, you undo one operation, or the agent's entire session, with one click. That is the whole product: an AI site builder you can actually let near a live site.
Status: early MVP. The safety engine and the first tools are shipped and tested. See the Roadmap for what is next.
Why wpmcp
- Recoverable by design. No mutating tool can touch the database except through a wrapper that snapshots first. This is enforced in code, not by convention.
- Builder-agnostic safety. Snapshots are taken at the WordPress data layer (post content, meta, options), so rollback works for any page builder without parsing its format.
- Runs inside WordPress. A single plugin on the official WordPress Abilities API, not a separate local proxy process. One install, works with any MCP client.
- Free and open. GPL-2.0. The whole safety engine and the Gutenberg tooling are free.
How it works
Every mutating tool routes through one orchestrator, Safe_Mutation::run():
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 any write, wpmcp captures the target's before-image (post content, title, status, and all meta) and stores it, compressed, in a dedicated table keyed by an operation id and an agent session id.
- Apply. The tool performs its edit.
- Verify. A lightweight check runs (for Gutenberg, the block markup must still parse). If it fails, the change is rolled back immediately and an error is raised.
- Rollback, on demand. Undo a single operation, or unwind an agent's entire session back to its pre-session state, including purging any meta the agent added.
You can drive rollback from the AI (the rollback-operation and rollback-session tools) or from the wpmcp screen in wp-admin, where every agent operation is listed with a one-click Restore button.
Audit Log in wp-admin has two tabs. Mutations lists agent writes with Restore. Requests is the MCP request outcome log: one row per tool call, reads included, with the calling client, ok or the error code, how long it took, and, when the call took a snapshot, a link straight to that operation's undo point in History. It is a capped ring buffer (200 rows by default, wpmcp_request_log_cap), and tool arguments are not recorded unless you switch on the wpmcp_request_log_capture_args option or filter, which still redacts secret-looking values and truncates long ones.
Requirements
| Dependency | Version |
|---|
| WordPress | >= 6.9 (bundles the Abilities API) |
| PHP | >= 8.1 |
| Composer | for installing from source |
Installation
wpmcp is not yet on the wp.org plugin directory (planned). For now, install from source:
git clone https://github.com/fahdi/wpmcp.git wp-content/plugins/wpmcp
cd wp-content/plugins/wpmcp
composer install --no-dev
Then activate wpmcp from the Plugins screen in wp-admin.
Connect your AI client
wpmcp exposes its tools over the Model Context Protocol. Authenticate with a WordPress Application Password (Users -> Profile -> Application Passwords).
Example for Claude Code, in 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"
}
}
}
}
Generate the credential with:
echo -n "your-username:xxxx xxxx xxxx xxxx xxxx xxxx" | base64
The same endpoint works with Cursor, Claude Desktop, and any MCP-compatible client.
Protocol revisions and the MCP adapter
Both transports (the HTTP endpoint above and wp mcp-stdio serve) speak MCP 2026-07-28 and 2025-11-25. A 2025-11-25 client runs the usual initialize session; a 2026-07-28 client calls the sessionless server/discover and then tags each request with the revision, with no session at all. server/discover lists both revisions and returns the same handshake instructions initialize does.
Clients that advertise the newer protocol features get them; everyone else keeps the existing contract:
- Elicitation. A destructive tool's
confirm: true gate asks the person instead of refusing when the client supports form elicitation: on 2026-07-28 (both transports) as an input_required result the client answers and retries, and on a 2025-11-25 stdio session as an elicitation/create request mid-call. The answer is bound to that exact call. Without the capability, and on a 2025-11-25 HTTP session (the adapter's HTTP transport has no server-to-client channel), confirm: true works as before, and an agent that passes it is not asked again.
- Tasks. With the
io.modelcontextprotocol/tasks extension on 2026-07-28, trigger-backup and dispatch-cli-job return a task handle, polled with tasks/get and withdrawn with tasks/cancel. Tasks are the existing job records, so they survive the stateless HTTP route and reconnects.
- Output schemas. Every tool declares an
outputSchema: the backup and CLI job tools a detailed one, the rest the JSON-object contract every structuredContent result meets.
The HTTP route is mounted by the official WordPress MCP adapter. wpmcp bundles adapter 0.6.x so it works on its own, and prefers the canonical MCP Adapter plugin when that is active: the bundled copy is only a fallback and never shadows another copy's classes (see src/adapter-guard.php). Adapter 0.7.0 and later answer 2026-07-28 requests natively; with the bundled 0.6.x, wpmcp answers them itself, so clients see the same behavior either way.
Available tools
| Tool | Type | What it does |
|---|
get-page | read | Read a page's title, content, and whether it is an Elementor page |
update-blocks | write (safe) | Replace a page's Gutenberg block markup, snapshotted and verified |
list-operations | safety | List recent agent operations (no snapshot payload leaked) |
rollback-operation | safety | Undo a single operation by id |
rollback-session | safety | Unwind an entire agent session to its pre-session state |
list-post-types | read | List registered post types (posts, pages, custom post types) |
list-taxonomies | read | List registered taxonomies (categories, tags, custom taxonomies) |
create-post | write | Create a post, page, or custom post type, with terms and meta |
get-post | read | Read a post's full content, status, terms, meta, and featured image |
update-post | write (safe) | Partially update a post's fields, terms, meta, or featured image |
delete-post | write (safe on force) | Trash a post by default, or permanently delete with force: true |
list-posts | read | Search/list posts by type, status, search text, author, or parent |
set-post-terms | write (safe) | Assign taxonomy terms to a post: replace, append, or remove |
search-content | read | Search all of the site's text at once, including copy post_content search cannot see: Elementor/Bricks element settings, Gutenberg block attributes, template parts, reusable blocks, and nav menus. Every hit returns an addressable location (block path, element id, menu item id) plus a snippet |
get-page-snapshot | read | One-call normalized page digest: structure summary with counts, content outline in document order, media and link inventory, builder detection, and SEO-lite signals, so understanding a page costs one call instead of a chain of reads. Heavy sections (, ) are opt-in via ; a block says which sections the stored content could not answer for Elementor/Bricks/Divi pages, and the response is bounded by item, string, and byte caps, the last of which is applied after the overlay filter and can drop whole sections (reported in ) |
Every write tool is wrapped in the safety engine, except sideload-image, create-user, install-plugin, install-theme, and insert-row: each only ever creates a brand new object, so there is nothing pre-existing to snapshot or roll back. sideload-image still records the attachment it adds and returns an operation_id, so rollback-operation deletes it and its files again. create-post, duplicate-post, create-custom-widget, and create-custom-block also have no prior state, but they record a creation row under the caller's session_id and return an operation_id, so a session-derived change set lists what they created and rollback-operation / rollback-session undo them without deleting anything: a created post moves to the trash (restore it from there), and a created widget or block spec is deactivated. duplicate-post with publish_stage is the exception: it writes a staged draft copy over its original (same ID, URL and comments), so it snapshots the original first and its operation_id restores it exactly; a staged copy stays a noindex draft until then. The exemption covers the install step only: install-plugin and install-theme called with activate: true do overwrite prior state (active_plugins, or template/stylesheet), so that step snapshots it and returns rollbackable operation id(s) (operation_id from install-plugin, operation_ids from install-theme, one per option), and additionally requires the activate_plugins / switch_themes capability the equivalent wp-admin screen requires. Reads and rollbacks are gated by the edit_posts capability. create-user and update-user are additionally gated by the create_users and edit_users capabilities; the package tools are gated by their matching WordPress capability (activate_plugins, install_plugins, update_plugins, delete_plugins, switch_themes, install_themes, update_themes, delete_themes); all six database tools (reads included) are gated by manage_options, since raw table/row access is equivalent to phpMyAdmin-level access to the site. additionally requires a site to opt in via the filter before it will run at all; , , , and are similarly disabled by default via their own filters and always require , since updating or deleting package files is not rollback-able (no file backup; see issue #24) and each response says so honestly (). snapshots each changed option individually, snapshots the user's profile fields and usermeta, and // snapshot the option(s) they change (the safety engine's snapshot/rollback supports WordPress options and users as well as posts), so any subset of a batch write can be undone via . There is deliberately no delete-user or role-change tool. , , and are disabled by default via the filter; and additionally require a non-empty (and requires ) and refuse the / tables (extend via the filter). Because a generic arbitrary-table rollback is out of scope for the safety engine's snapshot/restore logic, these two capture a before-image to a capped audit log instead and report rather than claiming an undo they cannot perform. All six filesystem tools (reads included) are gated by , same reasoning as the database tools: raw filesystem access is at least as sensitive as raw table access. Every filesystem path is confined to the WordPress install root () via , which canonicalizes through so path traversal (), a symlink pointing outside the root, a null byte, and an absolute path escaping the root are all rejected the same way; and are refused for every write/edit/delete regardless of path (extend via the filter). , , and are disabled by default via the filter, additionally require the capability, and honor ; unlike the database write tools, a filesystem write's before-image is a full byte-for-byte backup copy (not just an audit-log record), so in the response is a real, tested guarantee: the original file can be restored exactly, not merely logged. A write or edit also has to survive a site check before the call returns: the new content must parse ( in parse mode; a failure is refused with the parser's message and line and nothing is written), and after saving, the front end and wp-admin are requested with core's scrape key and nonce, the loopback the theme and plugin file editor uses to detect a fatal error. A fatal, or a site that stops answering that check, restores the previous version (or removes a new file) in the same call and reports the error. Only one PHP edit runs at a time per site, wpmcp's own files are refused, and a file in an inactive plugin or theme gets the syntax check only. When the loopback cannot run at all, the write is refused unless the caller passes , and the response then reports the file as unchecked rather than safe. is read-only and gated by , same reasoning as the database and filesystem tools: it exposes server, database, and page internals. Its optional page fetch is SSRF-safe: the target must resolve to this site's own host, any hostname whose resolved IP falls in a private, loopback, link-local, or reserved range is refused before any HTTP request is attempted, and the actual request goes through as a second layer of defense. and are gated by . is deliberately not routed through the safety engine and does not touch the safety core: clearing a cache has no meaningful before-image to restore (the data is regenerated on demand) and the operation is safe and idempotent, so it carries rather than being treated as a destructive write. Each third-party page-cache plugin is detected by its signature functions/constants and cleared only through its own public API (, , , the action, ), every call guarded by / so an absent plugin never fatals. and are both read-only, gated by , and never touch the safety core: the former only ever emits placeholder credential text (never a real Application Password), and the latter only reads metadata already public on each registered object, never invoking a tool's handler. Generating a one-click install bundle for Claude Desktop is deliberately out of scope for (see issue #18); it documents the manual path instead. is gated by and then re-checks the caller per post: whenever the post is not published, and whenever its post type is not viewable (a published entry of an internal CPT, such as an agent memory guardrail, is not site content and its own tools are gated far higher). A password-protected post's stored body is not read at all for a caller who cannot edit it: not by the extractor, not by the block parser, not by the builder-tree readers, and the overlay seam does not run either, so the digest cannot become a read-around for a draft, a private post, or a protected body. is gated by and then re-checks for every result whose post is not published, so the index can never surface a draft or private post the caller could not have read directly: an index is a performance structure, never an authorization bypass. is gated by because a rebuild walks every post on the site. Neither is routed through the safety engine, and both say why: the table is derived state recomputed from posts, postmeta, and menus, so its only meaningful "undo" is another , and a snapshot would protect nothing.
Compact tool surface
wpmcp registers 160+ tools. Some MCP clients cap tool counts outright, and all of them pay token cost for every tools/list. Compact mode collapses the advertised surface to five tools: list-tools, get-tool-schema, call-tool, plus the connection basics get-connection-info and get-site-context, while every other tool stays reachable through call-tool.
Compact mode is exposure-only and off by default. It never changes which abilities are registered and it is not a permission boundary: a dispatched call runs the target tool's own capability check, governance, identity scope, license check, rate limit, input validation, and snapshot/rollback behavior exactly as a direct call would (a conformance suite sweeps the entire surface in both modes to prove it). call-tool refuses abilities that were not registered by wpmcp.
Choose the mode site-wide with the wpmcp_tool_exposure_mode option (full or compact), per scoped identity via the identity's exposure field (create-identity accepts it; it overrides the site setting for that agent), or in code via the wpmcp_tool_exposure_mode filter. The wpmcp_compact_exposed_abilities filter can add tools to the compact core (the three meta-tools are always included).
Measured tools/list payload (test environment, all optional plugins active): full 72,897 bytes across 154 tools; compact 2,790 bytes across 5 tools, a 96.2% reduction. The numbers are pinned by a checked-in budget test (tests/free/MCP/ToolsListBudgetTest.php) and re-measured on every test run.
Pinning an identity to IP ranges
A scoped identity can be pinned to the addresses your agents run from. Pass allowed_ips to create-identity as a list of IPv4 or IPv6 addresses and CIDR ranges (for example ["203.0.113.0/24", "2001:db8::/32"]). A request acting as that identity from any other address is refused before any tool runs, with the same generic error as any other denial, and the refusal is recorded in the governance audit log as identity/ip-refused with the refused address. Overwriting the identity without allowed_ips clears the list. Identities without a list behave as before, the Connection screen shows each identity's list, and the admin screens are never restricted.
The client address is REMOTE_ADDR. Forwarded headers are ignored unless you name your reverse proxies, so a client cannot claim another address:
add_filter('wpmcp_trusted_proxies', function () {
return ['10.0.0.0/8']; // your load balancer or proxy addresses
});
When REMOTE_ADDR is a listed proxy, X-Forwarded-For is read right to left and the first hop that is not a listed proxy is the client. A malformed hop, or no usable address, is refused. The WP-CLI stdio transport has no network peer and is not affected.
Agent project memory (enforced, not advisory)
A site accumulates rules an agent should know: "the header is a reusable block", "never touch the homepage". wpmcp stores those as wpmcp_memory entries and puts a trust gate in front of them: everything an agent proposes lands pending and is completely inert. It is not injected into any future session, it is not enforced, and memory-recall will not read it back. An administrator publishing the entry in wp-admin is the single act that makes it live, and nothing on the agent-facing path can publish (memory-propose hard-codes the status and takes no parameter to override it).
Published entries reach the agent through the existing MCP initialize instructions channel, so there is no separate discovery seam.
The part that matters: an entry with severity: block is enforced by the server, not suggested to the model. Enforcement lives in Registrar::is_permitted(), the single gate every wpmcp ability passes through before it runs, so a rule denies uniformly across the whole surface, including tools written after the rule was created, with no per-tool opt-in and nothing for a future write path to remember to call. A rule names one or more targets:
| Target | Matches |
|---|
tool:<ability> | that ability by exact name (the wpmcp/ prefix is optional), or by prefix with a single trailing * |
post_id:<id> | a call whose arguments name that post |
post_type:<slug> | a call that declares that post type, or that names the id of a post of that type |
Every memory denial is written to the governance audit log with the blocking entry's id (memory-block:<id>), so "why was this refused" is answerable after the fact. Read-only abilities are never blocked: a guardrail restricts what an agent may change, and denying reads would break recall and diagnostics while protecting nothing. An untargeted block entry is rejected at validation time, because it would deny every write on the site.
Honest limitation, stated rather than hidden: post_id and post_type targets can only match arguments the server can see, so a tool that addresses content some other way (a slug-only API, a bulk operation with no id list) is not caught by those two forms. tool: targets have no such gap and are the airtight form.
Session summaries are a deterministic rollup of the session's snapshot rows, not an LLM summarization pass. wpmcp writes a before-image ahead of every mutation, so the rows are a complete server-side record of what actually changed; the digest is computed from them, which makes it reproducible, offline, free, and impossible for a summary to over- or under-claim.
The three tools are Pro tier and off by default (wpmcp_enable_memory, default false). Enforcement is deliberately not gated on either: it is on by default (wpmcp_memory_enforce) and keeps working after the tools are switched off or a license lapses, because a guardrail that quietly stops guarding is worse than no guardrail. Pending proposals surface as a count bubble on the wpmcp admin menu.
Agent skills
A safety-first tool surface only pays off if the agent uses it correctly, so the plugin ships the guidance itself. list-skills and get-skill serve versioned markdown playbooks (a SKILL.md per skill, with a validated frontmatter header) covering snapshots and rollback, governance and scoped identities, surgical Elementor editing, WooCommerce catalog and order work, and safe database work.
Discovery is deliberately cheap: list-skills returns slug, name, a one-line description, version and tags for the whole library in well under 4 KB, and only get-skill pays for a body.
- Validated, not best-effort. A
SKILL.md missing a name, description, or version, or over the 64 KB cap, is not served at all: it is refused and named, with its error codes, on the wpmcp -> Skills admin screen. Because oversized documents are refused at discovery time, get-skill can promise the body exactly as written, never a truncation.
- Site-aware. A skill may declare
requires (ability names). Skills whose tools are not registered on this install (a vertical build, a governance-disabled domain, an absent plugin) are hidden from the default listing, so an agent is never handed a playbook it cannot execute. include_unavailable: true shows them with the missing ability names attached.
- Extensible. Drop skills in
wp-content/wpmcp-skills/<slug>/SKILL.md, or register any directory through the wpmcp_skill_sources filter. A later source overrides a bundled slug, so a site can replace a shipped playbook with its own house style.
- No path ever comes from input.
get-skill resolves a slug against the index built by scanning disk; a caller-supplied string is never concatenated into a filesystem path, and every discovered file must realpath() inside its declared source root.
- Tiering is per skill, not per surface. Both tools and the starter library are free. A skill whose frontmatter says
tier: pro stays listed but its body is withheld behind Pro\Gate until the site is licensed.
- Removable. The checkbox on the Skills screen (option
wpmcp_skills_enabled, or the filter of the same name) unregisters both abilities entirely, so a site that does not want them pays zero tokens for them in tools/list.
Prompts and resources
The same content is also served through the MCP prompts and resources primitives, so clients that surface those natively (slash-command prompt pickers, attachable context) can use it without a tool call. The server advertises both capabilities on initialize, on the HTTP and the stdio transport alike.
- Prompts.
prompts/list offers every skill whose body this site can serve (available and not withheld), named by slug. prompts/get returns the skill body exactly as written, as a single user message.
- Resources.
resources/list offers wpmcp://site/context (the get-site-context payload) and wpmcp://skills (the list-skills catalog), both application/json. resources/read returns the same payload the tool would.
- Governed like the tools behind them. Each primitive is backed by the live ability that serves the same data (
get-skill, get-site-context, list-skills): the capability check, governance toggles, scoped identities, project-memory rules, audit log, rate limit and request log are that ability's own. When the backing ability is not registered (governance-disabled, skills switched off, exposure switch off) the primitive is absent. Every backing ability is a read, so nothing here writes.
Free vs Pro
The free plugin (this repo) is fully functional: the safety engine, Gutenberg editing, one-click rollback, and snapshot history.
Snapshot_Store itself has no licence check: Snapshot_Store::history_limit() returns the same flat cap (DEFAULT_HISTORY_LIMIT, 20) in every build, and the wpmcp_snapshot_history_limit filter raises or lowers it for free on any site (any whole number of 1 or more; a non-numeric or out-of-range return falls back to 20, since 0 would let the snapshots table grow without bound). A site that upgrades with a history deeper than the cap keeps it: the existing depth is recorded once as a floor and held until the owner sets the filter or acknowledges the admin notice, and pruning never deletes more than PRUNE_BATCH_LIMIT rows per write. The self-hosted Pro build lifts the cap to unlimited through that same filter (WPMCP\Pro\Snapshot_History, registered only while the licence is active), and if a licence that was active ends, the depth the site has then is held as the same floor so a lapse never deletes history. Pro (planned, via Freemius) adds Elementor deep editing, change previews, and priority support. The Pro gate (WPMCP\Pro\Gate) and Freemius bootstrap are wired from day one; the plugin degrades gracefully when the Pro SDK is absent.
Freemius consent is an explicit, default-off opt-in: anonymous_mode is false (see WPMCP\Freemius\Bootstrap::config()), so the SDK ships its stock connect screen with the Skip link intact (enable_anonymous is pinned true for that reason), and nothing is sent until someone affirmatively opts in. Because the config nests Freemius under the plugin's own top-level wpmcp menu, override_exact is pinned true as well: without it the SDK's activation-mode takeover removes the whole wpmcp menu and its submenus on every admin screen until the opt-in is answered. The activation URL is admin.php?page=wpmcp, the History page, so that one screen shows the connect page until the opt-in is answered or skipped; every other wpmcp submenu stays reachable meanwhile.
This applies to the self-hosted zip only. The WordPress.org build carries no licensing SDK at all: scripts/flavors/wporg/strip.php removes src/Freemius and scripts/build-wporg-release.sh composer-removes freemius/wordpress-sdk, with build gates that fail if either survives.
Credentials are already wired: wpmcp.php defines WPMCP_FS_ID and WPMCP_FS_PUBLIC_KEY, and the SDK arrives through composer (freemius/wordpress-sdk, found by Bootstrap::locate_sdk() at vendor/freemius/wordpress-sdk/start.php). The connect-screen icon is pinned to assets/wpmcp-icon.svg, so scripts/build-release.sh stages assets/ alongside src/ and fails the build if any assets/ path referenced from src/ is missing from the zip.
Roadmap
Known limitations
Snapshot history keeps the last 20 operations by default; raise it with the wpmcp_snapshot_history_limit filter (minimum 1). Pro keeps unlimited history while the licence is active. Pruning works on whole sessions: single writes in the default session are pruned one by one to the limit, but a named session (a bulk run such as a background optimize-media run, a product import or a multi-call database cleanup) is kept or dropped whole, never cut. The newest session and the newest session larger than the limit (the last bulk run) are always kept, so the table holds at most the limit plus those two sessions. A session that has been dropped makes rollback-session refuse with the reason rather than undo part of it. Snapshot capture currently covers post content, title, status, meta, and taxonomy terms, but not every post field (e.g. excerpt, parent). Force-deleting media (or deleting without MEDIA_TRASH enabled) restores the media record on rollback but not the physical file bytes, until issue #24 lands; media force-delete is disabled by default. Details and mitigations are in the design spec.
Development
composer install # install dev dependencies
composer test:local # the test gate: lint, drift check, PHPUnit on the default and minimum WordPress and on multisite, coverage floor
composer lint # PSR-12 with WordPress-idiomatic naming
The PHPUnit suite does not run on GitHub Actions. It runs locally through bin/test-local.sh, and the pre-push hook in .githooks/ runs it before every push, on the default WordPress, on the Requires at least minimum, and on the default WordPress installed as a multisite network (WP_MULTISITE=1). Every leg runs even when one fails, and the run ends with a pass/fail summary. The first run of the script turns the hook on for the clone. GitHub keeps only the static gates: lint, the drift check, compliance and Plugin Check.
The script needs MariaDB and Subversion, and nothing else to configure:
brew install [email protected] subversion # keg-only, does not conflict with a mysql install
bin/test-local.sh # default WordPress with the coverage floor, the `Requires at least` WordPress, multisite, live groups
bin/test-local.sh --all # the same on every WordPress in WP_VERSIONS, plus the released MCP Adapter leg
bin/test-local.sh --plan # list the legs a run would execute, run nothing
bin/test-local.sh --multisite # only the suite on the default WordPress as a multisite network (takes -- --filter too)
bin/test-local.sh -- --filter SnapshotTest # a targeted PHPUnit run
ELEMENTOR_VERSION=latest bin/test-local.sh # check for drift against the newest Elementor
WPMCP_TEST_ADAPTER_PLUGIN=/path/to/mcp-adapter/mcp-adapter.php bin/test-local.sh # against a canonical MCP Adapter build, e.g. its trunk after composer install --no-dev
bin/test-local.sh --live-forms # only the Contact Form 7 + Flamingo live group
bin/test-local.sh --live-blocks # only the block suites live group (Kadence Blocks, GenerateBlocks, Spectra, Otter Blocks)
bin/test-local.sh --live-buddypress # only the BuddyPress live group
bin/test-local.sh --live-lms # only the Tutor LMS and LifterLMS live groups
bin/test-local.sh --live-crm # only the FluentCRM and MailPoet live group
bin/test-local.sh --live-amelia # only the Amelia Lite live group
bin/test-local.sh --live-adapter # only both suites on the latest MCP Adapter plugin released on WordPress.org (WPMCP_ADAPTER_VERSION pins one)
It starts a private MariaDB server under ~/.cache/wpmcp-tests, installs each WordPress version there once per version of the install scripts and of the test plugins (each unpinned plugin's latest release is looked up on WordPress.org on every run, falling back to the newest cached install offline, and the summary names the plugin versions each leg ran against), and gives every worktree its own database, so worktrees can run side by side. The default run ends with seven live groups, each on its own install: the live forms group (the Contact Form 7 adapter against the real Contact Form 7 and Flamingo), the live blocks group (blocks inserted through the block-suites tools, rendered with the real Kadence Blocks, GenerateBlocks, Spectra and Otter Blocks), the live BuddyPress group (the BuddyPress ops run through the real BuddyPress, its hooks firing, and rolled back) and the live Tutor LMS and LifterLMS groups (a course tree built through the LMS ops, read back through each plugin's own curriculum API, and rolled back) and the live CRM group (FluentCRM contacts and MailPoet subscribers written, imported and rolled back through each plugin's own API without resubscribing anyone, and campaigns drafted, tested and sent through each plugin's own mailer with every email captured, never delivered) and the live Amelia group (services, employees with their hours, customers, appointments and events written and deleted through Amelia Lite's own controllers, with its slot checks and notifications running and customer mail captured, and rolled back). MySQL does not work: the harness makes every table TEMPORARY and MySQL cannot reopen one inside a query, which WooCommerce does. Skip the gate for one push with WPMCP_SKIP_LOCAL_TESTS=1 git push.
Contributing
Issues and pull requests are welcome. Please keep the safety invariant intact: no tool may write to the database except through Safe_Mutation::run(), and every change ships with a test. See CONTRIBUTING.md if present, or open an issue to discuss larger changes first.
Security
wpmcp edits live sites and executes agent instructions, so security is a first-class concern: admin actions are capability-checked and nonce-protected, all output is escaped, and all database access is parameterized. If you find a vulnerability, please open a private security advisory rather than a public issue.
License
GPL-2.0-or-later. Study it, fork it, ship it.