Every other lesson in this course has been about one direction: your own WordPress code
calling out to an AI provider. Course 1 also covered the opposite direction: an
external AI agent calling into WordPress through an ability, exposed via the MCP
Adapter. Those two directions aren’t mutually exclusive, and the most useful pattern in
this whole course is where they meet: an ability’s execute_callback that, when
triggered by an external agent over MCP, itself calls out to an LLM using the PHP AI
Client SDK to do real work before returning a result. This lesson builds exactly that,
end to end.
Course 1’s Abilities API lesson (wp_register_ability(), permission_callback,
execute_callback, meta.mcp.public), and Lessons 1 through 7 of this course. If
you’ve also taken Courses 2 through 4, the MCP server setup steps there apply directly
here, but they’re not required to follow this lesson.
Step 1: Recognize where the two directions meet
An ability’s execute_callback is just a PHP callback, whatever a normal WordPress
function can do, an ability can do. Nothing about the Abilities API restricts what runs
inside it to reading or writing WordPress data directly. Calling wp_ai_client_prompt()
from inside execute_callback is no different, architecturally, from calling
get_posts() from inside it, it’s simply what the callback does when it runs:
External MCP agent --(calls ability over MCP)--> execute_callback
|
v
wp_ai_client_prompt()->generate_text()
|
v
Configured provider (Claude/GPT/Gemini)
The external agent triggering the ability doesn’t need its own AI provider
credentials, doesn’t even need to know an LLM call happens inside the ability at all.
As far as that agent is concerned, it called an ability named
my-plugin/summarize-post and got a summary back. What happened inside is your site’s
own concern.
Step 2: Register the ability
This follows the exact shape Course 1 covered, registered on wp_abilities_api_init,
with a permission_callback gating who can trigger it and an execute_callback doing
the actual work:
// File: my-plugin/class-summarize-ability.php
add_action( 'wp_abilities_api_init', function () {
wp_register_ability(
'my-plugin/summarize-post',
array(
'label' => __( 'Summarize post', 'my-plugin' ),
'description' => __( 'Generates a short summary of a published post by its ID.', 'my-plugin' ),
'category' => 'content',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'post_id' => array( 'type' => 'integer' ),
),
'required' => array( 'post_id' ),
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'summary' => array( 'type' => 'string' ),
),
),
'permission_callback' => function ( $input ) {
return current_user_can( 'edit_post', $input['post_id'] );
},
'execute_callback' => 'my_plugin_summarize_post_ability',
'meta' => array(
'annotations' => array( 'readonly' => true ),
'mcp' => array( 'public' => true ),
),
)
);
} );
meta.mcp.public is set to true here, deliberately, this is the field that decides
whether an external MCP client can reach this ability at all, and for an ability whose
whole purpose is being triggered by an external agent, that’s the point.
Step 3: The execute_callback, where both SDKs meet
// File: my-plugin/class-summarize-ability.php
function my_plugin_summarize_post_ability( $input ) {
$post = get_post( $input['post_id'] );
if ( ! $post || 'publish' !== $post->post_status ) {
return new WP_Error( 'invalid_post', 'Post not found or not published.' );
}
try {
$result = wp_ai_client_prompt()
->with_text( wp_strip_all_tags( $post->post_content ) )
->using_system_instruction( 'Summarize this WordPress post in three sentences or fewer.' )
->using_temperature( 0.3 )
->using_model_preference( 'claude-sonnet-4-5', 'gemini-3-pro-preview', 'gpt-5.1' )
->generate_text();
if ( is_wp_error( $result ) ) {
return $result;
}
return array( 'summary' => $result );
} catch ( Throwable $e ) {
return new WP_Error( 'summary_failed', $e->getMessage() );
}
}
This is, on purpose, almost identical to Lesson 7’s meta box handler. The permission
check, the error handling, the builder chain, all the same techniques from earlier in
this course. The only real difference is who’s calling it: a button click inside
wp-admin in Lesson 7, an external MCP agent here. The execute_callback itself doesn’t
need to know or care which.
Step 4: Keep the two permission models straight
There are two entirely separate gates here, and it’s worth being precise about which is which:
permission_callbackdecides whether the calling user (whoever the MCP Adapter has authenticated as, on behalf of the external agent) is allowed to trigger this ability for this specific post. This is a WordPress capability check, same as any other ability.- The configured AI provider decides whether the
execute_callback’s internal call towp_ai_client_prompt()can succeed at all. This has nothing to do with the calling agent’s permissions, it’s whether your site has a working provider configured, exactly as covered in Lessons 1 and 5.
An external agent with full permission to trigger this ability still gets a WP_Error
back if your site’s provider plugin has no working API key. That’s expected and
correct, permission to ask isn’t the same thing as the site being able to deliver.
Because an ability triggered over MCP can be called repeatedly by an automated agent, not just by a human clicking a button once, it’s worth thinking about volume here differently than in Lesson 7’s admin-only feature. Consider rate-limiting or caching summaries per post (a transient keyed on post ID and content hash works well) so a misbehaving or overly persistent external agent can’t drive unbounded API spend against your configured provider.
Test it: call the ability directly, then confirm it’s MCP-visible
First, confirm the ability itself works, independent of any MCP client:
wp-env run cli wp eval '
$result = my_plugin_summarize_post_ability( array( "post_id" => 1 ) );
var_dump( $result );
'
You should see an array with a summary key holding real generated text, or a
WP_Error if post 1 isn’t published or no provider is configured.
Then, with an MCP server plugin connected (Courses 2 through 4 cover choosing and
setting one up), confirm the ability is actually listed as an available tool from an
external client, an AI assistant like Claude connected over MCP should be able to call
my-plugin/summarize-post with a post ID and get a real summary back, generated by
whichever provider your site has configured, without the assistant’s own model ever
seeing your site’s API key.
Recap
An ability’s execute_callback is ordinary PHP, and there’s no architectural reason it
can’t call out to an LLM using the AI Client SDK while it’s servicing a request that
came in from an external MCP agent. The permission model for triggering the ability
(permission_callback, meta.mcp.public) stays entirely separate from whether the
internal AI call itself succeeds (a configured provider, from Lessons 1 and 5). This
pattern, an external agent’s request triggering your own site’s AI-powered logic
without ever handling your provider credentials directly, is the real payoff of having
built both directions in this track.
Resources & further reading
- PHP AI Client SDK repository, GitHub
- wp_register_ability() function reference, developer.wordpress.org
- Introducing the AI Client in WordPress 7.0, make.wordpress.org