The WordPress
Abilities API
And how to interact with WooCommerce
using human language.
Marco Almeida · WordPress Faro Meetup · September 3, 2026
Marco Almeida
Chief Executive Meerkat at
Webdados / Naked Cat Plugins
WordPress.org
Twitter / X (I refuse to call it X)
GitHub
LinkedIn
Section 1
What is the
Abilities API?
Your plugin can do a lot.
But does WordPress know that?
Does Claude? Does anything outside your code?
1 / Introduction
Timeline
From plugin to core
| Version | Status |
| WP 6.8 | Composer package & feature plugin |
| WP 6.9 | Merged into core · Three read-only core abilities shipped |
| WP 7.0 | JavaScript client (@wordpress/abilities) · Hybrid abilities · WP AI Client |
| Abilities Explorer | Admin screen for browsing & testing abilities, available via the AI Experiments plugin, not core |
1 / Introduction
Register once.
Every surface discovers it.
| Consumer | Available since |
| PHP (direct) | WP 6.9 |
| REST API | WP 6.9 |
WP-CLI wp ability (separate package) | WP 6.9+ |
| MCP (via WordPress MCP Adapter) | WP 7.0 |
| JavaScript client | WP 7.0 |
| Command Palette | Coming |
| Workflows API | Coming |
| A2A · WebMCP · UTCP | Coming |
Section 2
Why better
than hooks?
What happens when you call do_action with
the wrong arguments?
Nothing. Silently. It just runs.
Hooks are powerful but dumb. They don't validate input. They don't document output. They don't check permissions unless you remember to add that yourself, and we all know how that ends. Half the WordPress plugin vulnerability database says hi.
2 / Technical
The validation chain
One contract, enforced end-to-end
input schema
→
permission callback
→
execute callback
→
output schema
Any failure → clean WP_Error. Your callback never sees bad data.
- Typed input & output: JSON Schema, validated before your code runs
- Permission callback: per-ability, centralized, auditable
- Self-describing: label, description, schemas, all introspectable at runtime
2 / Technical
Annotations
Machine-readable intent
'readonly' => true
Safe to call freely. Generates a GET request. No side effects.
'destructive' => true
May delete or irreversibly change data. Agents ask for confirmation.
'idempotent' => true
Safe to retry with the same input. Same result every time.
These map directly to MCP hints. Agents read them before calling, so your annotations control AI behavior.
2 / Technical
Hooks vs Abilities
Same power. One has a contract.
|
Hooks |
Abilities |
| Discoverable? | ✗ | ✓ |
| Validated input? | ✗ | ✓ |
| Typed output? | ✗ | ✓ |
| Permission model? | Manual | Built-in |
| Works with AI agents? | ✗ | ✓ |
Section 3
How to use it
Four functions.
That's the whole API surface you need to learn.
3 / How to use
The functions
Register · Discover · Execute
// Step 1: Register your category
// Hook: wp_abilities_api_categories_init
wp_register_ability_category()
// Step 2: Register your ability
// Hook: wp_abilities_api_init
wp_register_ability()
// Step 3: Discover what's registered
wp_get_abilities()
wp_get_ability( 'namespace/name' )
// Step 4: Execute from PHP
$ability = wp_get_ability( 'namespace/name' );
$result = $ability->execute( [ 'order_id' => 42 ] );
if ( is_wp_error( $result ) ) {
// handle error: $result->get_error_message()
} else {
// use result: $result['tracking_number']
}
3 / How to use
WP-CLI
Test without the LLM getting creative
$ wp cli update --nightly --allow-root
$ wp ability list
$ wp ability get your-plugin/your-ability
$ wp ability run your-plugin/your-ability --input='{"order_id":42}' --user=admin
Chat with Alain Schlesser
3 / How to use
Backwards compat guard
One line. Protects you on WP 6.8 and older.
if ( function_exists( 'wp_register_ability' ) ) {
// register your abilities
}
3 / How to use
What core ships today
Three read-only abilities. The foundation, not the ceiling.
| Ability | What it returns | Category |
core/get-site-info |
Site name, URL, description, language, timezone, date/time formats |
site |
core/get-user-info |
Current user's basic profile: login, display name, email, roles |
user |
core/get-environment-info |
WordPress version, PHP version, active theme, is multisite |
site |
The goal was to lay the foundation, not ship everything at once. Core keeps the ability count small and deliberate; plugins and WooCommerce fill the gaps.
Coming in 7.1+: core/get-active-theme · core/list-plugins · core/get-site-health · core/get-settings · core/update-settings
Available via PHP and WP-CLI, not MCP, by default. None of these three ship with meta.mcp.public set to true. That's deliberate: registered abilities aren't exposed to MCP by default, so agents can't touch anything until a site owner explicitly opts it in, even read-only core abilities. Flip it with a wp_register_ability_args filter if you want Claude to see them.
Section 4
Abilities API
& the MCP Adapter
"Show me all processing orders
from the last 7 days."
That's it. That's the interface.
4 / MCP Adapter
Install the MCP Adapter
Not on WordPress.org yet, grab it from GitHub
GitHub:
github.com/WordPress/mcp-adapter → Releases → download the plugin zip → Plugins → Add New → Upload → Activate
Composer:
composer require wordpress/mcp-adapter
// No feature flag. No settings screen.
// Activation alone creates a default
// server that exposes every
// public ability automatically.
HTTP: /wp-json/mcp/mcp-adapter-default-server
STDIO: wp mcp-adapter serve
4 / MCP Adapter
Connect Claude Code
Prerequisites
- No Node.js, no proxy, no
npx: Claude Code speaks HTTP to the adapter directly
- A WordPress user with the right capability for the abilities you'll call (e.g.
manage_woocommerce)
- An Application Password for that account
Create it:
Users → Your Profile → Application Passwords
Name it (e.g. "Claude Code") → Add New Application Password → copy it now, it's shown once.
4 / MCP Adapter
Connect Claude Code
One command. Restart Claude Code. Done.
$ echo -n "marco:xxxx xxxx xxxx xxxx xxxx xxxx" | base64
$ claude mcp add --transport http wordpress \
https://yourstore.com/wp-json/mcp/mcp-adapter-default-server \
--header "Authorization: Basic <base64-string>"
4 / MCP Adapter
What WooCommerce exposes
Seven canonical abilities out of the box
| Domain | Abilities |
| Orders |
woocommerce/orders-query find orders
woocommerce/order-add-note add order note
woocommerce/order-update-status update order status
|
| Products |
woocommerce/products-query find products
woocommerce/product-create create product
woocommerce/product-update update product
woocommerce/product-delete delete/trash/restore product
|
4 / MCP Adapter
Live demo
Natural language. Real data.
- "List all processing orders from the last 7 days"
- "Which products have stock below 5?"
- "What's my total revenue this week, broken down by day?"
- "Update product #42 stock to 0"
Section 5
Creating your
own abilities
WooCommerce's built-in abilities are just the start.
Your plugin can join that conversation.
5 / Creating abilities
Step 1: Register a category
Hook: wp_abilities_api_categories_init
add_action(
'wp_abilities_api_categories_init',
function() {
wp_register_ability_category( 'woo-dpd-portugal', [
'label' => __( 'DPD Portugal', 'woo-dpd-portugal' ),
'description' => __(
'DPD Portugal shipping abilities.',
'woo-dpd-portugal'
),
] );
}
);
5 / Creating abilities
Step 2: Register the ability
Hook: wp_abilities_api_init
add_action(
'wp_abilities_api_init',
function() {
wp_register_ability( 'woo-dpd-portugal/create-label', [
'label' => __( 'Create Shipping Label', 'woo-dpd-portugal' ),
'description' => __( 'Creates a DPD shipping label for a given order.', 'woo-dpd-portugal' ),
'category' => 'woo-dpd-portugal',
'input_schema' => [
'type' => 'object',
'properties' => [
'order_id' => [ 'type' => 'integer', 'description' => 'The WooCommerce order ID.' ],
'volumes' => [ 'type' => 'integer', 'description' => 'Number of volumes. Defaults to 1.' ],
],
'required' => [ 'order_id' ],
],
'output_schema' => [
'type' => 'object',
'properties' => [
'tracking_number' => [ 'type' => 'string', 'description' => 'Carrier tracking number.' ],
'label_url' => [ 'type' => 'string', 'description' => 'URL to download the PDF label.' ],
],
'required' => [ 'tracking_number', 'label_url' ],
],
'execute_callback' => 'dpd_create_label',
'permission_callback' => fn() => current_user_can( 'manage_woocommerce' ),
'meta' => [
'mcp' => [
'public' => true,
'type' => 'tool',
],
'annotations' => [
'readonly' => false,
'destructive' => false,
'idempotent' => false,
],
],
] );
}
);
5 / Creating abilities
Step 3: The legacy filter
One filter. Your entire namespace. Visible to Claude Code.
add_filter(
'woocommerce_mcp_include_ability',
function( $include, $ability_id ) {
if ( str_starts_with( $ability_id, 'woo-dpd-portugal/' ) ) {
return true;
}
return $include;
},
10,
2
);
Deprecated as of WooCommerce 10.9: this filter only scopes the old WooCommerce-specific MCP bridge. WooCommerce now exposes abilities through the standard WordPress MCP Adapter instead, set meta.mcp.public on the ability itself and it's discovered automatically, no filter needed.
Section 6
Live demo
Real plugin. Real courier API.
Mostly real store.
6 / Live demo
One prompt.
Claude orchestrates everything.
💬
[ live demo ]
How cool was that?
Well... it depends.
6 / Live demo
Same abilities. Smarter caller.
Fine for a one-off demo. Wasteful for the exact same job every day.
🎬 Live demo (interactive)
Claude orchestrates one call at a time, doing the counting, category checks, and next-step decisions itself in between
- woocommerce/orders-queryonce
- woocommerce/products-querylooped per line item, uncached, to resolve category for the volume rule
- woo-dpd-portugal/create-shipping-label× order
- woocommerce/order-add-note× order
- woocommerce/order-update-status× order
- webdados-toolbox/send-sms× order
- woo-dpd-portugal/request-collectonce
- woo-dpd-portugal/end-of-day-reportonce
⚙️ Daily batch (one call)
One prompt calls one ability, not sixty
- my-custom-abilities/process-daily-ordersinput: a date
Internally, in PHP, no LLM in the loop, it calls the exact same abilities:
- orders-query → products-query looped per product, cached
- create-shipping-label → order-add-note → order-update-status → send-sms looped server-side
- request-collect → end-of-day-report once, to close the day
Someone still has to write that PHP: the loop, the sequencing, the error handling. That's the developer's job, and it's exactly what turns AI into something that saves you money on daily token usage instead of costing you more.
Section 7
What this changes
for you
You've been writing hooks for years.
This isn't a replacement*. It's an upgrade.
* but it can be
7 / What changes
Register once. Let everything in.
PHP · REST · WP-CLI · MCP · Command Palette · whatever comes next
- You don't rewire anything. One registration. Every surface.
- Your plugin becomes discoverable. Composable. Agent-ready.
- The list of consumers will only grow. Your abilities grow with it, for free.
- When building abilities for deterministic workflows or analytical data: do the hard work inside the ability. Don't leave calculation or business logic to the AI agent, it's slower, less reliable, and costs tokens.
Note: This is exactly why "register once" matters in practice. When WooCommerce deprecated its own MCP bridge in favor of the shared WordPress MCP Adapter, none of the abilities anyone had registered needed to change, only the transport in front of them did. Earlier versions of this setup (WC 10.3–10.8) used a WooCommerce-specific MCP beta with its own endpoint and feature flag. That's gone now; the Adapter shown in Section 4 is the way in.
Questions or suggestions?
Thanks for attending.