WordPress MCP
MCP server for WordPress, built on the official modelcontextprotocol/php-sdk
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/faneraiy14/wordpress-mcp/archive/refs/heads/main.zipReadme
wordpress-mcp
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. Checkscurrent_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 viaMcp\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 installpullsmcp/sdk+nyholm/psr7cleanly.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 bridgesWP_REST_Request-> PSR-7 ->StreamableHttpTransport-> back toWP_REST_Response. initializehandshake: real 200 response with correct MCP envelope.- Session persistence via
FileSessionStoreunderuploads/wordpress-mcp-sessions/(with a.htaccessdeny-all so it's not web-readable, same lesson as the debug.log fix in luchezarik-arhiv). tools/callonget_site_healthreturns 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
WP_REST_Request::get_headers()canonicalizes header names to lowercase+underscore (Mcp-Session-Id->mcp_session_id); forwarding into PSR-7 needsstr_replace('_', '-', $name)or the SDK's session-id header lookup never matches.Server::builder()->addTool()doesn't accept a bare invokable object, needs[$obj, '__invoke']callable-array form.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 requiresupdate.php,plugin.php, andmisc.phpbefore touchingWP_Site_Health. Found by adding asetLogger()toServer::builder()that writes to WP'serror_log()- each missing function named its own file via "Call to undefined function", then cross-checked with a one-shot script that greps every functionclass-wp-site-health.phpcalls and diffs against what's already loaded, to catch the rest in one pass instead of one HTTP round-trip per missing function.- A plain
\InvalidArgumentExceptionthrown 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\ToolCallExceptionis the SDK's intended type for this -CallToolHandlercatches it specifically and surfaces the real message as a properisError: truetool 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 realWP_Site_Health::get_tests()/get_test_*()methods (verified against the actual core source inluchezarik-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- wrapget_posts(),get_post(), andwp_update_post()respectively. The two read tools require nothing beyondcurrent_user_can('read_post', ...)(or nothing at all for the list);update_postrequirescurrent_user_can('edit_post', ...).src/PluginFormatter.php+src/Tool/ListPluginsTool.php- wrapsget_plugins()+ theactive_pluginsoption.wordpress-mcp.php- plugin bootstrap: registers the REST route, builds the MCP server per-request (stateless PHP process model - session continuity comes entirely fromFileSessionStore, not from anything held in memory between requests)..github/workflows/tests.yml- runsphpuniton push/PR across PHP 8.1 and 8.3.
Next steps (roughly in order)
- 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). - Open a PR against
modelcontextprotocol/php-sdkadding 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.)