WP Manifestindependent plugin directory
manifest / ai / wordpress-mcp

WordPress MCP

MCP server for WordPress, built on the official modelcontextprotocol/php-sdk

★ 0stars

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/faneraiy14/wordpress-mcp/archive/refs/heads/main.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

MCP server for WordPress, built on the official mcp/sdk. Runs as a WordPress plugin, serves MCP over the REST API at /wp-json/wordpress-mcp/v1/mcp.

Goal: get WordPress added to the "PHP Libraries Using the MCP SDK" list in php-sdk's README - currently missing despite WordPress running roughly 40% of the web.

Status: v0 working end-to-end, five tools (read + write), CI running

A real MCP client (fetch() with a real WP nonce) gets a real initialize handshake, a real session, and real tools/call responses from a running WordPress site - not mocks, not stubs.

  • get_site_health - runs WordPress's own Site Health checks, returns a compact critical/recommended/good summary.
  • list_posts(limit, status) - recent posts (id, title, status, date, excerpt, url).
  • get_post(post_id) - one post's full content, not just an excerpt.
  • list_plugins() - installed plugins with name, version, description, and active/inactive.
  • update_post(post_id, title?, content?, status?) - the one write tool. Checks current_user_can('edit_post', $post_id) per call (not just "is authenticated" - an Author can edit their own posts but not someone else's). Errors are surfaced properly to the client via Mcp\Exception\ToolCallException (a plain exception gets sanitized by the SDK into a generic, unhelpful message).

What works (verified live against a real local WordPress install)

  • composer install pulls mcp/sdk + nyholm/psr7 cleanly.
  • SiteHealthSummarizer (pure PHP, no WP dependency) - 3/3 unit tests passing.
  • The REST route is registered, requires an authenticated WP user (is_user_logged_in()), and correctly bridges WP_REST_Request -> PSR-7 -> StreamableHttpTransport -> back to WP_REST_Response.
  • initialize handshake: real 200 response with correct MCP envelope.
  • Session persistence via FileSessionStore under uploads/wordpress-mcp-sessions/ (with a .htaccess deny-all so it's not web-readable, same lesson as the debug.log fix in luchezarik-arhiv).
  • tools/call on get_site_health returns real data: e.g. {"critical_count": 3, "recommended_count": 3, "good_count": 10, "critical_issues": [{"label": "You have plugins waiting to be updated", ...}, ...]}.

Bugs found and fixed along the way

  1. WP_REST_Request::get_headers() canonicalizes header names to lowercase+underscore (Mcp-Session-Id -> mcp_session_id); forwarding into PSR-7 needs str_replace('_', '-', $name) or the SDK's session-id header lookup never matches.
  2. Server::builder()->addTool() doesn't accept a bare invokable object, needs [$obj, '__invoke'] callable-array form.
  3. WP_Site_Health's test methods span several wp-admin-only files that aren't loaded outside a real wp-admin page request. SiteHealthTool::__invoke() now requires update.php, plugin.php, and misc.php before touching WP_Site_Health. Found by adding a setLogger() to Server::builder() that writes to WP's error_log() - each missing function named its own file via "Call to undefined function", then cross-checked with a one-shot script that greps every function class-wp-site-health.php calls and diffs against what's already loaded, to catch the rest in one pass instead of one HTTP round-trip per missing function.
  4. A plain \InvalidArgumentException thrown from a tool gets sanitized by the SDK's default catch-all into a generic -32603 Error while executing tool, hiding the real message from the client. Mcp\Exception\ToolCallException is the SDK's intended type for this - CallToolHandler catches it specifically and surfaces the real message as a proper isError: true tool result.

Architecture

Each tool follows the same split: a pure formatter/summarizer class (no WordPress dependency, unit-tested) plus a thin Tool\* glue class (calls real WP functions, needs a running WordPress to test).

  • src/SiteHealthSummarizer.php + src/Tool/SiteHealthTool.php - calls the real WP_Site_Health::get_tests() / get_test_*() methods (verified against the actual core source in luchezarik-arhiv/web/wp-admin/includes/class-wp-site-health.php, not guessed).
  • src/PostFormatter.php (format() for lists, formatDetailed() for a single post) + src/Tool/ListPostsTool.php, src/Tool/GetPostTool.php, src/Tool/UpdatePostTool.php - wrap get_posts(), get_post(), and wp_update_post() respectively. The two read tools require nothing beyond current_user_can('read_post', ...) (or nothing at all for the list); update_post requires current_user_can('edit_post', ...).
  • src/PluginFormatter.php + src/Tool/ListPluginsTool.php - wraps get_plugins() + the active_plugins option.
  • wordpress-mcp.php - plugin bootstrap: registers the REST route, builds the MCP server per-request (stateless PHP process model - session continuity comes entirely from FileSessionStore, not from anything held in memory between requests).
  • .github/workflows/tests.yml - runs phpunit on push/PR across PHP 8.1 and 8.3.

Next steps (roughly in order)

  1. Write a real test for the WP-runtime glue if feasible (currently only the pure formatters/summarizer are unit-tested; the Tool\* classes themselves need a running WordPress to test).
  2. Open a PR against modelcontextprotocol/php-sdk adding one line to the "PHP Libraries Using the MCP SDK" README section.

Testing locally

Symlinked into a real local WordPress install for live testing:

ln -s ~/Projects/wordpress-mcp ~/Projects/luchezarik-arhiv/web/wp-content/plugins/wordpress-mcp

(luchezarik-arhiv is unrelated to this project - it's just the local WP instance that happened to be running. Any local WP install works.)