# Exposing Your WordPress Plugin to AI Agents With MCP

> If your plugin already registers abilities, the MCP Adapter turns them into agent tools, and the remaining work is scoping, authentication, and schema design.

**Author:** MD Jamil Uddin  
**Published:** 2026-09-26  
**Tags:** WordPress, AI, Architecture  
**Canonical:** https://jamiluddin.com/blog/exposing-your-wordpress-plugin-to-ai-agents-with-mcp/

---

![Plugin capabilities passing through a secure MCP adapter to become tools available to an AI agent](../../assets/blog/exposing-your-wordpress-plugin-to-ai-agents-with-mcp/banner.png)

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](/blog/building-ai-features-on-wordpress-connectors-ai-client-and-abilities/)
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](https://github.com/WordPress/mcp-adapter/tree/v0.6.1).
Install it from the project's [GitHub releases](https://github.com/WordPress/mcp-adapter/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:

```php
'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-abilities` lists what is available
- `mcp-adapter-get-ability-info` returns the schema for one of them
- `mcp-adapter-execute-ability` runs 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`:

```php
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](https://github.com/WordPress/mcp-adapter/blob/v0.6.1/docs/guides/default-server.md#choosing-between-default-and-custom-servers)
for the configuration details.

If you want only your own, the default server can be switched off:

```php
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](/blog/wordpress-mcp-letting-an-ai-assistant-manage-your-site/), 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_plugins` cannot 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](/blog/building-ai-features-on-wordpress-connectors-ai-client-and-abilities/).

```php
'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-text` names 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: true` itself,
  so a required boolean does not establish that a person approved the action.
  The [MCP tool guidance](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/server/tools.mdx)
  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:

```bash
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:

```bash
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:

```json
{"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](https://github.com/WordPress/mcp-adapter/blob/v0.6.1/docs/guides/default-server.md)
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.
