TL;DR: A WordPress MCP server is two layers you install and one you already have.
- WordPress 6.9 and newer ship the Abilities API: a registry where core and plugins describe what they can do, with a schema and a permission check per ability.
- The official MCP Adapter turns that registry into an MCP endpoint at
/wp-json/mcp/mcp-adapter-default-server. It is a GitHub release, not a plugin-directory install. - Auth is an Application Password over HTTP Basic. The session is a
Mcp-Session-Idheader you read from theinitializeresponse. - You will see three tools, not thirty. That is by design, and it decides where your safety checks have to live.
/ Definition
What is a WordPress MCP server?
It is an HTTP endpoint on your own site that speaks the Model Context Protocol, so an AI client such as Claude Code, Cursor or claude.ai can discover what the site is able to do and call it. Nothing is proxied through a third party: the client talks to /wp-json/ on your domain, authenticates as a WordPress user, and gets exactly that user's capabilities.
The server itself is thin. WordPress core supplies the registry of things that can be done, the Abilities API, which landed in 6.9. The MCP Adapter, the official package from the WordPress AI team, reads that registry and answers MCP requests with it.¹ If you have read about registering abilities from a plugin, this article is the other half: standing the server up and connecting a client to it.
One consequence is worth stating early. An MCP server on a bare WordPress install is nearly empty. Core registers three abilities: core/get-site-info, core/get-user-info and core/get-environment-info. Everything else a client can do comes from the plugins you run, and only from the ones that register abilities and mark them public.
/ Layers
Which pieces do you actually have to install?
One, in most cases, and there are two ways to get it.
| Route | What you install | Auth it gives you | Pick it when |
|---|---|---|---|
| MCP Adapter | mcp-adapter.zip from the GitHub releases page (v0.6.1), uploaded under Plugins | Application Password, HTTP Basic | The client can send a static header: Claude Code, Cursor, scripts |
| Enable Abilities for MCP | A plugin-directory install (3,000+ active installs) that carries its own copy of the adapter | Application Password, plus an embedded OAuth 2.1 server | You need claude.ai, which refuses anything but OAuth |
The adapter is published as a release ZIP rather than through the plugin directory, so wp plugin install mcp-adapter finds nothing; download the asset and upload it. Releases 0.6.0 and later ship a production ZIP, and 0.6.1 exists only to repair that ZIP: the 0.6.0 build carried a class map pointing at files it had left out, which could fatal any site whose plugins call class_exists( 'WP_CLI' ). Take 0.6.1.
Enable Abilities for MCP is the pragmatic route when OAuth matters. It requires WordPress 6.9, bundles the adapter, and registers a large set of content-management abilities of its own, which you can switch off if you want the client to see only what your other plugins publish.
/ Connect
How do you connect Claude Code or Cursor to WordPress?
With an Application Password and one header. In WordPress, open Users → Profile, create an Application Password on an administrator account, and copy it once; the spaces are part of it and harmless. The client sends it as HTTP Basic credentials.
Claude Code — add the server
# username:application-password, base64-encoded CREDS=$(printf '%s' "admin:abcd EFGH ijkl MNOP qrst UVWX" | base64 -w0) claude mcp add --transport http my-wordpress \ https://example.com/wp-json/mcp/mcp-adapter-default-server \ --header "Authorization: Basic $CREDS"
Cursor takes the same URL and header as an entry in mcp.json. Claude Code's MCP documentation covers scopes and the /mcp status command, which is the quickest way to confirm the connection afterwards. There is a screenshot walkthrough of this route if you want to follow along click by click.
Two things fail in ways that look like a broken server. Permalinks set to Plain make every /wp-json/ path unresolvable, so check https://your-site.com/wp-json/ in a browser first. And an Application Password created on an editor account connects successfully and then finds almost nothing, because each ability runs its own permission check against the signed-in user.
/ Protocol
What does the MCP handshake look like on the wire?
It is JSON-RPC over a single POST endpoint, and the part that trips hand-written clients is the session. The Streamable HTTP transport lets a server assign a session at initialization by returning an Mcp-Session-Id response header, and a client that received one must send it on every later request.² The adapter does assign one. Skip it and every call after initialize fails with JSON-RPC error -32600, which reads like a malformed request rather than a missing header.
curl — initialize, then list tools
# 1. initialize: the session id comes back as a RESPONSE HEADER curl -si https://example.com/wp-json/mcp/mcp-adapter-default-server \ -H "Authorization: Basic $CREDS" -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \ | grep -i mcp-session-id # 2. every later call carries it curl -s https://example.com/wp-json/mcp/mcp-adapter-default-server \ -H "Authorization: Basic $CREDS" -H "Content-Type: application/json" \ -H "Mcp-Session-Id: <value from step 1>" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
An anonymous initialize returns 401 rest_forbidden. That is the behaviour you want from a public endpoint, and it is worth testing once from outside your network rather than assuming it.
/ Tools
Why does the client show only three tools?
Because the default server does not publish one MCP tool per ability. It exposes a fixed trio: mcp-adapter-discover-abilities, mcp-adapter-get-ability-info and mcp-adapter-execute-ability. The model lists what exists, reads one ability's input schema, then calls it through the third tool with two arguments, ability_name and parameters.
Two details save an afternoon. Ability names use a slash, such as core/get-site-info, and core rejects underscores in them, so a plugin whose internal names are snake_case has to convert at the boundary. And the slash becomes a dash in the MCP tool name, which is why the meta-tools read mcp-adapter-… rather than mcp-adapter/….
To the AI client, reading a log and deleting a record are the same tool called with different arguments. Per-tool approval in the client cannot tell them apart, so the confirmation has to live in the ability. — the consequence of three meta-tools
If you would rather hand the client named tools with their own schemas, the adapter has a filter for it, mcp_adapter_default_server_config. Merging ability names into $config['tools'] promotes each one to a first-class MCP tool. That is a better experience in a client with a tool picker, and the only way to get per-tool approval back.
/ Visibility
Why is a registered ability missing from the MCP server?
Because abilities are private by default, and there are four separate ways to stay private by accident. All four fail silently: wp_register_ability() reports a problem through _doing_it_wrong(), which nobody sees on a production site.
- An underscore in the name. Core validates names as lowercase letters, digits and dashes on both sides of one slash.
my-plugin/list_itemsnever registers. - The category registered on the wrong hook. Categories belong on
wp_abilities_api_categories_init, which fires beforewp_abilities_api_init, and need a description. A dropped category takes every ability assigned to it down with it. - No
meta.public. Without it the ability is invisible to MCP and to the core REST surface under/wp-abilities/v1/as well. - An older adapter elsewhere on the site. Plugins have bundled their own copy of the adapter in
vendor/, and whichever copy the autoloader resolves first serves the request. Early versions honour only the nested flag, so declare bothmeta.publicandmeta.mcp.public. The adapter has since deprecated being bundled at all, which is the long-term fix.
To see the failures, hook doing_it_wrong_run around a call to wp_get_abilities() in wp eval. Do not fire wp_abilities_api_init by hand to test; the registry initialises lazily and a manual fire reports a false zero.
| What the client needs | Bare MCP server | With Webhook Actions installed |
|---|---|---|
| Abilities for your other plugins | Whatever each plugin chose to register | Unchanged. It adds its own abilities and does not wrap anyone else's |
| Abilities to call | Three core abilities: site, user and environment info | Its whole toolset as abilities: list the hooks the site fires, create and edit webhooks, read delivery logs, fire a test |
| Destructive calls | Only the client-side approval, which sees one execute tool | Delete, enable and test-fire are refused until the call carries confirmed: true |
| Read-only mode | Remove the user capability, or nothing | A setting holds connected tools to read-only |
| Secrets | Depends on each ability | Stored credentials come back as names and masked hints only |
Seeing it run beats reading about it. The live preview boots a throwaway WordPress with Webhook Actions already installed and demo deliveries sitting in the log — no signup, nothing left on your machine afterwards.
/ claude.ai
How do you connect Claude on the web instead?
Through OAuth, which the adapter does not provide. A claude.ai custom connector will not send a static header; it expects an OAuth 2.1 sign-in and discovers it at /.well-known/oauth-authorization-server. That is what the second route in the table is for: its OAuth server answers that path, and the connector URL becomes /wp-json/mcp/mcp-oauth-server.
Three conditions decide whether it can work at all. The site must be publicly reachable over HTTPS, so localhost and anything behind a VPN or a basic-auth wall is out. The host must let WordPress answer /.well-known/ paths, which some managed hosts reserve for themselves. And custom connectors need a paid Claude plan. If any of those is a problem, the header route above works against a local site with nothing more than an Application Password. The connector setup is documented step by step, including the option Claude pre-selects that you should leave alone.
/ Exposure
What does a WordPress MCP server not protect you from?
Four things, none of which show up in a successful demo.
The Application Password is an administrator. It is not scoped to MCP. Anyone holding it can call the whole REST API as that user, so it belongs in the client's secret store, one per tool, revoked individually when a laptop changes hands.
The model decides what to call. A prompt-injected page or a poisoned issue comment can steer an agent toward execute-ability with arguments you did not intend. Client approval helps only if it can distinguish calls, and with three meta-tools it cannot. Abilities that delete, publish or spend should require an explicit confirmation argument and refuse without it.
Every plugin's abilities arrive together. Installing a plugin that marks ninety abilities public changes what your AI client can do, without a prompt anywhere. Run discover-abilities after each plugin update and read the list. Capability drift is the quiet failure of agent integrations.
There is no rate limit. The endpoint is ordinary REST. An agent stuck in a loop will call it as fast as PHP answers, so put the same request limiting in front of it that you would put in front of xmlrpc.php.
Mcp-Session-Id header of the initialize response, and clients must then include it in all subsequent requests.