Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
WordPressAI and AutomationDevelopmentPHPIndustry update

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.

Cloudsoft Zone

Updated 9 min read

Illustration of a glowing circuit-board brain with a chip at its centre on a dark background
Photograph: mikemacmarketing / photo on flickr / CC BY 2.0, via Wikimedia Commons
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:

PartWhat it does
Namenamespace/ability-name, lowercase letters, digits and hyphens; the namespace is normally your plugin slug.
Label and descriptionHuman-readable text that people and AI clients use to decide when to call it.
CategoryExactly one category, registered before the ability.
input_schema / output_schemaJSON Schema used to validate what goes in and what comes out.
permission_callbackDecides whether the current user may run it.
execute_callbackThe PHP that does the work.
metaExposure 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 the wp-abilities/v1 namespace. Abilities are private by default.
  • WordPress 7.0 "Armstrong" (May 2026) added the client-side packages @wordpress/abilities and @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 public flag, filtering arguments for wp_get_abilities(), four execution lifecycle filters, custom validation filters, a wp_ability_invoked action and typed input for REST runs. The current release is 7.1.3, a security release published on 6 October 2026.
Panelists seated on stage under a screen reading Exploring the Future of WordPress at WordCamp Asia 2025
The keynote panel on the future of WordPress at WordCamp Asia 2025. AI tooling has been a recurring theme since then.

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:

StepHookTypical use
1wp_ability_invoked (action)Audit and telemetry; fires for every call, even failed ones.
2wp_pre_execute_abilityShort-circuit: maintenance mode, cached results, approvals.
3wp_ability_normalize_inputAdd context or reject a request before validation.
4wp_ability_validate_inputExtra validation that JSON Schema cannot express.
5wp_ability_permission_resultAdd an authorisation rule on top of the callback.
6wp_ability_execute_resultStrip internal data or recover from a known error.
7wp_ability_validate_outputExtra 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
<?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:

PHP
$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:

Shell
# 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:

Shell
wp mcp-adapter list
wp mcp-adapter serve --user=editor-bot

Remote 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.

People working at laptops on a table next to a standing humanoid robot at an exhibition
Agents only act through the abilities you expose, so give them a dedicated low-privilege 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:

PHP
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_rest and filtered lists only control visibility. The permission_callback is the security boundary, and the 7.1 dev notes say so explicitly.
  • Older versions ignore public. On 6.9 and 7.0 only show_in_rest exposes an ability over REST. If your plugin supports those versions, set both flags.
  • Returning true from wp_ability_permission_result can override a denial. Preserve false and WP_Error results unless you mean to grant access.
  • REST-style validate_callback and sanitize_callback keywords in a schema are not executed. Use wp_ability_validate_input instead.
  • 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 readonly lets clients call it with GET and retry it freely.

What to do next

  1. List the actions in your plugin that an assistant could reasonably perform, and start with read-only ones.
  2. Register them with tight schemas, real capability checks and honest annotations.
  3. Move from show_in_rest to public for abilities meant for any client, keeping show_in_rest for older WordPress versions.
  4. Test with an Application Password on a staging site before connecting any AI client to production.
  5. Add audit logging through wp_ability_invoked before 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/abilities and @wordpress/core-abilities, and WordPress 7.1 added the public flag, filtered discovery and the execution lifecycle hooks.

  • Are abilities exposed over the REST API by default?

    No. Abilities are private by default. Set meta.public to true (WordPress 7.1 and later) or meta.show_in_rest to true. 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?

    public records that an ability is meant for external clients in general, and REST uses it as the default for show_in_rest. An explicit show_in_rest value 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 /run endpoint uses GET for read-only abilities, POST with a JSON body for abilities that change data, and DELETE for 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 the wp_ability_permission_result or wp_pre_execute_ability filters.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. Abilities API — WordPress Developer Resources, 2025-12-03
  2. Abilities API: REST API endpoints — WordPress Developer Resources
  3. A unified public exposure flag for Abilities in WordPress 7.1 — Make WordPress Core, 2026-08-04
  4. New execution lifecycle filters for the Abilities API in WordPress 7.1 — Make WordPress Core, 2026-07-29
  5. Filtering registered abilities with wp_get_abilities() in WordPress 7.1 — Make WordPress Core, 2026-08-05
  6. Abilities API improvements in WordPress 7.1 — Make WordPress Core, 2026-07-31
  7. 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.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.