# Declare a WordPress feature once: REST route, WP-CLI command and Ability with wp-kit

A plugin feature often needs more than one entry point. The admin screen calls a REST route, deploy scripts call a WP-CLI command, and with the Abilities API the same feature can also be registered as an Ability. Written by hand, each copy gets its own parameter list, permission check and validation, and over time they stop matching.

[wp-kit](https://github.com/mammothwp/wp-kit) takes a different approach. You declare the operation once, with its fields, capability and output, and three adapters register it on REST, WP-CLI and the Abilities API from that single definition.

## Install

```bash
composer require mammothwp/wp-kit
```

wp-kit is a runtime dependency, because the adapters register routes, commands and abilities inside WordPress, so ship `vendor/` with your plugin. It requires PHP 7.4 or newer. Composer's `autoload.files` loads `bootstrap.php`. Several active plugins can each include their own copy, and the newest copy boots on `plugins_loaded`.

## A minimal example

The service is plain PHP. wp-kit calls its methods and passes declared fields to parameters with the same name.

```php
final class Things_Service
{
    private static $instance;

    public static function instance()
    {
        return self::$instance ?: (self::$instance = new self());
    }

    public function list_items(int $limit = 50)
    {
        $posts = get_posts(['numberposts' => $limit]);

        return ['items' => array_map(function ($post) {
            return ['id' => $post->ID, 'title' => $post->post_title, 'status' => $post->post_status];
        }, $posts)];
    }
}
```

The resource is the one definition. Put it in its own file, `includes/Things_Resource.php`, and load it with a Composer classmap in your plugin's `composer.json`, then run `composer dump-autoload`:

```json
"autoload": {
    "classmap": ["includes/"]
}
```

The kit's classes exist only after `plugins_loaded`, so a class that extends `Resource` cannot be declared while your main plugin file loads. The classmap loads it on first use, inside `Kit::on_ready()`.

```php
use MammothWP\WpKit\{Field, Operation, Resource};

final class Things_Resource extends Resource
{
    public function name()             { return 'things'; }
    public function service()          { return Things_Service::instance(); }
    public function rest_namespace()   { return 'my-plugin/v1'; }
    public function cli_base()         { return 'my-plugin things'; }
    public function ability_category() { return 'my-plugin-things'; }
    public function ability_category_args() { return ['label' => 'Things', 'description' => 'Abilities for things.']; }

    public function operations()
    {
        return [
            'list' => Operation::read('list_items')
                ->label('List things')
                ->description('List things.')
                ->input([
                    Field::int('limit')->default_value(50)->max(200)->describe('Rows to return.'),
                ])
                ->rest('GET')
                ->cli('list', ['columns' => ['id', 'title', 'status']])
                ->ability('list-things'),
        ];
    }
}
```

Register the adapters inside `Kit::on_ready()`:

```php
\MammothWP\WpKit\Kit::on_ready(function () {
    $registry = (new \MammothWP\WpKit\Registry())->add(new Things_Resource())->ready();

    foreach ($registry->all() as $resource) {
        (new \MammothWP\WpKit\Adapters\Rest_Adapter($resource))->register();
        (new \MammothWP\WpKit\Adapters\Cli_Adapter($resource))->register();
        (new \MammothWP\WpKit\Adapters\Ability_Adapter($resource))->register();
    }
});
```

WordPress requires a description on every ability and on its category, which is why the example sets both.

From that one declaration, the `limit` field becomes:

- a REST argument of type integer with `default` 50 and `maximum` 200,
- a `[--limit=<limit>]` flag in the WP-CLI synopsis and in `wp help`,
- the same constraints in the Ability input schema.

The Ability adapter only registers when the Abilities API is available, and the CLI adapter only when WP-CLI is loaded. The capability defaults to `manage_options`. Override `capability()` on the resource, or call `->capability()` on one operation.

## One execution path

Every surface calls `Operation::run()`, which:

1. normalises the input against the fields and applies defaults,
2. validates it (`mmwp_invalid_param`, 400),
3. checks the capability (`mmwp_forbidden`, 403),
4. asks for confirmation on destructive calls where required,
5. calls your service method.

Operations come in three kinds: `Operation::read()`, `Operation::write()` and `Operation::destructive()`. By default, a destructive operation needs `confirm: true` when it is called as an Ability.

Every ability the kit registers is marked `meta.mcp.public = true`, so an MCP server built on `wordpress/mcp-adapter` can list it as a tool, and the call still goes through the same checks. To keep a resource off MCP, return `false` from `mcp()` on the resource.

If an operation should not exist on one surface, say so with `Operation::surfaces()` or by overriding `Resource::surfaces()`, and give a reason. `Inventory::export()` carries that reason, so a test can tell a deliberate omission from a missing registration.

## In a real plugin

[MammothWP Acceptance Logs](https://wordpress.org/plugins/mammoth-acceptance-logs/) on wordpress.org ships wp-kit in its `vendor/` directory.

## When to use it

- Your plugin exposes the same feature over REST and WP-CLI, or you plan to add Abilities.
- You want parameters, permission checks and validation defined in one place.
- You want a test to check that every operation is registered on each surface it should be.

## When not to

- The feature only needs a single REST route or a single command, and you have no plans for other surfaces.
- Your route or command needs request handling that does not fit an operation with declared fields.
- You cannot ship Composer dependencies inside your plugin.

## Links

- GitHub: https://github.com/mammothwp/wp-kit
- Packagist: https://packagist.org/packages/mammothwp/wp-kit
- License: GPLv2 or later
