Getting Started

Registering Your First Ability with wp_register_ability

⏱ 18 min

Course 1 covered the shape of wp_register_ability() in the abstract. This lesson builds one for real: an ability that lets an AI client create a draft post from a title and a body. It’s deliberately simple, the schema and permission work you’ll add on top of it across the next three lessons is where the real engineering happens.

What you'll learn in this lesson
Registering an ability category
Why category is required, and how to register your own on wp_abilities_api_categories_init.
A complete, working ability
A create-draft-post ability with a real execute_callback that calls wp_insert_post().
The two-hook registration order
Categories register before abilities, on separate hooks, and why the order matters.
Confirming an ability actually runs
Calling it directly from PHP before any MCP client is involved.
Prerequisites

The MCP Adapter installed and active (previous lesson). A basic custom plugin file or must-use plugin to add code to. Familiarity with wp_insert_post() is helpful but not required.

Step 1: Register a category first

Every ability must declare a category, and that category has to already be registered when wp_register_ability() runs. If you skip this or reference a category that isn’t registered yet, wp_register_ability() returns null and the ability never exists, with no WP_Error and no visible failure unless WP_DEBUG is on. WordPress core ships two categories you can reuse right away, site and user, but a real plugin should register its own so its abilities are grouped sensibly.

Categories register on wp_abilities_api_categories_init, a separate, earlier hook than the one abilities themselves use:

// File: my-mcp-abilities.php
add_action( 'wp_abilities_api_categories_init', function () {
	wp_register_ability_category(
		'my-plugin',
		array(
			'label'       => __( 'My Plugin', 'my-plugin' ),
			'description' => __( 'Abilities provided by My Plugin.', 'my-plugin' ),
		)
	);
} );

Step 2: Register the ability

Now the ability itself, on wp_abilities_api_init. This one accepts a post title and body and creates a draft:

// File: my-mcp-abilities.php
add_action( 'wp_abilities_api_init', function () {
	wp_register_ability(
		'my-plugin/create-draft-post',
		array(
			'label'               => __( 'Create draft post', 'my-plugin' ),
			'description'         => __(
				'Creates a new WordPress post in draft status from a title and body. ' .
				'Use this when the user asks to draft, start, or save a new post idea.',
				'my-plugin'
			),
			'category'            => 'my-plugin',
			'input_schema'        => array(
				'type'       => 'object',
				'properties' => array(
					'title' => array(
						'type'        => 'string',
						'description' => 'The post title.',
					),
					'body'  => array(
						'type'        => 'string',
						'description' => 'The post content, as HTML or plain text.',
					),
				),
				'required'   => array( 'title', 'body' ),
			),
			'output_schema'       => array(
				'type'       => 'object',
				'properties' => array(
					'post_id'  => array( 'type' => 'integer' ),
					'edit_url' => array( 'type' => 'string' ),
				),
			),
			'permission_callback' => function () {
				return current_user_can( 'edit_posts' );
			},
			'execute_callback'    => function ( $input ) {
				$post_id = wp_insert_post(
					array(
						'post_title'   => $input['title'],
						'post_content' => $input['body'],
						'post_status'  => 'draft',
						'post_type'    => 'post',
					),
					true
				);

				if ( is_wp_error( $post_id ) ) {
					return $post_id;
				}

				return array(
					'post_id'  => $post_id,
					'edit_url' => get_edit_post_link( $post_id, 'raw' ),
				);
			},
			'meta'                => array(
				'annotations' => array( 'destructive' => false, 'idempotent' => false ),
				'mcp'         => array( 'public' => false ),
			),
		)
	);
} );

A few details worth noticing: execute_callback receives the validated $input array as its only argument, matching the shape defined in input_schema. It can return any value on success, or a WP_Error on failure, wp_insert_post()’s second argument (true) makes it return a WP_Error instead of 0 on failure, which we pass straight through. permission_callback can likewise return true/false for a simple check, or a WP_Error for a more descriptive failure, Lesson 4 goes deeper on that. And meta.mcp.public is deliberately false here, this ability isn’t reachable by an external AI client yet, Lesson 5 covers turning that on safely.

Step 3: Why the description matters more than usual

Unlike a typical PHP docblock, description isn’t for other developers, it’s the text an AI model reads to decide whether this ability is the right one to call for a given request. Write it the way you’d brief a new team member on when to use this action, not just what it technically does. “Creates a draft post” is accurate but weak. “Use this when the user asks to draft, start, or save a new post idea” gives a model something concrete to match against.

What a strong ability description does
States the action plainly
What actually happens when this runs.
Gives trigger phrases
Words a user might say that should map to this ability.
Flags side effects
If it writes data, creates content, or is otherwise not read-only, say so.

Test it

Confirm the ability registered and call it directly from PHP, no MCP client needed yet:

# File: terminal
wp eval '
$result = wp_get_ability( "my-plugin/create-draft-post" )->execute( array(
	"title" => "Hello from an ability",
	"body"  => "This draft was created directly, without any AI client involved.",
) );
var_dump( $result );
'

You should see an array containing a real post_id and edit_url. Confirm it in wp-admin too, a new draft post with that title should exist.

Forgetting the category, or registering it too late

If you see wp_get_ability() return null for an ability you just wrote, the most common cause is a missing or unregistered category. Check WP_DEBUG is on and look for a _doing_it_wrong() notice, WordPress core does not throw a hard error here, it fails silently on production. The fix is almost always registering the category on wp_abilities_api_categories_init before the ability that references it, not on wp_abilities_api_init alongside it.

Recap

You registered a real ability category and a real ability that creates a draft post, end to end, and called it directly with wp_get_ability( ... )->execute() before any AI client was involved. The input_schema, output_schema, permission_callback, and execute_callback you wrote here are the exact shape every ability in this course builds on. Next, the schema work gets a full lesson of its own: required fields, types, and enums done properly.

Resources & further reading

← Installing and Configuring the MCP Adapter Defining Input and Output JSON Schemas for Abilities →