Exposing Your WordPress Plugin to AI Agents With MCP
- WordPress
- AI
- Architecture
Senior Full Stack Engineer · AI Focused

If your plugin already registers WordPress abilities, the MCP Adapter can expose them to AI clients with little additional code. You still need to choose which abilities to expose, describe their inputs and outputs, and check what the connecting user is allowed to do.
I have written about registering abilities and using the AI Client already. Here I will follow a read-only ability for finding images with missing alt text through exposure and a test call.
How the adapter connects abilities to MCP
The Model Context Protocol is a standard way for an AI client to discover tools, read their schemas, and call them. The WordPress MCP Adapter maps registered abilities to that interface, so you can reuse their schemas and callbacks without implementing the protocol yourself.
An ability’s permission callback still decides whether the current user may run it. Exposing an ability does not grant permission to execute it.
The examples below target MCP Adapter v0.6.1. Install it from the project’s GitHub releases. It is a pre-1.0 package; test against the version you intend to deploy.
Making an ability visible
Abilities are not exposed through the default server unless you opt in. An ability becomes available through the adapter’s default server when its metadata says so:
'meta' => array(
'public' => true,
),
There is a more specific form. Setting meta.mcp.public to true exposes an
ability to MCP only, and setting it to false opts an otherwise public
ability out of MCP while leaving it available elsewhere. That distinction is
useful when other integrations also consume your abilities. The word public
here means discoverable through the adapter; it does not bypass authentication
or the ability’s permission callback.
The default server does not register your abilities as individual tools. Instead it exposes three meta-tools, and the agent works through them:
mcp-adapter-discover-abilitieslists what is availablemcp-adapter-get-ability-inforeturns the schema for one of themmcp-adapter-execute-abilityruns it
This indirection keeps the initial tool schemas small. A site with sixty abilities still sends three tool schemas in its
tools/list response instead of sixty, and the agent fetches detail only for
what it actually intends to call.
Running your own server instead of the default one
A custom server fits a plugin exposing a small, focused set of capabilities. Each listed ability becomes a first-class MCP tool with its schema visible immediately, so the agent calls it in one step rather than three.
Servers are created on mcp_adapter_init:
add_action( 'mcp_adapter_init', function ( $adapter ) {
$adapter->create_server(
'content-server', // Unique server ID
'my-plugin', // REST API namespace
'mcp', // REST API route
'Content Server', // Human-readable name
'Focused content management tools',
'1.0.0',
array( \WP\MCP\Transport\HttpTransport::class ),
\WP\MCP\Infrastructure\ErrorHandling\ErrorLogMcpErrorHandler::class,
null, // Observability handler
array( // Tools
'my-plugin/find-images-missing-alt-text',
'my-plugin/set-image-alt-text',
),
array(), // Resources
array() // Prompts
);
} );
With these namespace and route arguments, the endpoint is
https://example.com/wp-json/my-plugin/mcp. The default endpoint is
https://example.com/wp-json/mcp/mcp-adapter-default-server.
Explicitly listing an ability on a custom server does not require the public metadata flag used by default-server discovery. Its permission callback still applies.
Use the default server when you want discovery across a changing collection of abilities. Use a custom server when you want an explicit tool list and schemas available up front. Each custom tool adds to the initial schema payload, so keep the list focused. You can run both. See the adapter’s server comparison for the configuration details.
If you want only your own, the default server can be switched off:
add_filter( 'mcp_adapter_create_default_server', '__return_false' );
Transports are pluggable. HttpTransport covers the REST case, STDIO is
available through WP-CLI for local work, and McpTransportInterface is there
if you need something else.
Authentication is the part that is still yours
Over HTTP, the adapter leans on WordPress application passwords. There is no built-in OAuth for self-hosted sites. WordPress.com offers OAuth 2.1 on its own MCP endpoint, but that is a platform feature and not something you inherit by installing the adapter. I have written about what that split looks like from the site owner’s side, and it affects which clients you can connect without additional authentication work.
What that means in practice:
- The endpoint is public surface area. It is a REST route. Rate limiting, logging and abuse handling are your responsibility, exactly as they would be for any other route you registered.
- Application passwords are per-user, not per-scope. The capability boundary comes from the user account, so the account you point an agent at is the real permission decision.
- Use a dedicated account with limited capabilities. Give it only the
access the integration needs, and enforce capability checks in each ability.
An account without
install_pluginscannot install plugins through code that correctly checks that capability. - Use one application password per client. Revoke a client’s password without interrupting the others.
Designing abilities an agent can actually use
An agent uses descriptions and schemas to choose a tool and construct its
arguments. For the alt-text example, give it a bounded request and a result
it can inspect. These are registration fields for a proposed read-only
my-plugin/find-images-missing-alt-text ability; your plugin must supply the
category, permission callback, and query implementation described in the
earlier ability-registration article.
'description' => 'Find image attachments whose alt text is empty. Returns attachment IDs and titles. Does not modify images or generate alt text.',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'limit' => array(
'type' => 'integer',
'minimum' => 1,
'maximum' => 20,
'description' => 'Maximum number of images to return.',
),
),
'required' => array( 'limit' ),
'additionalProperties' => false,
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'images' => array(
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array( 'type' => 'integer' ),
'title' => array( 'type' => 'string' ),
),
'required' => array( 'id', 'title' ),
),
),
),
'required' => array( 'images' ),
),
The description tells the agent what the ability finds and what it leaves untouched. The limit bounds each request. The callback must return the declared shape and enforce permissions for the records it exposes.
The same choices apply to other abilities:
- Describe intent, not implementation. An agent chooses a tool by reading
its description.
find-images-missing-alt-textnames a specific task; a general query tool needs more explanation of its filters and intended use. - Descriptions are functional text. Explain when to use the ability, what its arguments mean, and any important limits. A vague description can make tool selection less reliable.
- Keep inputs narrow and enumerated. An enum of four statuses is easier for a model to get right than a free string, and it fails loudly rather than quietly when it is wrong.
- Require human approval for destructive actions. Use an approval step
enforced by the client or server. A model can set
confirm: trueitself, so a required boolean does not establish that a person approved the action. The MCP tool guidance recommends a human in the loop who can deny tool invocations. - Return structured output. Prose is easy to generate and hard to verify. A typed result can be checked, and the model can reason about it without re-parsing English.
Give the agent enough information to choose the ability from its description and schema without needing to guess what your plugin means.
Testing it without an AI client in the loop
Debugging through a chat window is slow and non-deterministic. The adapter ships a WP-CLI path that speaks the protocol directly, which makes it possible to check behaviour before any model is involved:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
| wp mcp-adapter serve --user=admin --server=mcp-adapter-default-server
Once the example ability is registered and exposed on the default server, call it through the execution meta-tool:
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"mcp-adapter-execute-ability","arguments":{"ability_name":"my-plugin/find-images-missing-alt-text","parameters":{"limit":5}}}}' \
| wp mcp-adapter serve --user=admin --server=mcp-adapter-default-server
For a fixture with one matching image, the ability callback might return:
{"images":[{"id":42,"title":"Workshop entrance"}]}
That is the ability’s result shape, not the full MCP response envelope.
On the custom server above, the direct MCP tool name is
my-plugin-find-images-missing-alt-text, and its arguments are {"limit":5}.
Use a known fixture and assert the returned data in CI. Also test an invalid
limit and a user who lacks the required permission. A successful call as
admin does not verify the permission boundary. These tests cover the ability
and adapter path; separately check HTTP authentication and session handling
with the client you intend to use. The adapter’s
default-server guide
includes the HTTP initialization sequence.
The official AI plugin also ships an Abilities Explorer in the admin, which can help you confirm an ability registered before investigating why an agent cannot see it.
Start with one read-only ability and a user account limited to the access it needs. Check discovery, valid and invalid calls, and permission failures before connecting an agent. Then watch whether the agent chooses the ability for the requests you intended. Its mistakes can reveal where the description needs more detail before you add tools that change data.