The last lesson established that an agent’s access is whatever its underlying WordPress
account can do, and that reaching for a built-in role like Editor tends to over-grant.
This lesson builds the fix: a dedicated role, or a dedicated set of custom
capabilities, created specifically for one agent connection, then checked from
permission_callback the same way any other capability would be. This is genuine
role-based access control, it’s just built on WordPress’s own role system rather than
some separate adapter-native mechanism, because no such separate mechanism exists.
Lesson 3 (permission inheritance), and admin access to create new users and roles on your development site.
Step 1: Register a dedicated role
add_role() creates a new named role with exactly the capabilities you list, nothing
inherited from Editor or any other built-in role by default:
// File: my-plugin.php
add_action( 'init', function () {
if ( ! get_role( 'ai_content_agent' ) ) {
add_role(
'ai_content_agent',
__( 'AI Content Agent', 'my-plugin' ),
array(
'read' => true,
'edit_posts' => true,
'upload_files' => true,
)
);
}
} );
This role can read content, edit posts, and upload files, and nothing else. It cannot publish, cannot delete, cannot manage users or options, cannot install plugins. Compare that to Editor, which bundles roughly twenty capabilities this hypothetical content agent will never use.
add_role() runs on every request if left unguarded, which is harmless but wasteful.
The get_role() check above is the standard pattern, and matters more once you have
several agent-specific roles across a few plugins.
Step 2: Add a custom capability for anything that does not map cleanly
Sometimes an agent’s intended access doesn’t correspond to any existing WordPress capability. Rather than overloading an existing one (which affects humans with that capability too), define your own:
// File: my-plugin.php
add_action( 'admin_init', function () {
$role = get_role( 'ai_content_agent' );
if ( $role && ! $role->has_cap( 'my_plugin_summarize_comments' ) ) {
$role->add_cap( 'my_plugin_summarize_comments' );
}
} );
Custom capabilities are just strings. WordPress does not validate them against a fixed
list, current_user_can( 'my_plugin_summarize_comments' ) works exactly like checking
any built-in capability, as long as some role or user actually has it granted.
Step 3: Provision one account per agent connection
Create a real WordPress user for each distinct agent workflow, assigned the role you just built, and generate its Application Password from that account, never from an admin login:
wp-env run cli wp user create ai-content-agent ai-content-agent@example.test \
--role=ai_content_agent --user_pass="$(openssl rand -base64 24)"
wp-env run cli wp user application-password create ai-content-agent \
"Claude Desktop - Production - 2026-07"
Step 4: Check the custom role or capability from permission_callback
Once the role and account exist, your ability’s permission_callback checks it exactly
the way it would check any built-in capability:
// File: my-plugin.php
add_action( 'wp_abilities_api_init', function () {
wp_register_ability(
'my-plugin/summarize-recent-comments',
array(
'label' => __( 'Summarize recent comments', 'my-plugin' ),
'description' => __( 'Returns a summary of comments from the last 24 hours.', 'my-plugin' ),
'permission_callback' => function () {
return current_user_can( 'my_plugin_summarize_comments' );
},
'execute_callback' => function () {
// summarization logic
},
'meta' => array(
'annotations' => array( 'readonly' => true ),
'mcp' => array( 'public' => true ),
),
)
);
} );
Anyone, human or agent, without that specific capability gets denied. Anyone with it, including your dedicated agent account, gets through. There is nothing MCP-specific about this line of code, which is exactly the point: it’s the same permission infrastructure as the rest of WordPress, applied deliberately.
Test it: confirm the role is actually scoped correctly
wp-env run cli wp eval '
$user = get_user_by( "login", "ai-content-agent" );
var_dump( user_can( $user, "edit_posts" ) ); // expect true
var_dump( user_can( $user, "delete_users" ) ); // expect false
var_dump( user_can( $user, "my_plugin_summarize_comments" ) ); // expect false, until Step 2 is applied
'
Confirm the agent account can do exactly what its role grants, and nothing else.
Roles and custom capabilities do not clean themselves up. If an agent workflow is
retired, remove its dedicated account, its role (remove_role()), and any custom
capabilities added to it. A stale, forgotten agent role with live credentials still
attached is exactly the kind of thing an audit should catch, and exactly the kind of
thing that is easy to forget once the project that created it has moved on.
Recap
Real role-based access control for AI agents in WordPress means registering a dedicated,
minimally-scoped role with add_role(), optionally adding custom capabilities for
access patterns that do not map onto existing ones, provisioning a distinct account per
agent workflow, and checking all of it through the same current_user_can() pattern
every other ability uses. The next lesson goes one level deeper: permission checks that
depend on more than just the role, like object ownership and content status.
Resources & further reading
- add_role() function reference, developer.wordpress.org
- Roles and Capabilities, WordPress developer reference, developer.wordpress.org
- MCP Adapter repository, GitHub