close

Make WordPress Core

Opened 6 days ago

Last modified 6 days ago

#64990 assigned enhancement

Abilities API: Add filtering support to `wp_get_abilities()`

Reported by: gziolo's profile gziolo Owned by: gziolo's profile gziolo
Milestone: Future Release Priority: normal
Severity: normal Version: 6.9
Component: AI Keywords: abilities 2nd-opinion
Focuses: Cc:

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 (4)

#1 Image @gziolo
6 days 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).


Image

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


6 days ago

#3 Image @gziolo
6 days ago

  • Owner set to gziolo
  • Status changed from new to assigned

#4 Image @gziolo
6 days ago

  • Keywords 2nd-opinion added
Note: See TracTickets for help on using tickets.