Back to blog

WordPress 7.1's New public Flag for Abilities: One Line Instead of Five

WordPress 7.1 adds a single public flag to the Abilities API, replacing per-channel exposure settings for REST, MCP, and future clients.

WordPress 7.1's New public Flag for Abilities: One Line Instead of Five

If you have registered an ability since WordPress 6.9, you have written the same awkward block of metadata at least twice: one setting to expose it through the REST API, another nested one to expose it to the MCP Adapter, and a quiet worry that the next client type would need a third. WordPress 7.1 collapses that into a single public flag on the ability's meta array. The dev note announcing it went up on the Make WordPress Core blog on 4 August 2026, and the change landed in 7.1-RC1 under ticket #65568.

The saving is five lines of configuration, which is not much on its own. What matters more is that exposure becomes one decision instead of one decision per channel, and that the pattern is extensible, so a new client integration can honour the same flag without anyone patching WordPress core. This article covers what the flag does, exactly how it resolves against the older channel-specific settings, why "public" does not mean "unauthenticated", and what plugin authors should actually change right now.

TL;DR

  • WordPress 7.1 adds meta.public to wp_register_ability(). Setting it to true marks an ability as intended for external clients: the REST API, MCP adapters, AI agents, and whatever comes next.
  • It replaces having to set meta.show_in_rest and meta.mcp.public separately. Five lines of nested exposure config become one.
  • Channel-specific settings still win. Core resolves REST exposure as $show_in_rest = $meta['show_in_rest'] ?? $meta['public'] ?? false;, so you can be public everywhere but explicitly opt out of one channel.
  • The default is false. Abilities stay private unless you say otherwise, and nothing you already wrote breaks.
  • public is discoverability, not authorisation. The dev note is blunt about it: exposure flags are not a security boundary, and permission_callback still does all the real work.
  • Do not delete meta.mcp.public yet. The WordPress MCP Adapter picks up the unified flag from its next release, not today. And if your plugin supports 6.9 or 7.0, keep show_in_rest, because older core has no resolution logic for public.

What the flag actually changes

Here is the whole change, in the shape most plugin code takes.

Before, an ability meant for both the REST API and the default MCP server needed each channel configured on its own terms:

php
'meta' => array(
	'show_in_rest' => true,
	'mcp'          => array(
		'public' => true,
	),
),

After, in WordPress 7.1:

php
'meta' => array(
	'public' => true,
),

In a full registration, the dev note's example looks like this:

php
wp_register_ability(
	'my-plugin/export-users',
	array(
		'label'               => __( 'Export users', 'my-plugin' ),
		'description'         => __( 'Exports user data as CSV.', 'my-plugin' ),
		'category'            => 'data-export',
		'execute_callback'    => 'my_plugin_export_users',
		'permission_callback' => function (): bool {
			return current_user_can( 'export' );
		},
		'meta'                => array(
			'public' => true,
		),
	)
);

Read that registration again with the security question in mind. The thing that decides whether a request succeeds is current_user_can( 'export' ). The public flag decides only whether the ability shows up in a client's list of things it could try.

A short refresher on abilities

The Abilities API arrived in WordPress 6.9 as a central registry of the discrete things a site can do. An ability is a named unit of functionality following the namespace/ability-name pattern, with a declared input schema, a declared output schema, a callback that performs the work, and a callback that decides who is allowed to ask.

The point of the registry is that it is machine-readable. A REST client, an admin-side JavaScript app, or an AI agent connected through the Model Context Protocol can enumerate what a site can do and see the exact shape of the arguments each action expects, without anyone hand-writing an integration for that specific plugin.

Core's PHP reference lists label, description, category, execute_callback, and permission_callback among the required arguments to wp_register_ability(), with input_schema, output_schema, meta, and ability_class optional. Every ability belongs to exactly one category, registered with wp_register_ability_category().

WordPress 7.0 added the client-side half. The @wordpress/abilities package provides a framework-agnostic store, and @wordpress/core-abilities fetches everything registered on the server through the /wp-abilities/v1/ REST routes and populates that store. Core enqueues it on admin pages, which means server-registered abilities show up in the browser with no extra client-side wiring. That detail matters for the topic at hand: the client-side store is downstream of REST exposure, so what you allow through REST is what the admin-side JavaScript can see.

Why exposure needed unifying

The Abilities API grew channels faster than it grew a way to describe them.

REST exposure was controlled by meta.show_in_rest, defaulting to false. An ability with it set is listed at /wp-json/wp-abilities/v1/abilities and can be run at /wp-json/wp-abilities/v1/{namespace}/{ability}/run. Without it, the ability is hidden from listings, and hitting its endpoint directly returns a rest_ability_not_found error.

MCP exposure was controlled separately. The WordPress MCP Adapter, which turns registered abilities into MCP tools, requires meta.mcp.public to be true before an ability is offered by its default server. Custom MCP servers built in a plugin sidestep the flag entirely by naming the abilities they expose.

So a plugin author who wanted an ability available to both wrote two settings that meant the same thing in different vocabularies. Worse, they could drift. Add a new REST-facing feature, forget the MCP side, and the ability is discoverable in one place and invisible in the other, with nothing in the code to indicate whether that was a decision or an oversight.

public fixes the vocabulary problem. It says what the developer means once: this ability is intended for external clients. Each channel then decides what that implies for it.

How resolution works

The precedence chain is short and worth memorising: an explicit channel-specific value wins, then public, then the channel's own default.

For REST, core resolves it exactly like this:

php
$show_in_rest = $meta['show_in_rest'] ?? $meta['public'] ?? false;

Two details in that line do a lot of work. Explicit false values are preserved rather than being treated as "not set", and null counts as unset. That combination is what makes selective opt-out possible:

php
'meta' => array(
	'public'       => true,
	'show_in_rest' => false,
),

That ability is public in principle, available to MCP clients once the adapter supports the flag, and deliberately absent from the REST API. Perhaps it is expensive to run over HTTP, or it only makes sense with an agent's conversational context around it.

The combinations, in table form:

public show_in_rest REST result Notes
unset unset Hidden The default. Abilities are private until you say otherwise.
true unset Exposed The new one-line form.
true false Hidden Deliberate per-channel opt-out.
false true Exposed Channel-specific value still wins.
unset true Exposed Existing code, unchanged behaviour.

That last row is the compatibility guarantee. Every ability already registered with show_in_rest => true keeps working exactly as before, because the channel-specific value is checked first and never reaches the public fallback. Core migrated three of its own abilities to the new flag in the process: core/get-site-info, core/get-user-info, and core/get-environment-info.

Exposure is not authorisation

This is the part to internalise before you start setting flags, and the dev note does not hedge about it. The public flag controls discoverability and client exposure. It does not make an ability executable without authorisation, and it does not replace the ability's permission_callback. An ability marked public may be visible through a client while still requiring authentication and specific WordPress capabilities to run. Developers should not treat public, show_in_rest, or any other exposure flag as a security boundary.

In practice, three layers stack up:

  1. Transport authentication. Every endpoint under /wp-abilities/v1/ requires an authenticated user. An anonymous request does not get a list of your abilities, public flag or not.
  2. Discoverability. public and the channel flags decide whether an authenticated client can see the ability in a listing and address it by name.
  3. Authorisation. The ability's permission_callback decides whether this particular user, with these particular capabilities, may execute it. This is the only layer that stops the request.

The failure mode to watch for is the same one that produces REST API privilege escalation generally: a permission_callback that checks whether someone is logged in rather than whether they are allowed. is_user_logged_in() inside an ability that exports user data is a subscriber-to-data-breach pipeline, and marking it non-public only means an attacker has to know the ability's name, which is usually in your public repository.

The MCP Adapter's own guidance points the same direction: use the minimum capability the ability genuinely needs, give MCP access a dedicated user rather than an administrator account, and prefer read-only abilities when the endpoint is broadly reachable. Since MCP clients authenticate with Application Passwords in most setups, and an Application Password carries every capability of the account that issued it, the account you connect matters at least as much as the flag you set.

What plugin authors should change now

Three practical rules, in order of how much they will bite you.

Do not remove meta.mcp.public yet. The dev note states that the MCP Adapter will respect the unified flag starting with its next release. Until that ships and you have confirmed the version your users are running, an ability that carries only public => true may simply not appear on the adapter's default server. Set both for now, drop the channel-specific one later.

Mind your minimum supported WordPress version. The resolution logic ships in 7.1. Core releases before that have no concept of a public meta key, so on 6.9 or 7.0 it is unrecognised metadata that nothing reads. If your plugin's Requires at least header says 6.9, keep show_in_rest => true alongside public => true, and test on the oldest version you actually claim to support rather than assuming the fallback is there.

Migrate when an ability is genuinely multi-client. The dev note's guidance is to adopt public when an ability is generally intended for use by multiple client types, and to keep channel-specific settings for intentionally limited or override cases. If an ability exists purely to feed one admin screen through REST, show_in_rest => true still says what you mean more precisely than public => true does.

A reasonable migration pass looks like this:

php
// Multi-client ability: was five lines of exposure config, now one
// (keep show_in_rest while you still support WordPress 7.0 or earlier).
'meta' => array(
	'public'       => true,
	'show_in_rest' => true,
	'mcp'          => array(
		'public' => true,
	),
),

// REST-only by design: leave it alone.
'meta' => array(
	'show_in_rest' => true,
),

// Internal ability: no exposure meta at all. The default is private.

Once your minimum version is 7.1 and the MCP Adapter release has landed, the first block genuinely does collapse to 'meta' => array( 'public' => true ).

Teaching your own client to honour the flag

The extensibility argument for public is the more interesting half of the change. Any integration can derive its own exposure setting from the unified flag through the wp_register_ability_args filter, without core needing to know that integration exists. The dev note's example:

php
add_filter(
	'wp_register_ability_args',
	function ( array $args, string $name ): array {
		if (
			! isset( $args['meta']['my_client']['public'] )
			&& isset( $args['meta']['public'] )
		) {
			$args['meta']['my_client']['public'] =
				(bool) $args['meta']['public'];
		}

		return $args;
	},
	10,
	2
);

The ! isset() guard is the whole pattern, and it is easy to get wrong. It reproduces core's precedence rule: only fill in your channel's value when the developer has not already set one. Drop that condition and your filter starts overwriting deliberate per-channel decisions, which is exactly the class of bug the unified flag was meant to remove.

If you maintain a client integration, this is also the moment to decide what "public" means for you. REST reads it as "list it and allow it to be run over HTTP". A background indexing service might reasonably read it as "safe to enumerate, but only execute abilities annotated read-only". The flag communicates developer intent; the interpretation is yours.

What else changed for abilities in 7.1

The public flag arrived alongside a set of smaller Abilities API improvements, documented in a separate dev note. The ones worth knowing about:

  • Supplementary validation filters. wp_ability_validate_input and wp_ability_validate_output let you enforce rules that JSON Schema cannot express. Each receives the current validation result, the value, and the ability name, and returns true or a WP_Error describing the failure. Ticket #64311.
  • An invocation hook for observability. wp_ability_invoked fires at the start of WP_Ability::execute(), as do_action( 'wp_ability_invoked', $this->name, $input, $this ). It is intended for auditing, telemetry, tracing, and invocation accounting. Note carefully that it fires before validation and permission checks, so it records attempts rather than successes. That makes it useful for spotting an agent probing abilities it cannot run, and useless as a security gate. The existing wp_before_execute_ability and wp_after_execute_ability actions now also receive the WP_Ability instance as a final argument. Ticket #65248.
  • A richer core/get-user-info. It now returns first_name, last_name, nickname, description, and user_url, and accepts an optional fields input property so a client can ask for a subset. The roles property is normalised with array_values() so it encodes as a JSON array rather than an object. Tickets #65234 and #65355.
  • Consistent core schemas. core/get-site-info, core/get-user-info, and core/get-environment-info now follow the same conventions, with a translatable title and description on every output property, and core/get-environment-info gains the same fields input parameter.
  • Type coercion on REST reads. GET and DELETE requests now coerce query-string input to the types declared in input_schema before execution, so "10" becomes the integer 10 and "true" becomes the boolean true.

Together these point at the same trajectory as the public flag: the Abilities API is being shaped into something an autonomous client can be pointed at safely, with observability and validation hooks around the edges.

Timing and how to test this

WordPress 7.1 Release Candidate 1 is dated 5 August 2026, with RC2 scheduled for 12 August and general release on 19 August 2026. That means the behaviour described here is final in the sense that RC is past feature freeze, but it is still pre-release. Check the changeset and the ticket before shipping anything that depends on the exact resolution order, and re-verify once 7.1 is out, because release-blocking changes do occasionally land between RC and final.

Test it somewhere disposable rather than on production. A quick loop:

terminal
# Confirm the version you are actually testing against
wp core version

# List abilities visible to an authenticated user
curl -s -u 'wpuser:xxxx xxxx xxxx xxxx xxxx xxxx' \
  'https://<example.com>/wp-json/wp-abilities/v1/abilities' \
  | jq -r '.[] | .name'

# Run one and inspect the response
curl -s -u 'wpuser:xxxx xxxx xxxx xxxx xxxx xxxx' \
  'https://<example.com>/wp-json/wp-abilities/v1/my-plugin/export-users/run' | jq

Then repeat the listing as a low-privilege user. An ability that shows up in the list but returns a 403 on /run is working correctly. An ability that executes for a user who should not be able to run it is a permission_callback bug, and no combination of exposure flags will fix it.

If you need a throwaway environment for that, MagicWP gives every site one-click staging and cloning plus SSH and WP-CLI access, which is enough to install a release candidate, run the checks above, and throw the whole thing away afterwards.

Frequently Asked Questions

Does setting public => true make an ability available without logging in?

No. Every endpoint under the /wp-abilities/v1/ REST namespace requires an authenticated user, and each ability's permission_callback runs on top of that. The flag controls whether an authenticated client can discover and address the ability, not whether anyone can execute it. The dev note states directly that public is not a security boundary and does not replace permission_callback.

Will my existing abilities break in WordPress 7.1?

No. Channel-specific settings take precedence, and the resolution is a null-coalescing fallback: $show_in_rest = $meta['show_in_rest'] ?? $meta['public'] ?? false;. An ability registered with show_in_rest => true never reaches the public fallback and behaves exactly as it did before. The default remains false, so abilities with no exposure meta stay private.

Can I be public generally but hidden from the REST API?

Yes, and this is the main reason the precedence rule exists. Set 'public' => true together with 'show_in_rest' => false. Explicit false values are preserved rather than treated as unset, so the ability stays out of REST listings while remaining marked as intended for other clients. The same pattern works in reverse if you want REST exposure without a general public declaration.

Should I remove meta.mcp.public from my plugin now?

Not yet. The WordPress MCP Adapter respects the unified flag starting with its next release, not the version most sites are running today. Keep both settings until you are confident about the adapter version in the field, then drop the channel-specific one. There is no harm in having both, since the explicit MCP value simply wins over the fallback.

What happens to public on WordPress 6.9 or 7.0?

The resolution logic ships in 7.1, so earlier versions have nothing that reads a meta.public key. It becomes unrecognised metadata. If your plugin still supports 6.9 or 7.0, keep show_in_rest => true next to public => true so exposure works on both, and verify against the oldest version listed in your plugin's Requires at least header rather than assuming a fallback exists.

How do I expose the flag to a client integration of my own?

Hook wp_register_ability_args and copy the unified value into your channel's key, but only when the developer has not already set one. The ! isset( $args['meta']['my_channel']['public'] ) guard is what preserves the precedence rule. Without it, your filter silently overrides deliberate per-channel decisions. Core does not need to know your integration exists for this to work, which is the extensibility argument for the flag.

Does public affect the client-side Abilities API in the block editor?

Indirectly. The @wordpress/core-abilities package fetches abilities from the /wp-abilities/v1/ REST routes and registers them in the client store, and core enqueues it on admin pages. Since REST exposure is what the client reads, an ability that resolves to show_in_rest => true (whether directly or through public) becomes visible to admin-side JavaScript. Bear that in mind before marking something public purely for an agent's benefit.

Conclusion

The public flag is a small change with a good shape. It replaces five lines of per-channel exposure configuration with one, it keeps every existing registration working by letting channel-specific settings win, and it gives future client integrations a documented way to read developer intent without anyone touching WordPress core. If you maintain a plugin that registers abilities, the migration is mechanical: add public => true where an ability is genuinely meant for multiple clients, keep show_in_rest while you support 7.0 or earlier, and leave meta.mcp.public in place until the MCP Adapter's next release lands.

The one thing not to take from it is comfort. Marking an ability public says something about discoverability and nothing about safety, and the sentence in the dev note about exposure not being authorisation is there because someone will read the flag the other way. Every ability you expose still needs a permission_callback that checks a real capability, and every credential you hand an agent still carries the full weight of the account behind it.

If you want somewhere to try WordPress 7.1 RC before it reaches your production sites, MagicWP's managed WordPress hosting includes one-click staging, SSH and WP-CLI, and daily backups with one-click restore, so testing a release candidate against your own plugin code is a five-minute job rather than an afternoon.

A
Alex
MagicWP
Writing about WordPress, performance, and the infrastructure that makes sites fast.

Get the best of MagicWP in your inbox.

Monthly engineering notes, product updates, and WordPress performance tips. No spam, unsubscribe anytime.

Join 12,000+ builders. We send one email a month.