---
title: "WordPress MCP Server: Setup, Auth and the 3-Tool Catch"
description: "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."
url: "https://wpwebhooks.org/blog/wordpress-mcp-server/"
date: "2026-09-19"
---

# WordPress MCP Server: Setup, Auth and the 3-Tool Catch

**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](https://developer.wordpress.org/apis/abilities-api/), which landed in 6.9. The [MCP Adapter](https://github.com/WordPress/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](https://wpwebhooks.org/blog/wordpress-abilities-api-ai-agents/), 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](https://github.com/WordPress/mcp-adapter/releases) 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](https://wordpress.org/plugins/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](https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/) 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](https://code.claude.com/docs/en/mcp) 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](https://wpwebhooks.org/docs/connect-claude-code-and-cursor/) 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](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) 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.](https://wpwebhooks.org/blog/wordpress-mcp-server/og_image.jpg)

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

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.

[Try the live preview →](https://playground.wordpress.net/?blueprint-url=https://wpwebhooks.org/blueprint.json) [Install plugin](https://downloads.wordpress.org/plugin/flowsystems-webhook-actions.zip)

/ 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](https://wpwebhooks.org/docs/connect-claude-ai/), 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](https://wpwebhooks.org/blog/ai-agent-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](https://make.wordpress.org/ai/2025/07/17/mcp-adapter/) 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](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports): 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.

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"WordPress MCP Server: Setup, Auth and the 3-Tool Catch","description":"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.","datePublished":"2026-09-19","dateModified":"2026-09-19","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org"},"url":"https://wpwebhooks.org/blog/wordpress-mcp-server/","image":{"@type":"ImageObject","url":"https://wpwebhooks.org/blog/wordpress-mcp-server/og_image.jpg","width":1200,"height":630,"caption":"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."},"keywords":["wordpress mcp","wordpress mcp server","wordpress mcp adapter","woocommerce mcp","connect claude to wordpress","wordpress model context protocol"]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"WP Webhooks","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://wpwebhooks.org/blog/"},{"@type":"ListItem","position":3,"name":"WordPress MCP Server: Setup, Auth and the 3-Tool Catch","item":"https://wpwebhooks.org/blog/wordpress-mcp-server/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Does WordPress have an official MCP server?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Which WordPress version do I need for MCP?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"How does an MCP client authenticate to WordPress?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Why does my MCP client only show three WordPress tools?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Why do MCP calls to WordPress fail after initialize?","acceptedAnswer":{"@type":"Answer","text":"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."}}]}

{"@context":"https://schema.org","@type":"ImageObject","contentUrl":"https://wpwebhooks.org/diagrams/wordpress-mcp-server.png","caption":"FIG 01 — One MCP call, end to end: session, three meta-tools, and the permission check that runs last","description":"An AI client sends an initialize request to the MCP endpoint under wp-json with an Application Password as HTTP Basic credentials. An anonymous request is refused with 401. An authenticated one receives an Mcp-Session-Id response header, which the client must send on every later call or the server answers with error -32600. The client then uses three meta-tools in order: discover-abilities lists the public abilities, get-ability-info returns one ability's input schema, and execute-ability runs it with an ability name and parameters. The Abilities API validates the input against the schema, then runs the ability's own permission check against the signed-in user, and only then calls the plugin code that does the work.","encodingFormat":"image/png","creator":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org/"},"copyrightHolder":{"@type":"Organization","name":"Flow Systems","url":"https://flowsystems.pl/"},"copyrightNotice":"© Flow Systems","creditText":"WP Webhooks","license":"https://creativecommons.org/licenses/by/4.0/","acquireLicensePage":"https://wpwebhooks.org/image-license/"}
```
