Opened 6 days ago
Last modified 6 days ago
#64990 assigned enhancement
Abilities API: Add filtering support to `wp_get_abilities()`
| Reported by: |
|
Owned by: |
|
|---|---|---|---|
| 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.publicandmeta.show_in_restto determine which abilities to expose, using its ownarray_filterpass. - WooCommerce (v10.3+) introduced a
woocommerce_mcp_include_abilityfilter that performs namespace-prefix matching (str_starts_with( $ability_id, 'woocommerce/' )) to scope its custom MCP server. - WebMCP adapter experiment (WordPress/ai#224) hardcoded
isAbilityPublicForAgentsto returntruebecause core abilities lack protocol-specific metadata, with a comment noting this needs a properpublicflag that cascades intoshow_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 #115 —
WP_Abilities_Queryclass modeled afterWP_Query, with array-based$args(category, namespace, search, meta, orderby, order, limit, offset). Reviewed by @jason_the_adams, @justlevine, @jorgefilipecosta, @swissspidy. - PR #119 —
WP_Abilities_Collectionclass 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:
WP_*_Queryimplies 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 essentiallyarray_filterover a PHP array.- Collections introduce a new paradigm (@jorgefilipecosta): WordPress has no
*_Collectionpattern anywhere. Blocks, patterns, and other registries don't use it. Introducing it for abilities alone raises consistency questions across the project. - Return type BC break (@gziolo):
wp_get_abilities()returnsWP_Ability[]today. Changing it to return a Collection object would break everyarray_*call site and type expectation downstream. - 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. - 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
?categoryfiltering, but this is implemented in the controller rather than the underlying PHP function, creating a mismatch between PHP and REST capabilities. - There is no
namespacefiltering at any layer. - There is no
metafiltering at any layer — consumers who needshow_in_rest === trueormcp.public === trueabilities must filter manually. - There is no extensibility hook for custom filtering logic (e.g., role-based visibility, protocol-specific gates).
Related
- WordPress/abilities-api#38 — Original tracking issue (archived repo)
- WordPress/abilities-api#115 —
WP_Abilities_Queryapproach (archived repo) - WordPress/abilities-api#119 —
WP_Abilities_Collectionapproach (archived repo) - WordPress/abilities-api#85 —
wp_query_abilitiesfunction approach (archived repo) - WordPress/ai#224 — WebMCP adapter experiment (visibility workaround)
Proposed solution
Extend
wp_get_abilities()to accept an optional$argsarray parameter. When$argsis provided, the function filters the registry and returns the matching subset. When called without arguments, behavior is unchanged and returns all abilities asWP_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). Sincewp_get_abilities()already returnsWP_Ability[], changing its return type to a Collection object would break existing call sites, so the$argsapproach 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. Anamespacequery 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_filterpass 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
$argskeys 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. Returnstrueto include,falseto exclude. Covers cases thatcategory,namespace, andmetacannot 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, aftermatch_callback. Any plugin can hook this to enforce inclusion rules globally. For example, the MCP adapter could enforcemcp.publicvisibility, or a security plugin could restrict abilities by role. The filter receives whether the ability matched so far ($match), theWP_Abilityinstance, and the full$args.wp_get_abilities_result— fires once on the full array, afterresult_callback. Lets plugins shape the final output — sorting, pagination, reordering by priority. The filter receives theWP_Ability[]array and the full$args. Rather than bakingorderby,order,limit, andoffsetinto the$argssignature, 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
category,namespace,meta) — per-itemmatch_callback— per-item, caller-scopedwp_get_abilities_matchfilter — per-item, ecosystem-scopedresult_callback— on the full array, caller-scopedwp_get_abilities_resultfilter — on the full array, ecosystem-scopedSteps 1–3 run inside a single loop — no extra iteration.
Design notes
$argskey should correspond to a supported REST API query parameter where it makes sense.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
wp_get_abilities_collection()) once the foundational$argsfiltering is in place. The developer experience benefits are clear. The main reason not to start there is backward compatibility with the existing return type.publicmeta flag cascading into protocol-specific visibility (tracked separately as part of the WebMCP / MCP adapter discussions).