Portfolio / Case study

Model Context Protocol · WordPress plugin

WordPress MCP Server

239 tools, inside WordPress · for any AI client

An MCP server that runs inside a WordPress installation rather than beside it — so an AI client can read and write files, run WP-CLI, query MySQL, manage plugins and orders, and reach every ability other plugins register. And, when it is pointed at a client's live site, change nothing at all until a human approves the diff.

239Tools exposed
4Transports served
24AI clients supported
3Protocol revisions
The problem

Every MCP client disagrees

Connecting an AI assistant to a WordPress site sounds like a solved problem until you try it across more than one client. They disagree about which transport they speak, which config format they want, and whether they can send an HTTP header at all. Then the host fights back: a security plugin disables the REST API, FastCGI strips the Authorization header, permalinks are plain.

And underneath all of that sits the real risk. An MCP server on a WordPress site is a root shell for that site. Pointing one at a client's production install to diagnose something is genuinely useful — and one careless write away from an outage.

Four transports, one server

Streamable HTTP, legacy HTTP+SSE, plain JSON-RPC and a dependency-free stdio bridge — all backed by the same code, auth and audit trail.

Scoped credentials

Per-client tokens with independent scopes, tool allow-lists, IP restrictions and expiry. Tools above a scope are hidden from tools/list, not just refused.

It probes itself

On activation and twice daily it makes real loopback requests over every endpoint and credential channel, then generates client configs against the combination that actually authenticates.

The interesting part

Working on a live site without changing it

Protection mode moves the decision from “did I remember which window I'm in” to something the site enforces.

Every site sits in one of three modes, defaulting from WordPress' own wp_get_environment_type() — so a site that declares itself production is protected before anyone configures anything.

ModeWhat happens to writes
openApplied immediately. The default off production.
protectedCaptured into a changeset for review. The live site is untouched until it is applied. Default on production.
lockedRefused outright. Read and diagnose only.
> edit_file wp-content/plugins/shop/cart.php …

  { "staged": true,
    "changeset": "cs_20260806_143022_a91c",
    "diff": "- $total = $sub;
              + $total = $sub + $tax;",
    "note": "1 change staged. The live site is untouched." }

> diff_changeset        # review everything staged so far
> apply_changeset       # write it — the changeset IS the backup
> rollback_changeset    # put it all back

What makes this trustworthy rather than a wrapper:

  • The live file is never opened for writing. Staged paths are rewritten to a shadow copy inside the changeset, and the real tool runs against that. So the surgical edit tools behave identically staged or not — there is no second implementation of the edit semantics to drift, and no window in which the live file is briefly modified and then reverted.
  • It fails closed. A write that cannot be captured exactly — arbitrary PHP, raw SQL, user creation, plugin installs — is refused on a protected site, not waved through.
  • Drift is caught. Applying refuses if any target changed since it was staged, and lists exactly what moved. Overriding is deliberate and explicit.
  • The model is told up front. Protection state is injected into the server instructions, so a connecting client knows its writes are being staged rather than discovering it from a confusing tool result.

Building this surfaced a genuine security bug in the existing code: the batch tool invoked handlers directly, bypassing the permission check its own policy comment claimed it performed. A read-only credential could reach arbitrary PHP execution through it. Now closed, and covered by a regression test.

Local → live

Two ways to ship a change

Because hosts differ, and half of them block outbound HTTP.

Site-to-site runs the tool on the far end over its own MCP endpoint. That detail matters: the target's protection mode still applies, so pushing to a protected production site stages a changeset there rather than overwriting anything. Somebody still has to approve it.

> compare_sites live --path=wp-content/plugins/shop

  local_only:   includes/class-tax.php
  modified:     includes/class-cart.php
  remote_only:  assets/hotfix.css

> deploy_files live --path=wp-content/plugins/shop
  "2 file(s) were STAGED on 'live', not applied — that site is protected."

> pull_files live --files=["assets/hotfix.css"]   # bring the hotfix home

Bundles are the offline equivalent: a zip with a manifest, the intended after-images and a unified patch, which the target reads into a changeset. Exporting a reviewed changeset rather than raw paths means what ships is exactly what was approved.

Interoperability

Every ability other plugins register

WordPress 6.9 added the Abilities API — a core registry where plugins publish their own capabilities with a JSON Schema and a permission callback. A single commercial plugin suite can register around 900 of them, covering Elementor, Bricks, Divi, ACF, Yoast, WooCommerce and more.

Flattening those into individual MCP tools would put hundreds of entries into tools/list and drown the client's context. Instead the bridge follows a discover / inspect / execute shape, so the entire registry is reachable through four tools — and anything installed later shows up automatically, with no code change here.

Security model

Least privilege, enforced twice

ScopeCan doCannot do
ReadInspect files, posts, options, logs; SELECT queriesAny write
WriteThe above, plus posts, media, terms, comments, file editsPlugins, users, options, execution
AdminThe above, plus plugins, themes, users, options, caches, backupsPHP execution, shell, raw SQL, Git
FullEverything—
  • Tokens are stored as SHA-256 hashes; the raw value is shown once and never again.
  • Every tool is risk-classified, and a new tool is treated as a write until proven otherwise — nothing slips past protection by being unclassified.
  • Remote credentials are encrypted at rest with a key derived from the site's own salts, so a database dump alone is not enough to use them.
  • Staged content lives outside the web root's reach — the directory gets a deny rule and an index stub before any source code is written into it.
  • Every call is recorded with arguments, duration and result.
Engineering

Decisions worth defending

1
Shadow tree over simulation.

Staging could have re-implemented what each edit tool does. Instead the real tool runs against a copied path, so staged and unstaged behaviour cannot diverge — and the shadow persists per changeset, so a second edit builds on the first.

2
One interception point.

Protection hooks into a single line of the dispatch path and returns null when it does not apply, leaving the normal path untouched. The one tool that bypasses dispatch re-applies both gates itself.

3
The changeset is the backup.

Each change carries both its before and after image, so apply and rollback are the same walk in opposite directions. No separate backup step to forget.

4
Bridge, don't port.

Surfacing another vendor's ~900 abilities by reading the core registry — rather than copying them — means zero maintenance when they ship an update.

5
PHP 7.4 floor, no dependencies.

Modern string functions are polyfilled and the stdio bridges are dependency-free, so it installs on the shared hosts real clients actually use.

Under the hood

Stack & surface

Tool groups

FilesystemSurgical editingPHP & WP-CLI DatabasePosts & usersMedia TaxonomiesCommentsMetadata MenusRolesBlocks ElementorWooCommerceCron CachesHTTP & RESTEmail Security auditPerformanceBackups GitAbilities APIChangesets Deployment

Engineering

PHP 7.4 – 8.3Model Context Protocol JSON-RPC 2.0Streamable HTTPSSE stdio bridge (Node / Python)WordPress REST API Abilities APIWPCSAES-256-GCM Zero dependencies

Want a closer look?

The source is a private repository — happy to walk through the architecture, or talk about something similar for your stack.