#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.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)
Change History (29)
This ticket was mentioned in Slack in #core-ai by gziolo. View the logs.
5 months ago
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
@
5 months ago
- Component AI → Abilities 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.
@gziolo commented on PR #11531:
4 months ago
#12
I left my remaining feedback to address.
#13
@
4 months ago
- Keywords abilities removed
Removing abilities and abilities-api custom keywords. This is now indicated by the Abilities API component.
#14
@
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?
@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
@
3 months ago
- Keywords needs-dev-note added; 2nd-opinion removed
- Resolution fixed
- Status closed → reopened
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(notmatch_callback), the per-item filter iswp_get_abilities_item_include(notwp_get_abilities_match), the boolean parameter is$include. Framing proposed as "should we include this item?" rather than "did this match?".
namespaceaccepts a single string only. Unlikecategory, which acceptsstring|string[]with OR logic within,namespacecurrently takes one string. Document the as-shipped shape, but note that this is expected to change — see the follow-up below.
- REST endpoint exposes
categoryandnamespace, notmeta. The PHPmetaarg (AND logic, nested keys) is fully supported, but it is not surfaced as a REST query parameter. Describe the PHPmetaarg 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 readingwp_get_abilities().
Follow-up tasks
- Align
namespaceshape withcategory— acceptstring|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. - Expose
metafiltering on the REST endpoint — decide whether to add ametaquery 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
@
3 months ago
@gziolo Do these follow-up tasks have new tickets? or its code needs to be linked with this ticket only.
#22
@
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
@
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:
categoryas a single string, and that a non-string value is ignored.- The REST
metaparameter, including string-to-boolean coercion for annotations. - AND logic across several meta conditions.
- The
show_in_restguard, so meta cannot widen visibility.
npm run test:php -- --group abilities-api
[!NOTE]
Thetests/qunit/fixtures/wp-api-generated.jschange is the auto-generated REST API client fixture, regenerated to add the newmetaquery 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.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
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.
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.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.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.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).