WP Webhooks / Blog / AI integration

Running an MCP Server on WordPress and Connecting an AI Client

Set up a WordPress MCP server with the official MCP Adapter: install routes, Application Password auth, the Mcp-Session-Id header, and the three-tool design.

9 min 2026-09-19
#mcp#ai#abilities

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-Id header you read from the initialize response.
  • 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.

RouteWhat you installAuth it gives youPick it when
MCP Adaptermcp-adapter.zip from the GitHub releases page (v0.6.1), uploaded under PluginsApplication Password, HTTP BasicThe client can send a static header: Claude Code, Cursor, scripts
Enable Abilities for MCPA plugin-directory install (3,000+ active installs) that carries its own copy of the adapterApplication Password, plus an embedded OAuth 2.1 serverYou 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.

FIG 01 — One MCP call, end to end: session, three meta-tools, and the permission check that runs last

/ 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.

Cyberpunk illustration of a vast manual telephone exchange seen from inside its own frame: dozens of patch cords drawn taut on their own towards three lit mint pilot lamps high on an otherwise dead switchboard, an empty operator's chair pushed back and a single camera lens watching from the ceiling.

/ 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.

  1. An underscore in the name. Core validates names as lowercase letters, digits and dashes on both sides of one slash. my-plugin/list_items never registers.
  2. The category registered on the wrong hook. Categories belong on wp_abilities_api_categories_init, which fires before wp_abilities_api_init, and need a description. A dropped category takes every ability assigned to it down with it.
  3. No meta.public. Without it the ability is invisible to MCP and to the core REST surface under /wp-abilities/v1/ as well.
  4. 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 both meta.public and meta.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 needsBare MCP serverWith Webhook Actions installed
Abilities for your other pluginsWhatever each plugin chose to registerUnchanged. It adds its own abilities and does not wrap anyone else's
Abilities to callThree core abilities: site, user and environment infoIts whole toolset as abilities: list the hooks the site fires, create and edit webhooks, read delivery logs, fire a test
Destructive callsOnly the client-side approval, which sees one execute toolDelete, enable and test-fire are refused until the call carries confirmed: true
Read-only modeRemove the user capability, or nothingA setting holds connected tools to read-only
SecretsDepends on each abilityStored credentials come back as names and masked hints only
try_it

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.

/Footnotes
¹ The adapter was announced on Make WordPress AI as part of the AI Building Blocks initiative. Release data read from the GitHub repository on 2026-09-19: v0.6.1, published 2026-08-13.
² Session management is defined in the MCP transports specification: a server may assign a session id in the Mcp-Session-Id header of the initialize response, and clients must then include it in all subsequent requests.
FAQ

Things engineers always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

Does WordPress have an official MCP server? +
Yes. The MCP Adapter is the official WordPress package that exposes abilities registered with the Abilities API as Model Context Protocol tools, resources and prompts. It is distributed as a release ZIP on GitHub rather than through the plugin directory, and serves an endpoint at /wp-json/mcp/mcp-adapter-default-server.
Which WordPress version do I need for MCP? +
WordPress 6.9 or newer, which is the release that added the Abilities API to core. The MCP Adapter reads that registry. On a bare install only three core abilities exist, for site, user and environment information; everything else comes from plugins that register public abilities.
How does an MCP client authenticate to WordPress? +
With a WordPress Application Password sent as HTTP Basic credentials. The client acts as that user, and every ability runs its own permission check against them. Claude on the web is the exception: its custom connectors require OAuth 2.1, which needs an additional plugin that provides an OAuth server.
Why does my MCP client only show three WordPress tools? +
The default server exposes three meta-tools: discover-abilities, get-ability-info and execute-ability. Every registered ability is reached through execute-ability using an ability_name and parameters. The mcp_adapter_default_server_config filter can promote individual abilities to first-class MCP tools with their own schemas.
Why do MCP calls to WordPress fail after initialize? +
Usually a missing session header. The adapter returns an Mcp-Session-Id header on the initialize response, and the client must send it on every later request. Without it the server answers with JSON-RPC error -32600. Plain permalinks are the other common cause, because /wp-json/ paths do not resolve.
Ready

Your next automation is
one sentence away.

$ wp plugin install flowsystems-webhook-actions --activate