Blog

How to build a custom MCP server for WordPress

Build a custom MCP server for WordPress with the Abilities API and the MCP Adapter: register an ability, connect Claude, then add a write ability that saves drafts only.

Coding on a dark theme computer screen

Any WordPress site on 6.9 or later has the parts for a custom MCP server. The Abilities API in core lets a plugin describe what it can do, and the MCP Adapter, now a canonical plugin on WordPress.org, turns those descriptions into tools an AI assistant can call.

This guide takes about an hour: a read-only ability, the adapter, a connected client, then a write ability that leaves the last step to a person.

Key takeaways

  • A WordPress site on 6.9 or later already has what it needs for a custom MCP server. The Abilities API in core describes what a plugin can do, and the MCP Adapter turns those descriptions into tools an AI assistant can call. arvindkumar.ca
  • Register each ability with wp_register_ability() on the right hook, with a clear description and a permission check that runs as the connected user, so the agent can never do more than that user could. arvindkumar.ca
  • Install the MCP Adapter, give a dedicated user an Application Password and point your client at the default server. The assistant then reaches your abilities through the three default adapter tools, not through a tool of its own. arvindkumar.ca
  • For anything that writes, prepare the change and leave the last step to a person. Keep the rule in your code, for example by creating drafts and never publishing, and test that a user without the capability is refused and bad input is rejected. arvindkumar.ca

Summarised from this post, formatted for answer engines.

What do you need before you build a custom MCP server for WordPress?

You need WordPress 6.9 or later, PHP 7.4 or later, HTTPS, and a staging copy of the site. WordPress 6.9 is where the Abilities API joined core; the MCP Adapter plugin (version 0.7.1 at the time of writing) requires the same. For the client side you need an MCP client such as Claude Desktop and Node.js, because remote sites connect through Automattic’s @automattic/mcp-wordpress-remote proxy.

Put abilities in a small plugin of their own, not the theme’s functions.php, so they survive a theme change.

How do you register an ability with the WordPress Abilities API?

You register a category on wp_abilities_api_categories_init, then the ability on wp_abilities_api_init with wp_register_ability(). Each ability needs a name in the form namespace/ability-name, a label, a description, a category, an output schema, a permission callback and the function that does the work. Add an input schema too: WordPress checks input against it before your code runs.

  1. Create a folder in wp-content/plugins, save the file below in it and activate the plugin. It worked if the plugin activates without a notice with WP_DEBUG on.
<?php
/**
 * Plugin Name: My Site Abilities
 * Requires at least: 6.9
 */

add_action( 'wp_abilities_api_categories_init', function () {
	wp_register_ability_category( 'my-site', array(
		'label'       => 'My site',
		'description' => 'Abilities for working with this site\'s content.',
	) );
} );

add_action( 'wp_abilities_api_init', function () {
	wp_register_ability( 'my-site/list-drafts', array(
		'label'               => 'List drafts',
		'description'         => 'Lists the most recently edited draft posts with ID, title and last modified date. Read only.',
		'category'            => 'my-site',
		'input_schema'        => array(
			'type'       => 'object',
			'properties' => array(
				'limit' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 20 ),
			),
		),
		'output_schema'       => array(
			'type'       => 'object',
			'properties' => array(
				'drafts' => array( 'type' => 'array' ),
			),
		),
		'permission_callback' => function () {
			return current_user_can( 'edit_others_posts' );
		},
		'execute_callback'    => function ( $input = array() ) {
			$posts = get_posts( array(
				'post_status' => 'draft',
				'orderby'     => 'modified',
				'numberposts' => isset( $input['limit'] ) ? (int) $input['limit'] : 10,
			) );
			$drafts = array();
			foreach ( $posts as $post ) {
				$drafts[] = array(
					'id'       => $post->ID,
					'title'    => get_the_title( $post ),
					'modified' => $post->post_modified_gmt,
				);
			}
			return array( 'drafts' => $drafts );
		},
		'meta'                => array(
			'public'      => true,
			'annotations' => array(
				'readonly'    => true,
				'destructive' => false,
				'idempotent'  => true,
			),
		),
	) );
} );

The description is what an assistant reads when it decides whether to call the ability, so say what comes back and that it only reads. The permission check runs as the connected user, so the agent can never do more than that user could. Abilities are private until 'public' => true in meta lets the adapter’s default server offer them, and the annotations are hints clients can use.

How do you expose the ability through the MCP Adapter and connect a client?

Install the MCP Adapter, give a dedicated user an Application Password, and point your client at the default server. This is the Claude Desktop entry you’ll need in step 3, with your staging URL, the agent’s username and its Application Password:

{
  "mcpServers": {
    "my-site": {
      "command": "npx",
      "args": ["-y", "@automattic/mcp-wordpress-remote@latest"],
      "env": {
        "WP_API_URL": "https://staging.example.com/wp-json/mcp/mcp-adapter-default-server",
        "WP_API_USERNAME": "mcp-editor",
        "WP_API_PASSWORD": "xxxx xxxx xxxx xxxx xxxx xxxx"
      }
    }
  }
}
  1. Install the adapter from Plugins, Add New Plugin: search for MCP Adapter and activate it. It worked if https://staging.example.com/wp-json/mcp lists /mcp/mcp-adapter-default-server.
  2. Create a user just for the agent, with the Editor role (the ability above needs edit_others_posts), and under Users, Profile, Application Passwords add one named after the client. It worked if WordPress shows the new password once; copy it then.
  3. In Claude Desktop, open Settings, Developer, Edit config, add the entry above, save and restart the app. It worked if the server shows as connected.
  4. Ask the assistant to list your site’s abilities, then to list your drafts. It worked if my-site/list-drafts appears and returns your draft titles.

Your ability won’t be in the client’s tool list. The default server offers three tools, mcp-adapter-discover-abilities, mcp-adapter-get-ability-info and mcp-adapter-execute-ability, and the assistant reaches your abilities through them. To give an ability its own tool, list it in a custom server created on the mcp_adapter_init hook. If the connection fails, WordPress MCP Adapter errors and how to fix them goes through each error message.

How do you add a write ability that needs a person’s approval?

Make the ability prepare the change and stop short of making it live. For content, that means it creates drafts and never publishes. Your MCP client may ask before it runs a tool, but that prompt belongs to the client; the rule belongs in your code, where no agent can talk its way past it.

  1. Register my-site/create-draft the same way, with title and content as required strings in the input schema, edit_posts in the permission check, and this callback and meta. It worked if asking the assistant for a draft leaves a new draft in Posts, and nothing goes live.
'execute_callback' => function ( $input ) {
	$id = wp_insert_post( array(
		'post_title'   => sanitize_text_field( $input['title'] ),
		'post_content' => wp_kses_post( $input['content'] ),
		'post_status'  => 'draft', // Never 'publish': a person does that.
	), true );
	return is_wp_error( $id ) ? $id : array( 'id' => $id, 'edit_link' => get_edit_post_link( $id, 'raw' ) );
},
'meta' => array(
	'public'      => true,
	'annotations' => array( 'readonly' => false, 'destructive' => false, 'idempotent' => false ),
),

The edit link tells the person exactly where to review. Anything that deletes, changes posts in bulk or touches settings stays out of the first version.

What are the common mistakes, and how do you check the result?

The mistakes I’d check for first:

  • Connecting as an administrator. The agent inherits every capability that user has. Use the smallest role that works and one Application Password per client.
  • Registering outside the hooks. wp_register_ability() returns null when it runs outside wp_abilities_api_init or names a category that doesn’t exist, and with WP_DEBUG off it fails silently.
  • Using __return_true as the permission check. The WordPress developer blog’s introduction to the adapter warns against it for destructive operations.

To check the result, test it like any plugin. I test my plugins with Playwright, and here I’d confirm three things: a user without the capability is refused, bad input is rejected by the schema, and the write ability never produces a published post. AI can help write those tests, but the thinking and the review stay with the developer.

Run the read-only ability on staging for a week and watch where the assistant answers well and where it guesses. That tells you which ability to write next better than any list made up front.

4 questions about building a WordPress MCP server, answered

What do I need to build a custom MCP server for WordPress?

You need WordPress 6.9 or later, PHP 7.4 or later, HTTPS and a staging copy of the site. You also need an MCP client such as Claude Desktop and Node.js, because remote sites connect through Automattic's @automattic/mcp-wordpress-remote proxy.

How do I register an ability in WordPress?

Register a category on wp_abilities_api_categories_init, then register the ability with wp_register_ability() on wp_abilities_api_init. Each ability needs a name in the form namespace/ability-name, a label, a description, a category, an output schema, a permission callback and the function that does the work.

Why doesn't my ability show up in the MCP client's tool list?

The default server offers only three tools: mcp-adapter-discover-abilities, mcp-adapter-get-ability-info and mcp-adapter-execute-ability. The assistant reaches your abilities through those, and to give an ability its own tool you list it in a custom server created on the mcp_adapter_init hook.

How do I stop an AI agent from publishing content on my WordPress site?

Make the ability create drafts and never publish, and keep that rule in your code rather than relying on the client's prompt. Connect the agent as a user with the smallest role that works, not an administrator, because the agent inherits every capability that user has.

Read next