WordPress 7.1 Abilities API: expose plugin actions to AI safely
What the Abilities API changed in WordPress 7.1: the public flag, filtered discovery, lifecycle hooks and typed REST input, with a working ability.
Updated 9 min read

In this article
The WordPress Abilities API lets a plugin describe an action once, with a name, a JSON Schema for its input and output, and a permission check, so that PHP code, the REST API, the block editor and AI agents can all discover and run it the same way. It arrived in WordPress 6.9, gained a JavaScript counterpart in 7.0, and WordPress 7.1 (August 2026) made it much easier to expose abilities deliberately and control what happens when they run.
If you maintain a plugin, this is the API that decides whether an AI assistant connected to a client's site can use your features, and under which rules. This update explains what changed in 7.1, shows a complete read-only ability, and lists the mistakes that turn a helpful integration into a security problem.
What an ability is
An ability is a single, named unit of work, such as "list my drafts" or "update a site option". Each one is registered in a central registry and is an instance of WP_Ability. The official handbook lists the parts every ability has:
| Part | What it does |
|---|---|
| Name | namespace/ability-name, lowercase letters, digits and hyphens; the namespace is normally your plugin slug. |
| Label and description | Human-readable text that people and AI clients use to decide when to call it. |
| Category | Exactly one category, registered before the ability. |
input_schema / output_schema | JSON Schema used to validate what goes in and what comes out. |
permission_callback | Decides whether the current user may run it. |
execute_callback | The PHP that does the work. |
meta | Exposure flags and annotations such as readonly, destructive and idempotent. |
How the API grew from 6.9 to 7.1
- WordPress 6.9 (December 2025) added the registry,
wp_register_ability(), categories and REST endpoints under thewp-abilities/v1namespace. Abilities are private by default. - WordPress 7.0 "Armstrong" (May 2026) added the client-side packages
@wordpress/abilitiesand@wordpress/core-abilities, so editor code can register and execute abilities in the browser. The same release introduced the provider-agnostic WP AI Client and the Settings > Connectors screen, and the AI Client can call abilities. - WordPress 7.1 "Mary Lou" (August 2026) added a unified
publicflag, filtering arguments forwp_get_abilities(), four execution lifecycle filters, custom validation filters, awp_ability_invokedaction and typed input for REST runs. The current release is 7.1.3, a security release published on 6 October 2026.

What changed in WordPress 7.1
One flag to say "this ability is for external clients"
Before 7.1 you exposed an ability channel by channel, for example 'show_in_rest' => true for REST and a separate MCP setting for the MCP Adapter. WordPress 7.1 adds meta.public. When it is true, show_in_rest defaults to true, and integrations such as the MCP Adapter can use the same value. A channel-specific setting still wins, so 'public' => true, 'show_in_rest' => false keeps an ability off REST. The core abilities core/get-site-info, core/get-user-info and core/get-environment-info now use the new flag.
Filtering abilities without hand-written loops
wp_get_abilities() now accepts category, namespace and meta arguments, combined with AND logic, plus per-call item_include_callback and result_callback options. The REST list endpoint uses the same code, so ?namespace=acme and ?meta[annotations][readonly]=true work as query parameters. One behaviour change matters: two new global filters run even when you call wp_get_abilities() with no arguments, so if you need the raw registry, use WP_Abilities_Registry::get_instance()->get_all_registered().
Hooks for the whole execution pipeline
Ability execution now runs in a fixed order, and 7.1 adds filters at the points where plugins most often need control:
| Step | Hook | Typical use |
|---|---|---|
| 1 | wp_ability_invoked (action) | Audit and telemetry; fires for every call, even failed ones. |
| 2 | wp_pre_execute_ability | Short-circuit: maintenance mode, cached results, approvals. |
| 3 | wp_ability_normalize_input | Add context or reject a request before validation. |
| 4 | wp_ability_validate_input | Extra validation that JSON Schema cannot express. |
| 5 | wp_ability_permission_result | Add an authorisation rule on top of the callback. |
| 6 | wp_ability_execute_result | Strip internal data or recover from a known error. |
| 7 | wp_ability_validate_output | Extra checks on the result. |
Input and output transformations run before their schema checks, so whatever a filter returns must still match the registered schema.
Typed input over REST
Query strings deliver everything as text. In 7.1, a run request such as ?input[limit]=10&input[featured]=true is coerced to the types declared in input_schema before your permission and execute callbacks see it, so 10 arrives as an integer and true as a boolean.
Register a read-only ability, step by step
The plugin below exposes one ability, acme/list-drafts, which returns the current user's most recently edited drafts. It is read-only, idempotent and requires the edit_posts capability.
<?php
/**
* Plugin Name: Acme Editorial Abilities
* Description: Exposes a read-only "list my drafts" ability.
* Version: 1.0.0
* Requires at least: 6.9
* Requires PHP: 7.4
* Text Domain: acme
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// 1. Categories must exist before abilities that use them.
add_action( 'wp_abilities_api_categories_init', 'acme_register_ability_category' );
function acme_register_ability_category() {
wp_register_ability_category(
'acme-editorial',
array(
'label' => __( 'Editorial', 'acme' ),
'description' => __( 'Read-only information about editorial work in progress.', 'acme' ),
)
);
}
// 2. Register the ability itself.
add_action( 'wp_abilities_api_init', 'acme_register_abilities' );
function acme_register_abilities() {
wp_register_ability(
'acme/list-drafts',
array(
'label' => __( 'List my drafts', 'acme' ),
'description' => __( 'Returns the current user\'s most recently edited draft posts.', 'acme' ),
'category' => 'acme-editorial',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'limit' => array(
'type' => 'integer',
'minimum' => 1,
'maximum' => 20,
'default' => 5,
),
),
'additionalProperties' => false,
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'total' => array( 'type' => 'integer' ),
'drafts' => array(
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array( 'type' => 'integer' ),
'title' => array( 'type' => 'string' ),
'modified' => array( 'type' => 'string' ),
),
),
),
),
),
'execute_callback' => 'acme_list_drafts',
'permission_callback' => static function () {
return current_user_can( 'edit_posts' );
},
'meta' => array(
'public' => true, // WordPress 7.1+: REST and other clients.
'annotations' => array(
'readonly' => true,
'destructive' => false,
'idempotent' => true,
),
),
)
);
}
// 3. The work itself. Input has already been validated against the schema.
function acme_list_drafts( $input = array() ) {
$limit = isset( $input['limit'] ) ? (int) $input['limit'] : 5;
$query = new WP_Query(
array(
'post_type' => 'post',
'post_status' => 'draft',
'author' => get_current_user_id(),
'posts_per_page' => $limit,
'orderby' => 'modified',
'order' => 'DESC',
)
);
$drafts = array();
foreach ( $query->posts as $post ) {
$drafts[] = array(
'id' => (int) $post->ID,
'title' => get_the_title( $post ),
'modified' => get_post_modified_time( 'c', true, $post ),
);
}
return array(
'total' => (int) $query->found_posts,
'drafts' => $drafts,
);
}Three details are worth copying. The schema caps limit at 20, so no client can ask for every draft on the site. The annotations tell clients the call is safe to repeat. And the permission check uses a real capability, not __return_true.
Call it from PHP, REST and an MCP client
From PHP, fetch the ability and run it. The result is either your array or a WP_Error:
$ability = wp_get_ability( 'acme/list-drafts' );
if ( $ability ) {
$result = $ability->execute( array( 'limit' => 3 ) );
if ( is_wp_error( $result ) ) {
error_log( $result->get_error_message() );
}
}Over REST, every Abilities endpoint requires an authenticated user. Application Passwords are the recommended method for external tools. Read-only abilities run with GET, others with POST and a JSON body, and destructive ones with DELETE:
# Discover your plugin's abilities
curl -u 'editor-bot:APPLICATION_PASSWORD' \
"https://example.com/wp-json/wp-abilities/v1/abilities?namespace=acme"
# Run the read-only ability (input is typed by the schema in 7.1)
curl -u 'editor-bot:APPLICATION_PASSWORD' \
"https://example.com/wp-json/wp-abilities/v1/acme/list-drafts/run?input%5Blimit%5D=3"For AI agents, the official MCP Adapter plugin (version 0.7.0 in the plugin directory) converts abilities into Model Context Protocol tools. Abilities with meta.public (or meta.mcp.public) set to true are reachable through the default server's three tools: mcp-adapter/discover-abilities, mcp-adapter/get-ability-info and mcp-adapter/execute-ability. Locally, a client can talk to it over STDIO through WP-CLI:
wp mcp-adapter list
wp mcp-adapter serve --user=editor-botRemote clients use the HTTP endpoint at /wp-json/mcp/mcp-adapter-default-server. Run the server as a dedicated account with the smallest role that can do the job, never as your own administrator account.

Add guard rails with the new 7.1 hooks
The lifecycle filters let a site owner or a companion plugin apply policy without editing every ability. This example records every call without logging its input, and pauses the ability during maintenance:
add_action(
'wp_ability_invoked',
static function ( $ability_name, $input, $ability ) {
if ( 0 !== strpos( $ability_name, 'acme/' ) ) {
return;
}
// Never log raw input: it can contain personal data or secrets.
error_log( sprintf( 'Ability %s called by user %d', $ability_name, get_current_user_id() ) );
},
10,
3
);
add_filter(
'wp_pre_execute_ability',
static function ( $pre, $ability_name, $input, $ability ) {
if ( 'acme/list-drafts' !== $ability_name || ! get_option( 'acme_maintenance', false ) ) {
return $pre; // Unchanged: continue normally.
}
return new WP_Error(
'ability_temporarily_unavailable',
__( 'This ability is paused for maintenance.', 'acme' ),
array( 'status' => 503 )
);
},
10,
4
);Gotchas
- Exposure is not authorisation.
public,show_in_restand filtered lists only control visibility. Thepermission_callbackis the security boundary, and the 7.1 dev notes say so explicitly. - Older versions ignore
public. On 6.9 and 7.0 onlyshow_in_restexposes an ability over REST. If your plugin supports those versions, set both flags. - Returning
truefromwp_ability_permission_resultcan override a denial. PreservefalseandWP_Errorresults unless you mean to grant access. - REST-style
validate_callbackandsanitize_callbackkeywords in a schema are not executed. Usewp_ability_validate_inputinstead. - WP-CLI lists every ability, public or not, so do not rely on hiding an ability for safety.
- Annotations are promises. Marking a write operation as
readonlylets clients call it withGETand retry it freely.
What to do next
- List the actions in your plugin that an assistant could reasonably perform, and start with read-only ones.
- Register them with tight schemas, real capability checks and honest annotations.
- Move from
show_in_resttopublicfor abilities meant for any client, keepingshow_in_restfor older WordPress versions. - Test with an Application Password on a staging site before connecting any AI client to production.
- Add audit logging through
wp_ability_invokedbefore you enable write abilities.
Frequently asked questions
Which WordPress version introduced the Abilities API?
WordPress 6.9, released in December 2025. WordPress 7.0 added the JavaScript packages
@wordpress/abilitiesand@wordpress/core-abilities, and WordPress 7.1 added thepublicflag, filtered discovery and the execution lifecycle hooks.Are abilities exposed over the REST API by default?
No. Abilities are private by default. Set
meta.publictotrue(WordPress 7.1 and later) ormeta.show_in_resttotrue. Even then, every Abilities endpoint requires an authenticated user and the ability's permission callback must pass.Is the MCP Adapter part of WordPress core?
No. It is a separate official plugin, maintained by the WordPress project on GitHub and published in the plugin directory, that turns abilities into Model Context Protocol tools, resources and prompts over HTTP or STDIO.
What is the difference between public and show_in_rest?
publicrecords that an ability is meant for external clients in general, and REST uses it as the default forshow_in_rest. An explicitshow_in_restvalue always wins, so you can make an ability public but keep it off REST, or the reverse.Which HTTP method runs an ability over REST?
The
/runendpoint usesGETfor read-only abilities,POSTwith a JSON body for abilities that change data, andDELETEfor destructive ones, based on the ability's annotations.How do I stop an AI agent from running a risky ability?
Do not expose it, give the agent's account a role without the required capability, and enforce the rule in the ability's
permission_callback. In 7.1 you can also add a site-wide policy with thewp_ability_permission_resultorwp_pre_execute_abilityfilters.
Your next customer could start right here.
Introduce your tool, product or business to readers exploring technology.
Explore placements and request a private quote
Sources
- Abilities API — WordPress Developer Resources, 2025-12-03
- Abilities API: REST API endpoints — WordPress Developer Resources
- A unified public exposure flag for Abilities in WordPress 7.1 — Make WordPress Core, 2026-08-04
- New execution lifecycle filters for the Abilities API in WordPress 7.1 — Make WordPress Core, 2026-07-29
- Filtering registered abilities with wp_get_abilities() in WordPress 7.1 — Make WordPress Core, 2026-08-05
- Abilities API improvements in WordPress 7.1 — Make WordPress Core, 2026-07-31
- MCP Adapter — WordPress on GitHub
Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.
Comments
No comments yet. Be the first to join the conversation.
Leave a comment
Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.
Related tools
- Directory profile
ChatGPT
OpenAI's AI assistant for chat, writing, image creation, file analysis and coding with Codex, on web, desktop and mobile, with paid plans for individuals and teams.
A tool connected to a service selected for this article. Compare its fit for your project; inclusion is not an endorsement.
- Directory profile
Claude
Anthropic's AI assistant for chat, writing, research, coding and file analysis, available on web, desktop and mobile and through a developer API.
A tool connected to a service selected for this article. Compare its fit for your project; inclusion is not an endorsement.
More from Cloudsoft Zone

Resource guideWordPress
WordPress security hardening checklist for 2026 (WordPress 7.1)
A practical WordPress 7.1 hardening checklist based on the official handbook: updates, two-step logins, wp-config.php constants, file permissions, database privileges, firewalls, WP-CLI checksum checks and tested backups.
By Cloudsoft Zone

TutorialWordPress
How to update WordPress safely with WP-CLI: backup, update, verify, roll back
A repeatable WordPress update routine with WP-CLI: check pending updates, export the database and archive files, update core, plugins, themes and translations in order, verify checksums, and restore if something breaks.
By Cloudsoft Zone

Troubleshooting guideCodeIgniter
CodeIgniter 4 validation not working? 9 causes and fixes
Fix CodeIgniter 4 validation problems fast: wrong input source, lost errors after redirect, permit_empty, strict JSON rules, is_unique placeholders and more.
By Cloudsoft Zone
