Skip to content

WordPress Abilities API – WordPress Plugin Boilerplate

Why This Plugin Includes Abilities Support

Section titled “Why This Plugin Includes Abilities Support”

The project provides interfaces and Loader integration for the WordPress Abilities API to give you a head start when exposing your plugin’s functionality.

What this integration offers:

  • Consistent structure — The Ability_Interface enforces input/output schemas, permission checks, and annotations so every ability follows the same pattern.
  • Automatic registration — Categories are collected and registered automatically when you add an ability to the Loader.
  • Type safety — PHPStan and IDE autocompletion guide your implementation.
  • No overhead when unused — If you don’t register abilities, no code runs. On WordPress < 6.9, the feature is silently skipped.

To understand what the Abilities API is or why you might use it see the official documentation.


Create src/Abilities/Categories/Data_Retrieval.php:

src/Abilities/Categories/Data_Retrieval.php
<?php
namespace Demo_Plugin\Abilities\Categories;
use Demo_Plugin\Abilities\Ability_Category_Interface;
/**
* Category for abilities that retrieve data without modifications.
*/
class Data_Retrieval implements Ability_Category_Interface {
public static function get_slug(): string {
return 'data-retrieval';
}
public static function get_label(): string {
return __( 'Data Retrieval', 'demo-plugin' );
}
public static function get_description(): string {
return __( 'Abilities that retrieve and return data from the WordPress site.', 'demo-plugin' );
}
public static function get_meta(): array {
return array();
}
}

Create src/Abilities/My_Ability.php:

src/Abilities/My_Ability.php
<?php
namespace Demo_Plugin\Abilities;
use Demo_Plugin\Abilities\Categories\Data_Retrieval;
use WP_Error;
class My_Ability implements Ability_Interface {
public static function get_name(): string {
return 'demo-plugin/my-ability';
}
public static function get_label(): string {
return __( 'My Ability', 'demo-plugin' );
}
public static function get_description(): string {
return __( 'Does something useful.', 'demo-plugin' );
}
public static function get_category(): string {
return Data_Retrieval::class;
}
public static function get_input_schema(): array {
return array(
'type' => 'object',
'properties' => array(
'post_id' => array( 'type' => 'integer', 'minimum' => 1 ),
),
'required' => array( 'post_id' ),
);
}
public static function get_output_schema(): array {
return array(
'type' => 'object',
'properties' => array(
'success' => array( 'type' => 'boolean' ),
'title' => array( 'type' => 'string' ),
),
);
}
public static function get_annotations(): array {
return array(
'readonly' => true,
'destructive' => false,
'idempotent' => true,
);
}
public static function show_rest(): bool {
return true;
}
public static function get_mcp(): array {
// Expose this ability on the MCP Adapter's default server. Return an empty
// array instead to keep it off MCP (it can still be added to a custom server).
return array( 'public' => true );
}
public static function check_permissions( mixed $input = null ): bool|WP_Error {
return current_user_can( 'read' );
}
public static function execute( mixed $input = null ): mixed {
$post = get_post( $input['post_id'] );
if ( ! $post ) {
return new WP_Error( 'not_found', 'Post not found.', array( 'status' => 404 ) );
}
return array( 'success' => true, 'title' => $post->post_title );
}
}

In Demo_Plugin.php:

src/Demo_Plugin.php
$this->loader->add_ability( Abilities\My_Ability::class );

Categories are automatically registered via the Loader when abilities reference them.

Because registration is manual, the easy mistake is writing an ability class but forgetting the add_ability() call — it then silently does nothing. tests/php/AbilityRegistrationTest.php guards against this: it discovers every class implementing Ability_Interface (by reflection, so it survives the setup rename) and asserts each one — and its category — is present in the live Abilities API registry (wp_get_ability() / wp_get_ability_category()).

The test needs no changes when you add abilities, and is skipped when none exist or on WordPress < 6.9. Run it with npm run test:php.

MethodReturnsPurpose
get_name()stringUnique ID: namespace/ability-name
get_label()stringDisplay name
get_description()stringWhat the ability does
get_category()stringCategory class name
get_input_schema()arrayJSON Schema for input
get_output_schema()arrayJSON Schema for output
get_annotations()arrayBehavioral hints (readonly, destructive, idempotent)
show_rest()boolExpose via REST API
get_mcp()arrayMCP exposure config, registered as meta.mcp
check_permissions($input)bool|WP_ErrorPermission check
execute($input)mixedMain logic
MethodReturnsPurpose
get_slug()stringUnique slug (lowercase, hyphens only)
get_label()stringDisplay name
get_description()stringCategory purpose
get_meta()arrayOptional metadata
$ability = wp_get_ability( 'demo-plugin/my-ability' );
$result = $ability->execute( array( 'post_id' => 123 ) );
if ( is_wp_error( $result ) ) {
echo $result->get_error_message();
}
  • PHP67%
  • JavaScript33%