Make WordPress Core

Opened 5 months ago

Closed 3 months ago

Last modified 10 days ago

#64990 closed enhancement (fixed)

Add filtering support to `wp_get_abilities()`

Reported by: gziolo Owned by: gziolo
Priority: normal Milestone: 7.1
Component: Abilities API Version: 6.9
Severity: normal Keywords: has-patch has-unit-tests has-dev-note
Cc: Focuses:

Description

Background


The Abilities API landed in WordPress 6.9 (dev note) with registration, retrieval, and REST API exposure for abilities. The PHP API currently offers two retrieval paths:

// All registered abilities.
$abilities = wp_get_abilities();
 
// A single ability by name.
$ability = wp_get_ability( 'core/create-post' );


The REST API mirrors this with GET /wp-json/wp/v2/abilities (list all) and GET /wp-json/wp/v2/abilities/{name} (single), plus a ?category query parameter for filtering by category slug.

There is no server-side filtering support in the PHP API. Callers who need a subset — by category, namespace, meta properties, or any combination — must retrieve all abilities and filter manually.

Observed need


As the number of registered abilities grows (core, plugins, themes), several consumers have independently built ad-hoc filtering to work around this gap:

  • MCP Adapter checks meta.mcp.public and meta.show_in_rest to determine which abilities to expose, using its own array_filter pass.
  • WooCommerce (v10.3+) introduced a woocommerce_mcp_include_ability filter that performs namespace-prefix matching (str_starts_with( $ability_id, 'woocommerce/' )) to scope its custom MCP server.
  • WebMCP adapter experiment (WordPress/ai#224) hardcoded isAbilityPublicForAgents to return true because core abilities lack protocol-specific metadata, with a comment noting this needs a proper public flag that cascades into show_in_rest, show_in_mcp, show_in_webmcp.
  • The REST API controller for abilities already supports ?category=slug, but the underlying PHP function it delegates to (wp_get_abilities()) has no filtering — the controller does its own post-retrieval filtering.


This duplication signals a missing primitive. Each consumer reimplements the same patterns (namespace matching, meta checks, category scoping) with slightly different semantics.

Prior exploration


This was tracked as WordPress/abilities-api#38 ("Proposal: Add a convenient way to filter the list of all registered abilities"). Two competing implementations were explored before the repo was archived:

  • PR #115WP_Abilities_Query class modeled after WP_Query, with array-based $args (category, namespace, search, meta, orderby, order, limit, offset). Reviewed by @jason_the_adams, @justlevine, @jorgefilipecosta, @swissspidy.
  • PR #119WP_Abilities_Collection class with fluent chainable methods (->where_category(), ->where_namespace(), ->filter(), ->sort_by()), inspired by Laravel Collections.

Both draft POC by @ovidiu-galatan. Key review feedback that emerged:

  1. WP_*_Query implies DB-backed storage (@justlevine): abilities are an in-memory registry, not a database. The Query pattern sets wrong expectations and adds unnecessary complexity (pagination, query vars, getters) for what is essentially array_filter over a PHP array.
  2. Collections introduce a new paradigm (@jorgefilipecosta): WordPress has no *_Collection pattern anywhere. Blocks, patterns, and other registries don't use it. Introducing it for abilities alone raises consistency questions across the project.
  3. Return type BC break (@gziolo): wp_get_abilities() returns WP_Ability[] today. Changing it to return a Collection object would break every array_* call site and type expectation downstream.
  4. REST API is query-style (@jorgefilipecosta): REST filtering is inherently ?category=x&namespace=y, so a Collection approach would require translation back to query-style args anyway.
  5. Extensibility (@gziolo, @swissspidy): Some mechanism for custom filtering logic (e.g., OR across meta conditions) is needed beyond what declarative args can express.


The team deferred the feature from 6.9, agreeing it needed more time to settle on the right API shape. The WordPress/abilities-api repo was archived on February 5, 2026 with this issue still open.

Current limitations


  • wp_get_abilities() accepts no arguments and always returns the full registry.
  • The REST API supports ?category filtering, but this is implemented in the controller rather than the underlying PHP function, creating a mismatch between PHP and REST capabilities.
  • There is no namespace filtering at any layer.
  • There is no meta filtering at any layer — consumers who need show_in_rest === true or mcp.public === true abilities must filter manually.
  • There is no extensibility hook for custom filtering logic (e.g., role-based visibility, protocol-specific gates).


Change History (29)

#1 @gziolo
5 months ago

Proposed solution


Extend wp_get_abilities() to accept an optional $args array parameter. When $args is provided, the function filters the registry and returns the matching subset. When called without arguments, behavior is unchanged and returns all abilities as WP_Ability[].

No new classes are introduced for this path. Filtering logic lives inside wp_get_abilities() (or a private helper it delegates to), following the established WordPress convention of array-based $args (get_posts, get_terms). Since wp_get_abilities() already returns WP_Ability[], changing its return type to a Collection object would break existing call sites, so the $args approach is the natural fit for the existing function.

That said, the Collection approach explored in PR #119 has real developer experience appeal, and I'd be open to it as a parallel entry point: e.g., a separate wp_get_abilities_collection() function that wraps the same registry. This would let developers who prefer the fluent style opt into it without affecting backward compatibility. However, establishing the foundational $args-based filtering first gives both paths a shared filtering primitive to build on, so I'd suggest starting here.

Phase 1: Category and namespace filtering


Aligns the PHP API with what the REST API already supports for categories, and adds the namespace filtering that WooCommerce and other consumers have been building ad-hoc.

// Filter by category (single or array, OR logic within).
$content_abilities = wp_get_abilities( array( 'category' => 'content' ) );
 
// Filter by namespace.
$woo_abilities = wp_get_abilities( array( 'namespace' => 'woocommerce' ) );
 
// Combine (AND logic between different arg types).
$woo_content = wp_get_abilities( array(
    'category'  => 'content',
    'namespace' => 'woocommerce',
) );
 
// Multiple values use OR logic within the same arg type.
$abilities = wp_get_abilities( array(
    'category' => array( 'content', 'settings' ),
) );


The REST API controller should be refactored to delegate to wp_get_abilities( $args ) internally, eliminating its own post-retrieval filtering. A namespace query parameter should be added to the REST endpoint to match.

Phase 2: Meta filtering


Enables internal refactoring of the MCP adapter, WebMCP adapter, and REST controller visibility checks — all of which currently do their own array_filter pass over meta properties.

// Abilities exposed over REST.
$rest_abilities = wp_get_abilities( array(
    'meta' => array( 'show_in_rest' => true ),
) );
 
// Abilities exposed over MCP.
$mcp_abilities = wp_get_abilities( array(
    'meta' => array(
        'show_in_rest' => true,
        'mcp'          => array( 'public' => true ),
    ),
) );


Meta filters use AND logic, so all specified conditions must match. Nested keys are supported for structured metadata like mcp.public. It should be added to the REST endpoint as well.

Phase 3: Caller-scoped callbacks


Two $args keys that give the caller control over per-item inclusion and final result shaping, without touching global state.

match_callback — receives each ability that survived the declarative filters. Returns true to include, false to exclude. Covers cases that category, namespace, and meta cannot express: OR conditions across meta fields, role-based visibility, protocol-specific gates. This was flagged as a need during review of the prior proposals.

// Per-item: only abilities the current user can execute.
$abilities = wp_get_abilities( array(
    'category'       => 'content',
    'match_callback' => function ( WP_Ability $ability ) {
        return current_user_can( $ability->get_meta()['capability'] ?? 'manage_options' );
    },
) );


result_callback — receives the full matched array after all per-item filtering is done. Returns the transformed array. Lets the caller sort, slice, or reshape the result in a single self-contained call.

// Result-level: sort and paginate without global filters.
$abilities = wp_get_abilities( array(
    'category'        => 'content',
    'result_callback' => function ( array $abilities ) {
        usort( $abilities, function ( WP_Ability $a, WP_Ability $b ) {
            return strcasecmp( $a->get_label(), $b->get_label() );
        } );
        return array_slice( $abilities, 0, 10 );
    },
) );


Phase 4: Ecosystem-scoped hooks


Today, plugin authors who need filtered abilities call wp_get_abilities() and apply their own logic after the fact. This works for the individual caller, but it means no other plugin can influence that filtering — there is no hook point between retrieval and consumption. A security plugin cannot enforce capability checks, the MCP adapter cannot gate visibility, and core cannot apply default scoping. Each consumer is an island.

By moving filtering inside wp_get_abilities(), the pipeline ensures that ecosystem hooks fire in a defined order, giving plugins a reliable place to participate. Each callback from Phase 3 has a corresponding filter that lets the ecosystem inject logic universally, regardless of what the caller passed.

wp_get_abilities_match — fires per-item, after match_callback. Any plugin can hook this to enforce inclusion rules globally. For example, the MCP adapter could enforce mcp.public visibility, or a security plugin could restrict abilities by role. The filter receives whether the ability matched so far ($match), the WP_Ability instance, and the full $args.

wp_get_abilities_result — fires once on the full array, after result_callback. Lets plugins shape the final output — sorting, pagination, reordering by priority. The filter receives the WP_Ability[] array and the full $args. Rather than baking orderby, order, limit, and offset into the $args signature, this hook lets those concerns be handled by plugins (e.g., the REST API controller applying its own pagination) without growing the core API surface.

Ecosystem hooks fire last at each level, so plugins always get the final say.

Pipeline summary


  1. Declarative filters (category, namespace, meta) — per-item
  2. match_callback — per-item, caller-scoped
  3. wp_get_abilities_match filter — per-item, ecosystem-scoped
  4. result_callback — on the full array, caller-scoped
  5. wp_get_abilities_result filter — on the full array, ecosystem-scoped


Steps 1–3 run inside a single loop — no extra iteration.

Design notes


  • AND between arg types, OR within multi-value args — matches WordPress convention.
  • Single pass — all conditions (category, namespace, meta, callback) are evaluated per-ability in one loop, avoiding multiple iterations over the registry.
  • REST parity — every $args key should correspond to a supported REST API query parameter where it makes sense.
  • Separation of concerns — the function handles selection (declarative args); callbacks handle caller-scoped logic (match_callback, result_callback), while filters handle ecosystem-scoped logic (wp_get_abilities_match, wp_get_abilities_result). Each layer has a clear owner without overlap.

Out of scope


  • Fluent/collection-style API — worth exploring as a parallel entry point (e.g., wp_get_abilities_collection()) once the foundational $args filtering is in place. The developer experience benefits are clear. The main reason not to start there is backward compatibility with the existing return type.
  • A universal public meta flag cascading into protocol-specific visibility (tracked separately as part of the WebMCP / MCP adapter discussions).


This ticket was mentioned in Slack in #core-ai by gziolo. View the logs.


5 months ago

#3 @gziolo
5 months ago

  • Owner set to gziolo
  • Status newassigned

#4 @gziolo
5 months ago

  • Keywords 2nd-opinion added

This ticket was mentioned in PR #11531 on WordPress/wordpress-develop by @sheldorofazeroth.


5 months ago
#5

  • Keywords has-patch added

Core Trac Ticket: https://core.trac.wordpress.org/ticket/64990

Problem
wp_get_abilities() accepts no arguments and always returns the full registry. Callers who need a subset — by category, namespace, meta properties, or any combination — must retrieve all abilities and filter manually. As the number of registered abilities grows across core, plugins, and themes, several consumers have independently built ad-hoc filtering to work around this gap:

The MCP Adapter checks meta.mcp.public and meta.show_in_rest with its own array_filter pass
WooCommerce (v10.3+) introduced a woocommerce_mcp_include_ability filter that performs namespace-prefix matching (str_starts_with( $ability_id, 'woocommerce/' ))
The REST API controller already supports ?category filtering, but implements it with its own post-retrieval array_filter rather than delegating to wp_get_abilities()
This duplication signals a missing primitive. Each consumer reimplements the same patterns with slightly different semantics and no shared hook point for ecosystem-level participation.

Root Cause
wp_get_abilities() was shipped in 6.9 without filtering support. The REST API controller added its own array_filter logic for show_in_rest and category filtering instead of a shared primitive, creating a mismatch between what the PHP and REST APIs support. There was no per-item or result-level hook for plugins to participate in ability filtering without monkey-patching call sites.

Solution
Extends wp_get_abilities() to accept an optional $args array, following the established WordPress convention of array-based arguments (get_posts(), get_terms()). When called without arguments, behaviour is completely unchanged — no BC break.

#6 @desrosj
5 months ago

  • Component AIAbilities API

Moving tickets related to the Abilities API to a new sub-component.

@gziolo commented on PR #11531:


4 months ago
#7

@Vedanshmini26, I missed your PR. This is a great start, and it covers the scope for extending wp_get_abilities() nicely. It would be very valuable to add unit tests that cover new functionality.

---

I see several unrelated changes like wp_lostpassword_form() or if ( current_user_can( $post_type_object->cap->read_private_posts ) ) { checks added in the codebase. Would you mind removing them?

@sheldorofazeroth commented on PR #11531:


4 months ago
#8

@gziolo I've removed the unrelated changes. Can you please check it now once?

@sheldorofazeroth commented on PR #11531:


4 months ago
#9

@gziolo I've resolved the PR comments and added the unit test coverage. Can you please look into it?

@gziolo commented on PR #11531:


4 months ago
#10

Yes, I will have a closer look later, but it should be good as is.

Looking at WordPress 7.0 Release Party Updated Schedule, we can commit these changes in mid-May because WordPress trunk is currently locked for bug fixes targeting WordPress 7.0.

#11 @gziolo
4 months ago

  • Keywords has-unit-tests added
  • Milestone Future Release7.1

@gziolo commented on PR #11531:


4 months ago
#12

I left my remaining feedback to address.

#13 @desrosj
4 months ago

  • Keywords abilities removed

Removing abilities and abilities-api custom keywords. This is now indicated by the Abilities API component.

#14 @desrosj
4 months ago

  • Keywords 2nd-opinion, has-patch, has-unit-tests → 2nd-opinion has-patch has-unit-tests
  • Summary Abilities API: Add filtering support to `wp_get_abilities()`Add filtering support to `wp_get_abilities()`

@gziolo commented on PR #11531:


4 months ago
#15

@Vedanshmini26, let me know if you need help to bring it to the finish line. The release cycle for WordPress 7.1 has just started. As soon as the feedback is addressed, I will commit it.

@sheldorofazeroth commented on PR #11531:


3 months ago
#16

@Vedanshmini26, let me know if you need help to bring it to the finish line. The release cycle for WordPress 7.1 has just started. As soon as the feedback is addressed, I will commit it.

@gziolo I've made all the changes as per the comments you added. Can you please check it and let me know if everything is looking good?

#17 @gziolo
3 months ago

  • Resolutionfixed
  • Status assignedclosed

In 62420:

Abilities API: Add filtering support to wp_get_abilities()

Extends wp_get_abilities() with an optional $args array, giving callers a shared primitive for filtering registered abilities by category, namespace, or meta. Two callback slots — item_include_callback (per ability) and result_callback (on the full matched array) — round out the caller-scoped pipeline.

Two new filters, wp_get_abilities_item_include and wp_get_abilities_result, expose ecosystem-scoped extension points so plugins can participate in ability resolution without monkey-patching call sites. This replaces the ad-hoc array_filter passes that consumers (the REST list controller, the MCP adapter, WooCommerce) had each implemented independently.

The REST list controller now delegates to the new primitive instead of running its own post-retrieval filtering, and gains a namespace query parameter alongside the existing category filter.

Called without arguments, wp_get_abilities() behaves exactly as before — no backward compatibility break.

Props sheldorofazeroth, gziolo.
Fixes #64990.

@gziolo commented on PR #11531:


3 months ago
#18

Impressive work @Vedanshmini26. I committed the changes as they cover most of the spec with the feedback applied as requested. I will list the remaining items on the ticket next.

#19 @gziolo
3 months ago

  • Keywords needs-dev-note added; 2nd-opinion removed
  • Resolution fixed
  • Status closedreopened

For the dev note purposes

The implementation follows the proposal's pipeline and semantics. A few specifics that should be reflected accurately in the dev note:

  • Arg and filter renamed. The per-item caller callback is item_include_callback (not match_callback), the per-item filter is wp_get_abilities_item_include (not wp_get_abilities_match), the boolean parameter is $include. Framing proposed as "should we include this item?" rather than "did this match?".
  • namespace accepts a single string only. Unlike category, which accepts string|string[] with OR logic within, namespace currently takes one string. Document the as-shipped shape, but note that this is expected to change — see the follow-up below.
  • REST endpoint exposes category and namespace, not meta. The PHP meta arg (AND logic, nested keys) is fully supported, but it is not surfaced as a REST query parameter. Describe the PHP meta arg without implying REST parity, until the decision changes later to extend params in the REST API.
  • New private helper. _wp_get_abilities_match_meta() recursively evaluates nested meta conditions (AND across keys, descending into sub-arrays). It's underscore-prefixed and not part of the public API — out of scope for the dev note, but worth knowing it exists when reading wp_get_abilities().

Follow-up tasks

  1. Align namespace shape with category — accept string|string[] with OR logic within. Alternatively, limit that to a single string aligning with how the current REST API contract is implemented. Best landed before the dev note publishes, so the public surface is consistent from day one.
  2. Expose meta filtering on the REST endpoint — decide whether to add a meta query parameter so the REST surface reaches parity with the PHP API, as the original Phase 2 suggested.

This ticket was mentioned in Slack in #core-ai by gziolo. View the logs.


3 months ago

#21 @sheldorofazeroth
3 months ago

@gziolo Do these follow-up tasks have new tickets? or its code needs to be linked with this ticket only.

#22 @gziolo
3 months ago

Since we are still in the early stages of the WP 7.1 cycle, it appears that the ticket remains open as follow-ups are primarily decision-driven. However, based on the agreed-upon path forward, we can open new tickets if the scope of the project expands.

#23 @sheldorofazeroth
3 months ago

@gziolo , got your point. I'll raise a new PR for the follow-up tasks and link it to this trac ticket. Please let me know if it works.

This ticket was mentioned in PR #12233 on WordPress/wordpress-develop by @gziolo.


3 months ago
#24

## Summary

This addresses the remaining feedback on filtering support for wp_get_abilities(). It makes the declarative filter arguments consistent and exposes meta filtering through the REST API.

## Details

Single-string category. The category argument now takes one slug, the same shape as namespace. Both do an exact match on a single value. A caller that needs to match several values can use item_include_callback. Starting with a single string keeps the public surface simple and predictable, and it leaves room to accept arrays later without a breaking change. The reverse (arrays now, single value later) could not be undone.

REST meta parameter. The abilities list endpoint (/wp-abilities/v1/abilities) now accepts a meta query parameter, next to category and namespace. Conditions combine with AND logic and may be nested. The schema declares the well-defined behavioral annotations (readonly, destructive, idempotent), so a query-string value such as true is coerced to a real boolean before matching. Open-ended meta keys still pass through as sent.

Visibility stays protected. The endpoint always forces meta[show_in_rest] => true. Caller meta is merged first, so this forced condition always wins. A caller cannot use meta to reveal abilities that are hidden from REST.

## Testing

New and updated unit tests cover:

  • category as a single string, and that a non-string value is ignored.
  • The REST meta parameter, including string-to-boolean coercion for annotations.
  • AND logic across several meta conditions.
  • The show_in_rest guard, so meta cannot widen visibility.
npm run test:php -- --group abilities-api

[!NOTE]
The tests/qunit/fixtures/wp-api-generated.js change is the auto-generated REST API client fixture, regenerated to add the new meta query parameter.

🤖 Generated with Claude Code

@jorgefilipecosta commented on PR #12233:


3 months ago
#25

Hi @gziolo the PR seems to be in a good shape, just left some comments for consideration.

@gziolo commented on PR #12233:


3 months ago
#26

@jorgefilipecosta Good catch, thank you. You are right that a custom meta key like mcp.public stays a string from the query string, so the strict match against the stored boolean fails.

I went with your second idea: let developers pass the schema for their own meta. The list controller now has a rest_abilities_collection_params filter, mirroring rest_comment_collection_params, rest_user_collection_params, and the other controllers. A plugin hooks it to declare the type of its custom key. REST then coerces the value before the meta filter runs, so the match works:

add_filter( 'rest_abilities_collection_params', function ( $params ) {
        $params['meta']['properties']['mcp'] = array(
                'type'       => 'object',
                'properties' => array(
                        'public' => array( 'type' => array( 'boolean', 'null' ) ),
                ),
        );
        return $params;
} );

This keeps open-ended meta filtering available without guessing types in core. The well-defined annotations stay coerced out of the box.

I added two tests using a made-up featured key: one for the default behavior (no declared type, no coercion) and one showing the filter make it work. One thing to note: the filter runs when routes are registered, so a plugin must add it on or before rest_api_init.

Pushed in f1c7ad44dc.

#27 @gziolo
3 months ago

  • Resolutionfixed
  • Status reopenedclosed

In 62548:

Abilities API: Refine filtering and expose meta over REST.

Follow-up to [62420]. The category argument now takes a single slug, matching namespace; callers needing multiple values can use item_include_callback. This keeps the public surface simple and leaves room to accept arrays later without a break.

The list endpoint (/wp-abilities/v1/abilities) gains a meta query parameter alongside category and namespace. Conditions combine with AND logic, may be nested, and known annotations (readonly, destructive, idempotent) are coerced to booleans before matching. The endpoint always forces meta[show_in_rest] => true, so meta cannot reveal hidden abilities.

Props gziolo, jorgefilipecosta, apermo.
Fixes #64990.

#29 @gziolo
10 days ago

In 63391:

Abilities API: Add missing @since 7.1.0 changelog entries

Document the 7.1 changes to existing docblocks that had no changelog line: the public meta argument, the wp_ability_validate_input and wp_ability_validate_output filters, exception handling in invoke_callback(), and the new schema property and collection parameters in the REST list controller.

Follow-up to [62238], [62398], [62420], [62548], [62729], [62737].

Props khokansardar.
See #65058, #64311, #64990, #65568.

Note: See TracTickets for help on using tickets.