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_Interfaceenforces 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.
Quick Start
Section titled “Quick Start”1. Create Category Class (Optional)
Section titled “1. Create Category Class (Optional)”Create 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(); }}2. Create Ability Class
Section titled “2. Create Ability Class”Create 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 ); }}3. Register Ability
Section titled “3. Register Ability”In Demo_Plugin.php:
$this->loader->add_ability( Abilities\My_Ability::class );Categories are automatically registered via the Loader when abilities reference them.
Registration is guarded by a test
Section titled “Registration is guarded by a test”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.
Ability Interface Reference
Section titled “Ability Interface Reference”| Method | Returns | Purpose |
|---|---|---|
get_name() | string | Unique ID: namespace/ability-name |
get_label() | string | Display name |
get_description() | string | What the ability does |
get_category() | string | Category class name |
get_input_schema() | array | JSON Schema for input |
get_output_schema() | array | JSON Schema for output |
get_annotations() | array | Behavioral hints (readonly, destructive, idempotent) |
show_rest() | bool | Expose via REST API |
get_mcp() | array | MCP exposure config, registered as meta.mcp |
check_permissions($input) | bool|WP_Error | Permission check |
execute($input) | mixed | Main logic |
Category Interface Reference
Section titled “Category Interface Reference”| Method | Returns | Purpose |
|---|---|---|
get_slug() | string | Unique slug (lowercase, hyphens only) |
get_label() | string | Display name |
get_description() | string | Category purpose |
get_meta() | array | Optional 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();}