Add: WordPress Core Post Management Abilities
Part of: https://github.com/WordPress/ai/issues/40 cc: @Jameswlepage Equivalent core PR of: https://github.com/WordPress/gutenberg/pull/74234 (in Gutenberg). Inspired by the work on https://github.com/galatanovidiu/mcp-adapter-implementation-example/tree/experiment/layerd-mcp-tools/includes/Abilities by @galatanovidiu.
Ticket: https://core.trac.wordpress.org/ticket/64455
Core abilities organization
This PR also proposes a logic for how core abilities are organized. In abilities.php, we have two functions: wp_register_core_abilities and wp_register_ability_category (or in the case of Gutenberg, _gutenberg_register_core_abilities and _gutenberg_register_core_ability_categories). These functions then call ability registration functions that are inside the abilities folder. If the ability is simple, it can be registered in just a single internal function, e.g., _wp_register_site_info_ability; for complex abilities, we can register them in a class (like this post management one).
The abilities can be in both Gutenberg and core. Having them in Gutenberg allows us to use them in the workflows functionality that is being worked on by @senadir and allows us to get some testing before core is released.
Gutenberg unregisters the equivalent core ones, so Gutenberg is the source of truth. The same ability can exist in Gutenberg and core, but Gutenberg takes precedence so we can test changes in Gutenberg before releasing in core (similar to what happens with blocks and other WordPress artifacts).
Core Post management abilities
This PR adds core post management abilities for the WordPress abilities API: core/create-post, core/get-post, core/find-posts, and core/update-post.
It supports nested query support for meta_query, tax_query, and date_query matching WordPress's native WP_Query structure with AND/OR relations. This allows very complex query operations which the REST API does not allow and may be useful for agents to find information.
It uses the permission callback mechanism and tries to correctly check permissions for all cases. The basic permission checking logic first checks the basic and common use case (user can edit, create, or see a post), then we go into specifics in separate functions that are reused for checking status changes (e.g., publish), author changes, and taxonomy assignment permissions.
The class WP_Posts_Abilities_Gutenberg is organized into 6 main areas:
-
Ability Registration: The main part that does the
wp_register_abilitycalls. - Initialization: Initializes shared data between the abilities, e.g., schemas.
- Output Formatting: Formats the post object (and taxonomies) for output shared between all abilities.
- Permission Checking: Permission checking logic; some parts are also shared between abilities.
-
Query Processing: Utilities to process queries and map the ability input format to the
WP_Queryformat. - Data Processing: Utilities to process input data, sanitize it, map to other formats, etc.
Missing
The idea of this PR is mainly to get feedback if this is the right direction for the post management abilities. There are some things that are missing:
- Automated testing: Deeply covering all the abilities. If we agree with this approach, I will add the tests to this PR.
-
Partial Edits: Not all edits are possible, e.g., it is not possible to mark a post as sticky. We need to decide if marking a post as sticky should be done on the update/create post abilities, following the REST API approach, or if we should have a specific ability for that, following the
wp_corePHP API approach where we have thewp_update_postfunction and alsostick_post/unstick_postfunctions. We can decide on what to do regarding the missing edits as follow-ups, as the PR is already too big. - Pagination: Pagination was not yet implemented; we need to have a general solution for pagination at the abilities API level.
Test plan
- Open
/wp-admin/edit.php?post_type=post. - Open the browser console the following examples.
const abilitiesAPI = (await import( '@wordpress/abilities' ) );
- [ ] Verify
core/create-postcreates posts with various fields (title, content, status, meta, taxonomies):
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/create-post/run', method: 'POST', data: { input: {post_type: 'post', title: 'Hello World', content: 'My awesome content', 'status': 'publish', meta: {a:23, c:1} } } });
- [ ] Verify
core/get-postretrieves posts by ID with optional taxonomy/meta inclusion:
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/get-post/run', method: 'POST', data: { input: {id: 334, include_meta: true, include_taxonomies: true} } })
- [ ] Verify
core/find-postsqueries work with nestedmeta_query,tax_query, anddate_query. -
Create posts with the following structure where the title
a-23|c-1represents a post with meta key "a" value of 23 and meta key "c" value of 1. Also adds the correct tags.
- Execute complex find post queries like:
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/find-posts/run', method: 'POST', data: { input: {
post_type: 'post',
include_meta: true,
meta_query: {
queries: [
{
key: 'footnotes',
compare: 'EXISTS',
},
]
}
} } });
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/find-posts/run', method: 'POST', data: { input: {
post_type: 'post',
include_meta: true,
meta_query: {
relation: 'AND',
queries: [
{
key: 'a',
compare: '=',
value: '23'
},
{
relation: 'OR',
queries: [
{
key: 'b',
compare: '=',
value: '1'
}, {
key: 'c',
compare: '=',
value: '1'
} ] }
]
}
} } });
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/find-posts/run', method: 'POST', data: { input: {
post_type: 'post',
include_meta: true,
tax_query: {
relation: 'AND',
queries: [
{
taxonomy: 'post_tag',
field: 'slug',
terms: ['t-23']
},
{
relation: 'OR',
queries: [
{
taxonomy: 'post_tag',
field: 'slug',
terms: ['c-1']
},
{
taxonomy: 'post_tag',
field: 'slug',
terms: ['b-1']
}
]
}
]
}
} } });
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/find-posts/run', method: 'POST', data: { input: {
post_type: 'post',
date_query: {
relation: 'AND',
queries: [
{ year: 2025 },
{
relation: 'OR',
queries: [
{ day: 26 },
{ month: 11 }
]
}
]
}
} } });
- [ ] Verify
core/update-postmodifies existing posts correctly:
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/update-post/run', method: 'POST', data: { input: {
id: 327,
content: 'Hello'
} } });
- [ ] Test permission checks prevent unauthorized access, e.g., users without publish permissions trying to change status to published, etc.