Back to Directory/Media & Images

WordPress MCP

Talk to your WordPress site: content, SEO, WooCommerce, page builders, media and speed, with undo.

Media & ImagesPHPv2.0.0
WordPress MCP: one endpoint for every MCP-capable AI

WordPress MCP

Turn any WordPress site into a remote MCP server that any AI assistant can read, see, and work on directly, with an undo button. Engine-agnostic SEO (Yoast / Rank Math) · AEO/GEO (JSON-LD, llms.txt, robots, redirects, IndexNow) · complete WooCommerce store operations · page-builder aware editing · site speed auditing and optimisation · live Search Console / GA4 / PageSpeed, all over one authenticated endpoint.

Works with any MCP client Version License

WordPress PHP MCP Tools Undo Agency safe

Works with every AI client that speaks MCP


Claude

ChatGPT

Codex

Gemini CLI

Cursor

VS Code

Windsurf

Zed

Cline

Any MCP client

🔌 One endpoint, every AI client

WordPress MCP speaks the open Model Context Protocol over standard Streamable HTTP. It isn't tied to any one AI vendor. Install it on a client site and connect whichever assistant you or your team use: Claude, ChatGPT, Codex, Gemini, Cursor, Copilot, or all of them at once. The settings screen generates a ready-to-paste config for each one. The agent can then see the real site and implement changes itself. No copy-paste, no second OAuth, no guessing which SEO plugin the client runs.


Table of contents


What it does

One plugin, one endpoint, 165 typed tools, plus ready-made prompts and browsable resources. Any MCP client connects once per site and can then do the whole job rather than describe it:

📊 Read the real siteWhat is published, which SEO engine runs, what schema exists, what Search Console actually reports, what is slow, what is out of stock.
👁️ Look at the imagesget_image_bytes returns a picture, not a filename, so alt text and product copy come from what is in the photograph.
✍️ Write to the live siteMeta through whatever SEO plugin the client runs, JSON-LD, posts, products, prices, stock, menus, widgets, theme files.
🧩 Edit builder pages safelyElementor, Gutenberg, Divi and WPBakery pages are edited in their own data structure, not by flattening post_content.
🛒 Run the storeCatalogue, variations, images, bulk repricing, scheduled sales, inventory and coupons. Orders and customer data sit behind their own switch.
🚀 Make the site fasterAudit, apply reversible front-end fixes, clean the database, and prove the change with real Core Web Vitals.
↩️ Take it all backWrites are journalled. Every change returns an operation_id; one call reverses it.

Quick start

  1. Upload the ZIP from Releases via Plugins → Add New → Upload Plugin, or copy the folder to wp-content/plugins/wordpress-mcp/.
  2. Activate WordPress MCP.
  3. Open WordPress MCP in the admin menu. Under Connect an AI client, pick your client and copy the snippet. The site's real endpoint and key are already filled in. Claude and ChatGPT need only the URL: you sign in with WordPress when they ask.
  4. Click Test connection to confirm AI clients can get through. It catches stripped headers, firewalls and caches.
  5. Toggle the capability groups you need. The defaults are safe. To give someone else access, create a connection key with a preset instead of sharing yours.

Then ask for something real:

"Audit this site's SEO, show me the ten worst pages, and fix the missing meta descriptions."

Requires WordPress 5.6+ and PHP 7.4+. WooCommerce and Google Site Kit are optional. Their tools appear only when those plugins are active.


Connect your AI client

ClientConnects withPromptsResourcesNotes
Claude Codeclaude mcp add + Bearer header✅ /mcp__…✅ @Best for dev work; files + builders
Claude web & desktopCustom connector: sign in with WordPress, or URL with ?key=✅✅OAuth sign-in built in
ChatGPTDeveloper mode: OAuth sign-in, or URL with ?key=NoNoWrite tools ask for confirmation; search/fetch built in
OpenAI Codexcodex mcp add / config.tomlNoNoCLI and IDE extension share the config
Gemini CLIgemini mcp add / httpUrl✅ slash commandsNoUse httpUrl, not url
Cursormcp.json✅✅
VS Code / Copilot.vscode/mcp.json✅✅Key held in VS Code's secret storage
Windsurfmcp_config.jsonNoNo100-tool cap; use ?groups=
Zed · Cline · Continue · LM StudioConfig filevariesvaries
Anything elseStreamable HTTP, or mcp-remote for stdiovariesvariesAny spec-compliant client

Prompt and resource support depends on the client and changes often. Tools work in every client above.

Every client connects to the same endpoint:

https://client.example.com/wp-json/wp-mcp/v1/mcp

A client can present the key in any of these ways:

HowWhen to use it
Authorization: Bearer <key> headerAny client with a header field. This is the preferred option because it keeps the key out of URLs and logs.
X-API-Key: <key> headerClients whose UI has an "API key header" box.
?key=<key> on the URLClients that only accept a URL: Claude web/desktop connectors, ChatGPT developer mode, Continue.
Sign in with WordPress (OAuth)Clients that support MCP sign-in (Claude, ChatGPT and others). Paste the plain URL; no key needed. See below.

The snippets below use my-site and YOUR_KEY. The settings screen generates them with your real values.

  Claude Code
claude mcp add --transport http my-site https://client.example.com/wp-json/wp-mcp/v1/mcp \
  --header "Authorization: Bearer YOUR_KEY"
  Claude (claude.ai web & Claude Desktop)

Go to Settings → Connectors → Add custom connector, paste the plain endpoint URL and click Connect. Claude sends you to your site to sign in and approve:

https://client.example.com/wp-json/wp-mcp/v1/mcp

To skip sign-in, paste the URL with a key built in instead: …/wp-json/wp-mcp/v1/mcp?key=YOUR_KEY.

  ChatGPT (developer mode)
  1. Go to Settings → Apps & Connectors → Advanced settings and turn on Developer mode.
  2. Click Create, paste the plain endpoint URL, and choose OAuth. ChatGPT sends you to your site to sign in and approve.
https://client.example.com/wp-json/wp-mcp/v1/mcp

Alternatively, choose No authentication and paste the URL with a key built in: …/wp-json/wp-mcp/v1/mcp?key=YOUR_KEY.

ChatGPT asks for confirmation before any tool that writes. Read-only tools carry readOnlyHint, so ChatGPT runs them without asking. The search and fetch tools follow the shape ChatGPT's deep research expects.

  OpenAI Codex (CLI & IDE extension)
codex mcp add my-site --url https://client.example.com/wp-json/wp-mcp/v1/mcp \
  --bearer-token-env-var WPMCP_MY_SITE_KEY
export WPMCP_MY_SITE_KEY="YOUR_KEY"        # PowerShell: $env:WPMCP_MY_SITE_KEY="YOUR_KEY"

You can also write it straight into ~/.codex/config.toml, which the CLI and the IDE extension share:

[mcp_servers.my-site]
url = "https://client.example.com/wp-json/wp-mcp/v1/mcp"
http_headers = { "Authorization" = "Bearer YOUR_KEY" }
tool_timeout_sec = 120
  Gemini CLI
gemini mcp add --transport http --header "Authorization: Bearer YOUR_KEY" \
  my-site https://client.example.com/wp-json/wp-mcp/v1/mcp

Or put it in ~/.gemini/settings.json. Use httpUrl, because url selects the old SSE transport:

{ "mcpServers": { "my-site": {
    "httpUrl": "https://client.example.com/wp-json/wp-mcp/v1/mcp",
    "headers": { "Authorization": "Bearer YOUR_KEY" },
    "timeout": 120000
} } }

This site's prompts show up in Gemini CLI as slash commands, for example /seo_audit.

  Cursor

Add this to ~/.cursor/mcp.json for all projects, or .cursor/mcp.json for one:

{ "mcpServers": { "my-site": {
    "url": "https://client.example.com/wp-json/wp-mcp/v1/mcp",
    "headers": { "Authorization": "Bearer YOUR_KEY" }
} } }
  VS Code (GitHub Copilot agent mode)

Add this to .vscode/mcp.json. VS Code prompts for the key once and stores it securely:

{
  "inputs": [{ "type": "promptString", "id": "wpmcp-key", "description": "WordPress MCP key", "password": true }],
  "servers": { "my-site": {
    "type": "http",
    "url": "https://client.example.com/wp-json/wp-mcp/v1/mcp",
    "headers": { "Authorization": "Bearer ${input:wpmcp-key}" }
  } }
}
  Windsurf

Windsurf loads at most 100 tools across all servers, so narrow the catalogue with ?groups= (see below):

{ "mcpServers": { "my-site": {
    "serverUrl": "https://client.example.com/wp-json/wp-mcp/v1/mcp?groups=content,woocommerce",
    "headers": { "Authorization": "Bearer YOUR_KEY" }
} } }
  Zed · Cline / Roo Code · Continue · LM Studio
// Zed: settings.json
{ "context_servers": { "my-site": { "url": "https://…/wp-json/wp-mcp/v1/mcp", "headers": { "Authorization": "Bearer YOUR_KEY" } } } }

// Cline / Roo Code: cline_mcp_settings.json
{ "mcpServers": { "my-site": { "type": "streamableHttp", "url": "https://…/wp-json/wp-mcp/v1/mcp", "headers": { "Authorization": "Bearer YOUR_KEY" } } } }

// LM Studio: mcp.json
{ "mcpServers": { "my-site": { "url": "https://…/wp-json/wp-mcp/v1/mcp", "headers": { "Authorization": "Bearer YOUR_KEY" } } } }
# Continue: .continue/mcpServers/my-site.yaml (agent mode)
mcpServers:
  - name: my-site
    type: streamable-http
    url: https://…/wp-json/wp-mcp/v1/mcp?key=${{ secrets.WPMCP_KEY }}
  Anything else (including stdio-only clients)

Any client that supports remote Streamable HTTP MCP servers works. Point it at the endpoint and send the Bearer header. If a client can only launch local (stdio) servers, bridge it with mcp-remote:

{ "mcpServers": { "my-site": {
    "command": "npx",
    "args": ["-y", "mcp-remote", "https://…/wp-json/wp-mcp/v1/mcp",
             "--header", "Authorization:${WPMCP_AUTH}", "--transport", "http-only"],
    "env": { "WPMCP_AUTH": "Bearer YOUR_KEY" }
} } }
Fewer tools per connection: ?groups=

Every model picks tools more reliably from a shorter list, and some clients cap how many tools they load. To expose only some capability groups on a connection, add ?groups= to the endpoint URL or send an X-WPMCP-Groups header:

https://client.example.com/wp-json/wp-mcp/v1/mcp?groups=content,woocommerce

Group keys are content, woocommerce, wc_orders, performance, builders, appearance, sitekit, diagnostics, site_mgmt, filesystem and database. Diagnostics always stays in scope so the agent can orient itself.

?groups= only narrows what's exposed. It can never switch on a group that's disabled in settings. One practical setup is two connections to the same site: a content writer with ?groups=content and a developer with everything.

HTTPS. When the key travels in the URL, serve the site over HTTPS so the key is encrypted in transit. Regenerate the key if a URL is ever shared. Header auth keeps the key out of URLs entirely.


Prompts, resources, search and fetch

MCP has more than tools, and clients use the other parts differently. This server implements the ones that help:

Prompts: ready-made workflows. They appear as commands in the client, for example /mcp__my-site__seo_audit in Claude Code, /seo_audit in Gemini CLI, and the / menu in VS Code and Cursor. A marketer can run a whole engagement step without knowing the tool catalogue. Each prompt is offered only when the tools it needs are enabled on the site.

PromptWhat it does
site_briefingOrients on the stack, health, SEO engine and capabilities, then lists the top five opportunities. Changes nothing.
seo_auditAudits one page, a post type or the whole site; proposes exact titles and descriptions; applies them only after approval
write_seo_articleResearches existing coverage, outlines, writes, and saves a draft with SEO fields, internal links and schema
ai_search_readinessGEO/AEO scorecard covering llms.txt, AI-crawler rules, schema coverage and sitemap health
speed_checkupRuns the audit, image report and PageSpeed, then previews an optimisation plan
fix_broken_linksRuns a full broken-link sweep and proposes replacements or 301s
product_seo_sweepWooCommerce: audits, inspects images, and previews product SEO and schema repairs
search_performance_reportSite Kit: top pages and queries, what moved, and quick-win keywords

Resources: content you can attach. In Claude Desktop, VS Code, Cursor, Zed and similar clients, you can @-mention or attach site content directly. wordpress://site/overview gives the stack and capabilities. wordpress://content/{id} gives any post, page or product as clean text. The most recently updated items are listed for browsing.

search and fetch: universal retrieval. search returns {results: [{id, title, url, text}]} and fetch returns {id, title, text, url, metadata} for an ID or a URL on the site. That's the shape ChatGPT's deep research and company-knowledge connectors look for. It also gives any agent a cheap way to find something and then read it before reaching for the heavier list_content / get_content.


Sign in with WordPress (OAuth)

Clients that support MCP sign-in, such as Claude and ChatGPT, don't need a key at all. Add the plain endpoint URL, and the connection works like "Log in with Google":

  1. The client finds this site's sign-in details on its own (MCP authorization, RFC 9728 and RFC 8414).
  2. It opens a consent page on your site. You log in to WordPress as usual. Only administrators can approve.
  3. You choose what the connection may do: a preset, read-only, or "ask me to approve every change". Then you click Approve.
  4. The client receives a one-hour access token and a refresh token that rotates on every use.

Each approved sign-in appears under Connection keys with the client's name, for example "ChatGPT (signed in by priya)". The audit log names it on every call, and Revoke cuts it off immediately.

SecurityPKCE (S256) is required. Codes work once and expire after 10 minutes. Redirect addresses must match the client's registration exactly, using https, a loopback address, or an app scheme. The consent page can't be framed. Refresh tokens rotate, so a stolen one stops working after the real client refreshes.
Client registrationDynamic client registration (RFC 7591) and client ID metadata documents are both supported.
RequirementsPretty permalinks, so the endpoint has no query string. Works when WordPress is installed in a subfolder.
Turning it offCapabilities → Sign in with WordPress. Keys keep working either way.

Teams, clients and approvals

The plugin was built for two situations: one person running their own site, and an agency running many client sites. Both come down to the same question: who can do what, and who checks it?

Connection keys

The owner key (shown on the settings screen) has full access. For anyone or anything else, create a connection key under Connection keys → New key:

SettingWhat it does
LabelWho or what uses it, e.g. "Priya, ChatGPT" or "Acme Ltd staff". Every call in the audit log names the key.
PresetA ready-made bundle of capability groups (table below), or pick groups yourself.
Read-onlyOnly tools that read. Nothing on the site can change.
Needs approvalEvery change waits for an administrator (see below).
ExpiresAfter a number of days, or never.

The key itself is shown once, then stored only as a hash. Revoking one key never affects the others.

Presets
PresetGroupsTypical use
fullEverything enabled in settingsYou, on your own site
writerContent & SEOA copywriter or a client's staff
seoContent, Site Kit, page builders, performanceAn SEO specialist
storeContent, WooCommerce catalogue, orders, Site KitA store manager
developerEverything, including files, database and site management (when enabled)A developer, for the length of a job
readonlyContent, WooCommerce, performance, builders, appearance, Site Kit; read tools only. Files, raw database and site management are left out because reading them can expose credentials.Audits, reporting, a new AI tool you are trying out

A preset also works on a single connection without creating a key. Add ?preset=writer to the endpoint URL, or pick it from Connect an AI client → Tool preset. Presets, key groups and ?groups= only ever narrow each other. They're enforced on every call, including the steps inside a batch.

Approvals

Give a client's team a key with Needs approval turned on. Their AI can still read everything and preview changes (dry_run=true). Anything that would change the site is stored as a change request, and the agent is told it's waiting:

update_content id=812 title="Summer Sale: 20% off all hoodies"
  → { "queued_for_approval": true, "request_id": "260924123533fb4f9c", … }

You review it under WordPress MCP → Approvals, which shows a side-by-side diff for content and SEO edits. Approve it or reject it with a note. Approving runs the change exactly as requested, and it's journalled, so undo_operation still reverses it. The agent checks the outcome with list_change_requests. From a terminal, use wp mcp approvals list and wp mcp approvals approve <id>.

A typical agency setup
Client site A
  ├─ Owner key ............. agency lead, full access (Claude Code)
  ├─ "Agency SEO team" ..... preset seo, expires in 90 days
  ├─ "Acme staff" .......... preset writer, needs approval
  └─ "ChatGPT (signed in by acme-admin)" ... read-only, via OAuth

Health checks and the connection test

Most failed connections aren't bugs. The usual causes are a host that strips the Authorization header, a firewall answering with an HTML block page, a cache replaying an old response, or a site without HTTPS. Three tools find these problems and say how to fix each one:

  • Test connection, on the settings screen, calls the endpoint from your browser the way an AI client would. It tries the Bearer header, X-API-Key, the key in the URL, tools/list and GET, then runs the server-side checks.
  • Tools → Site Health gains two WordPress MCP checks: configuration and endpoint reachability.
  • wp mcp doctor runs the same checks from a terminal.

They detect HTTPS problems, plain permalinks, an Authorization header stripped by the host (with the exact .htaccess line to fix it), firewall or WAF block pages, CDN or page caches answering MCP requests, slow responses, high-risk groups left on, unused or expired keys, pending approvals, a short PHP time limit, and recent PHP crashes inside tools.


WP-CLI

Everything on the settings screen is also available as wp mcp commands. With WP-CLI aliases, an agency can run them across every client site from one terminal.

wp mcp status                                    # endpoint, version, groups, keys, pending approvals
wp mcp key create "Acme staff" --preset=writer --approval --expires=90
wp mcp key list
wp mcp key revoke ab12cd34
wp mcp groups disable filesystem database site_mgmt
wp mcp approvals list
wp mcp approvals approve 260924123533fb4f9c --note="Looks good"
wp mcp doctor                                    # connection health checks
wp mcp tools --group=woocommerce
wp mcp connect codex                             # print the Codex config for this site

# Onboard every client site in one go
for site in @client-a @client-b @client-c; do
  wp $site plugin install wordpress-mcp --activate
  wp $site mcp key create "Agency SEO team" --preset=seo --expires=90
done

Structured output and live progress

Structured output. Tools with a stable result shape declare an outputSchema and return their result as typed structuredContent too. Clients can then render tables and pass the data straight to code instead of parsing JSON out of text. The tools are search, fetch, site_info, seo_status, get_seo, list_content, list_operations, list_change_requests and mcp_status.

Live progress. Long sweeps such as seo_audit, find_broken_links, product_seo_fix and regenerate_thumbnails send progress notifications while they work, as long as the client asks for them. The client asks by sending a progressToken and accepting text/event-stream. Those calls are answered as a short event stream, and every other call stays plain JSON. Clients that don't ask can still poll get_progress from a second connection.


MCP Registry

The plugin is described in server.json for the official MCP Registry. Registry-aware clients and directories can list it, and you set it up by entering your site's domain and, optionally, a key. A GitHub Actions workflow (.github/workflows/publish-mcp-registry.yml) publishes it when run from the Actions tab, and it checks first that the listed version matches the plugin.


Who it's for

You are…You get…
SEO agency / freelancer running many WordPress clientsOne plugin per client → each site wired into whichever AI your team uses. Audit, plan, and ship SEO/AEO/GEO at agency scale, with an undo journal behind every write.
Digital marketer who lives in ChatGPT, Claude or GeminiAsk in plain English; it reads Search Console, finds quick wins, writes the meta, publishes the post.
Developer / power userA standards-compliant MCP server for Codex, Claude Code, Gemini CLI, Cursor or Copilot: 165 typed tools, filesystem and raw-SQL access (gated), batching, and a two-step way to add your own tool.
WooCommerce store ownerThe agent handles product copy, images, pricing, stock, categories, and Merchant-grade product schema.

Powerful by default for content and SEO. Safe by configuration for everything dangerous.


Why it exists

You manage SEO for many clients on WordPress. The friction: an AI assistant can't see a client's site (what's published, which schema exists, what the product photo actually shows, what Search Console reports), and it can't act on it without you shuttling data between tools.

WordPress MCP closes that loop. It turns each client site into a remote MCP server. You connect it to your AI client once per site with a single command or pasted config; from then on the agent can list content, audit SEO, look at images, write meta to whichever SEO plugin the client runs, generate schema, publish optimised posts and products, control llms.txt / robots / redirects / sitemaps, and pull live Google data, all over one authenticated endpoint.

Design principle: the core SEO and content work is always available and safe to delegate; the dangerous power (filesystem, raw SQL, user and plugin management) exists but ships off, behind explicit capability switches; and anything that writes can be previewed first and reversed afterwards.


What makes it different

Plenty of things can talk to WordPress. What matters is what happens when an agent is actually trusted with a client's live site.

0. It works with the AI you already use

There's no vendor lock-in and no per-assistant plugin. One standards-compliant MCP endpoint serves Claude, ChatGPT, Codex, Gemini, Cursor, Copilot and anything else that speaks the protocol. Different people on the team can use different assistants against the same site at the same time. → Connect your AI client

1. It writes to the SEO plugin the client already has

You never tell the agent whether it's Yoast or Rank Math. One normalised field set maps to the right meta keys, including the social-image pair that most integrations get half-right. → Engine-agnostic SEO layer

2. It edits builder pages without destroying them

Overwriting post_content on an Elementor or Divi page does nothing, or wrecks the layout. These tools read the page as an addressable tree and rewrite one element inside the builder's own data. → Page-builder editing

3. Every write can be taken back

Bulk repricing, a site-wide search and replace, and an SEO sweep are all one-way in most tooling. Here each instrumented write records the value it overwrites, and undo_operation puts it back. → Safety: dry run, undo, audit

4. The agent can see the images

Alt text written from a filename is a guess. get_image_bytes hands the model the actual picture, so the alt text, the caption and the product description describe what is in it. You can also confirm the right photo is on the right product. → Media and images

5. Failures are recoverable, not just reported

Every error carries a stable code, a human message, an actionable hint, and structured details. The agent fixes its own call instead of guessing. → Error handling

6. Long jobs finish

A three-thousand-product audit doesn't die at PHP's execution limit. Sweeps stop early, return a resume offset, and report progress to a second connection while they run. → Safety: dry run, undo, audit

7. Two hundred edits, one request

batch runs up to 50 tool calls in a single round trip, each with its own result or error, reversible as one operation.

8. Danger is opt-in, per site, per engagement

Filesystem, raw SQL, site management and customer data each sit behind their own switch, enforced server-side. A content engagement cannot read user emails. → Capability model


Architecture

A single bootstrap file wires focused, single-purpose classes. Nothing is global except the constants and one accessor.

wordpress-mcp.php                 Bootstrap: constants, requires, register wpmcp(), activation hook
│
└── includes/
    ├── class-wpmcp-plugin.php      Singleton orchestrator: builds collaborators, registers hooks
    ├── class-wpmcp-settings.php    Single option row: API key + capability-group flags
    ├── class-wpmcp-rest.php        Streamable HTTP transport: dual-era JSON-RPC routing, auth, throttle
    ├── class-wpmcp-tools.php       Registry: catalogue, capability gate, validation, dispatch, wire shaping
    ├── class-wpmcp-prompts.php     MCP prompts: ready-made workflows surfaced as client commands
    ├── class-wpmcp-resources.php   MCP resources: site overview and content by wordpress:// URI
    ├── class-wpmcp-clients.php     Ready-to-paste connection recipes for each MCP client
    ├── class-wpmcp-keys.php        Connection keys, presets, OAuth tokens (hashed)
    ├── class-wpmcp-oauth.php       OAuth 2.1 sign-in: metadata, client registration, consent, tokens
    ├── class-wpmcp-approvals.php   Change requests held for an administrator's approval
    ├── class-wpmcp-health.php      Site Health checks, connection test, wp mcp doctor
    ├── class-wpmcp-cli.php         wp mcp … commands
    ├── class-wpmcp-error.php       Typed tool exception, error codes, rolling log, fatal guard
    ├── class-wpmcp-validator.php   Argument validation and coercion against each inputSchema
    ├── class-wpmcp-journal.php     Undo journal: records what each write overwrites
    ├── class-wpmcp-audit.php       Rolling log of every tool call, arguments redacted
    ├── class-wpmcp-progress.php    Progress heartbeat + the execution-time budget
    ├── class-wpmcp-util.php        Multibyte counting, value coercion, PHP syntax checking
    ├── class-wpmcp-media.php       Shared image ingest: sources, dedup, SEO filenames, processing
    ├── class-wpmcp-seo.php         Engine-agnostic Yoast / Rank Math normalised read+write
    ├── class-wpmcp-schema.php      Product / ProductGroup structured data builder
    ├── class-wpmcp-sitekit.php     Read-only Google Site Kit data bridge (runs as connected admin)
    ├── class-wpmcp-performance.php Front-end speed flags + measurement helpers
    ├── class-wpmcp-frontend.php    Public output: JSON-LD, /llms.txt, robots.txt, redirects, IndexNow key
    ├── class-wpmcp-admin.php       Material-3 settings screen: key, toggles, status, error log
    │
    └── tools/                      One trait per capability group: definitions + handlers
        ├── trait-wpmcp-content.php      38 tools  content, SEO, terms, meta, revisions, comments
        ├── trait-wpmcp-search.php        2 tools  search + fetch (universal retrieval shape)
        ├── trait-wpmcp-woocommerce.php  29 tools  products, variations, images, inventory, coupons
        ├── trait-wpmcp-sitemgmt.php     16 tools  plugins, themes, users, options, cron, permalinks
        ├── trait-wpmcp-filesystem.php   13 tools  read/write/search/backup/restore, child themes
        ├── trait-wpmcp-integrations.php 13 tools  Site Kit data + diagnostics + self-check
        ├── trait-wpmcp-appearance.php    9 tools  menus, widgets, customizer, site identity
        ├── trait-wpmcp-orders.php        8 tools  orders, refunds, customers (own group, off by default)
        ├── trait-wpmcp-performance.php   8 tools  audit, optimise, cleanup, cache, images
        ├── trait-wpmcp-builders.php      8 tools  Elementor/Gutenberg/Divi/WPBakery layout editing
        ├── trait-wpmcp-media.php         7 tools  view, optimise, regenerate, dedupe, orphans, alt text
        ├── trait-wpmcp-ops.php           6 tools  batch, undo, restore points, progress, audit log
        ├── trait-wpmcp-seotech.php       4 tools  broken links, sitemaps, IndexNow
        └── trait-wpmcp-database.php      3 tools  guarded raw SQL + schema inspection

A tool is two things in one place: a definition appended to a defs_*() method, and a tool_<name>( $args ) handler beside it. registry() maps one to the other by convention, so nothing central changes when you add one.

Responsibility map

LayerClassOwns
TransportWPMCP_RESTHTTP route, auth, brute-force throttle, protocol-version negotiation, JSON-RPC envelope, MCP routing, content blocks
DispatchWPMCP_ToolsTool catalogue, capability gate, per-connection scope, validation, dispatch() → handler, cross-client schema shaping and annotations
Beyond toolsWPMCP_Prompts, WPMCP_ResourcesWorkflow prompts; attachable site content
OnboardingWPMCP_ClientsConnection snippets for Claude, ChatGPT, Codex, Gemini, Cursor, VS Code, Windsurf, Zed, Cline, mcp-remote
FailureWPMCP_Errors, WPMCP_ValidatorTyped errors with codes and hints; argument checking; rolling log
ReversibilityWPMCP_JournalRecords what each write overwrote; replays it in reverse on undo
AccountabilityWPMCP_AuditEvery call: tool, redacted arguments, status, duration, IP, undo handle
PacingWPMCP_ProgressHeartbeat for long runs; the time budget that stops them cleanly
DomainWPMCP_SEO, WPMCP_Schema, WPMCP_Media, WPMCP_SiteKit, WPMCP_PerformanceSEO field mapping; product structured data; image ingest and processing; Google data; speed flags
Persistence/configWPMCP_SettingsAPI key, capability flags (one wp_options row)
Public surfaceWPMCP_FrontendJSON-LD in <head>, /llms.txt, robots.txt rules, redirects, IndexNow key file
Control planeWPMCP_AdminSettings UI + form handler
WiringWPMCP_PluginConstruct + register everything on the right hooks

Request lifecycle (the wire)

Every interaction is one POST to a single endpoint speaking JSON-RPC 2.0 over MCP's Streamable HTTP transport. Responses are plain application/json, which the transport allows in every protocol revision and every client supports.

MCP client                      WordPress MCP                         WordPress core
─────────────────────────────────────────────────────────────────────────────────────
POST /wp-json/wp-mcp/v1/mcp
Authorization: Bearer <key>  ─► WPMCP_REST::check_auth()
{ "jsonrpc":"2.0",              │   brute-force lockout?  ── 429 if IP over limit
  "method":"tools/call",        │   hash_equals( key ) over header / X-API-Key / ?key  ── 401 on mismatch
  "params":{                    ▼
    "name":"set_seo",        WPMCP_REST::handle()  ── routes by JSON-RPC method
    "arguments":{...} } }        │
                                 ├─ initialize          → negotiated version + capabilities + instructions
                                 ├─ server/discover     → same, for stateless (2026-07-28) clients
                                 ├─ tools/list          → WPMCP_Tools::exposed_definitions()
                                 │                          (enabled groups ∩ ?groups scope, deps present,
                                 │                           titles + annotations, schema shaped for every model)
                                 ├─ prompts/list|get    → WPMCP_Prompts
                                 ├─ resources/list|read → WPMCP_Resources
                                 └─ tools/call          → WPMCP_Tools::dispatch(name,args)
                                                            │
                                                            ├─ registry lookup (unknown → did-you-mean)
                                                            ├─ WPMCP_Settings::can(group)?  ── disabled → error
                                                            ├─ WPMCP_Validator::check(schema,args)
                                                            ├─ WPMCP_Progress::boot()    time budget starts
                                                            ├─ WPMCP_Journal::open()     writes only
                                                            ├─ tool_<name>($args) ──────────► WP_Query / wp_insert_post
                                                            │                                 update_post_meta / $wpdb …
                                                            ├─ WPMCP_Journal::close()    → operation_id
                                                            └─ WPMCP_Audit::record()     → audit log
                                 ◄─ { result:{ content:[{type:"text",text:"<json>"}] } }
                                    (images add {type:"image",data,mimeType} blocks)
                                    (errors → { result:{ isError:true, content:[…] } })

MCP methods implemented: initialize, server/discover, ping, logging/setLevel, tools/list, tools/call, prompts/list, prompts/get, resources/list, resources/templates/list, resources/read. Notifications are accepted with 202. See Protocol compliance.

Two gates on every tools/call:

  1. Transport auth: valid key (constant-time compare) + brute-force throttle, else 401/429.
  2. Capability gate: the tool's group must be enabled in settings, else a tool error. Tools whose group is off are also hidden from tools/list, so the agent never sees them.

Orientation on connect. initialize returns an instructions string describing this site: its name, the SEO engine detected, whether WooCommerce is active, which groups are on, that writes are journalled and reversible, and that batch exists. The agent starts oriented instead of probing.


Safety: dry run, undo, audit

Three independent layers, because the interesting failures are different at each one: "don't do that", "put that back", and "what did it do last Tuesday?"

Preview: dry_run

Anything that writes broadly defaults to dry_run=true and returns exactly what it would change: bulk_update_products, bulk_update_content, search_replace_content, bulk_set_seo, bulk_set_image_alt, product_seo_fix, bulk_assign_variation_images, optimize_site, optimize_image, database_cleanup, indexnow_submit, and category-wide update_inventory. Repeat with dry_run=false to apply.

Reverse: the undo journal

Every write goes through a journal that snapshots the value it is about to overwrite. Nothing is stored if a tool changes nothing, so read calls cost nothing.

update_product id=4821 regular_price="24.00"
   → { "success": true, …, "operation_id": "260824182233a91f",
       "undo_with": "undo_operation id=260824182233a91f" }

undo_operation id=260824182233a91f
   → { "restored": 1, "success": true }
RecordedPost meta, term meta, post columns, options, the normalised SEO field set, WooCommerce fields through their own setters
CoversBulk repricing, inventory, search and replace, bulk content and SEO writes, schema, alt text, product images, restore points, whole batches
list_operationsThe last 40 write operations: tool, time, records touched, whether already reversed
create_restore_pointSnapshot the SEO, schema and optionally the content of a whole post type before a risky run. One ID restores the lot
GuardsFirst-write-wins per target, so a loop that touches a key twice still reverts to the start. Double-undo refused. An operation too large to record in full is marked incomplete and needs force=true. Undo is itself not journalled.

Not everything is reversible, and the plugin doesn't pretend otherwise: database_cleanup deletes rows for good, and file writes are covered by their own backup system (list_backups / restore_file) rather than the journal.

Account: the audit log

get_audit_log returns every call the server has handled: tool, arguments with secrets redacted, success or failure, duration, requesting IP, a result headline, and the operation_id that would undo it. Filter by tool, writes_only, errors_only, or since. The error log says what broke; this says what was done.

Finish: progress and the time budget

A catalogue-wide sweep used to be a coin flip against max_execution_time. Now:

  • get_progress: call it on a second connection while a run is working to see the tool, items done out of total, elapsed seconds and the current item.
  • A budget: at 70% of the server's execution limit, long sweeps stop, return stopped_early with a next_offset, and say how to continue. Applies to seo_audit, product_seo_audit, product_seo_fix, find_broken_links, sitemap_audit, optimize_image, regenerate_thumbnails, find_unused_media, create_restore_point and batch.
Fewer round trips: batch
{ "tool": "batch", "arguments": { "operations": [
    { "id": "a", "tool": "set_seo",        "args": { "id": 12, "fields": { "title": "…" } } },
    { "id": "b", "tool": "update_product", "args": { "id": 44, "regular_price": "24.00" } }
] } }

Up to 50 steps. Each reports its own result or its own typed error, so one failure does not lose the rest. The whole batch reverses as one operation. A batch cannot contain a batch.


Error handling

An agent can only recover from a failure it can understand. Every error crossing the wire is therefore structured, not prose:

{
  "error": true,
  "tool": "update_product",
  "code": "not_found",
  "message": "Product 4821 was not found.",
  "hint": "Use list_products to find the right ID, or pass sku to get_product.",
  "details": { "id": 4821 }
}

code is stable and machine-readable: invalid_argument, missing_argument, not_found, capability_disabled, dependency_missing, permission_denied, conflict, io_failed, upstream_failed, unknown_tool, tool_failed, fatal_error.

Five layers, outermost first

LayerCatches
TransportMalformed JSON, unknown method (answered as valid JSON-RPC, never a bare 500)
Pre-flightUnknown tool (with a did you mean suggestion), disabled group, missing dependency, bad arguments
ValidationType mismatches, coerced where sane ("42" → 42, "a,b" → ["a","b"]), rejected with a precise message where not
HandlerWooCommerce/WordPress rejections converted into typed errors carrying the fix
Fatal guardA crash inside a handler still returns a readable JSON-RPC error via a shutdown hook

Beyond reporting. PHP warnings raised during a call are captured and returned alongside a successful result under _warnings, so a deprecation inside a theme hook is visible rather than silent. Warning capture is re-entrant, so a tool running inside batch doesn't clear the outer call's state. Every failure lands in a rolling 30-entry log readable with get_error_log and shown on the settings screen. mcp_status reports the plugin's own view of itself: enabled groups, exposed versus defined tool counts, dependency state, memory, and recent failures.

A failed write is still undoable. When a handler throws halfway through a bulk run, the journal is kept rather than discarded, because a half-finished change is exactly the one you want to reverse.

Preventing the unrecoverable one. Writing broken PHP to a live site takes down the site and this endpoint. There is no second call to fix it with. So PHP is parsed before every write and refused if it would not compile, and every overwrite or delete keeps a timestamped backup that restore_file can roll back.


Capability model

Every tool is tagged with exactly one capability group. A group must be enabled before its tools are exposed or runnable. The gate is enforced server-side in dispatch(), not just hidden in the UI. Defaults are agency-safe: the destructive groups ship off.

GroupDefaultSurfaceRisk
Content & SEOon (locked)Posts / pages / any CPT, terms, meta, media library, revisions, comments, JSON-LD, llms.txt, robots, redirects, sitemaps, broken links, IndexNow, Yoast/Rank Math fields, plus batching, undo and resto

Setup from the maintainer

This listing does not have a supported local package template. Use the maintainer’s documentation for its hosted endpoint, authentication, and client-specific setup. No install command has been inferred.

Learn More