Neither the MCP Adapter nor the PHP AI Client SDK has a rate limiter or a budget system.
There is no setting anywhere that caps how many times an agent can call an ability per
minute, or how much a given integration is allowed to spend on model calls per day. The
PHP AI Client SDK does expose a filter, wp_ai_client_prevent_prompt, that can block an
individual prompt conditionally, but it is explicitly a single gate, not a quota
tracker. This lesson builds the actual counting and enforcement yourself, using
WordPress transients, and wires the result into both a permission_callback and that
filter.
Lesson 5 (permission-gated tools), and a basic idea of how WordPress transients work (set_transient / get_transient with an expiry).
Step 1: What actually exists, and what does not
Without any limiting, a misconfigured workflow, a runaway automation loop, or a compromised credential can call an ability (or trigger model calls through the AI Client SDK) as fast as the connection allows. For anything production-facing, treat rate limiting as a required control, not an optional extra, precisely because nothing enforces it for you by default.
Step 2: A transient-based counter
WordPress transients are a simple, already-available key-value store with a built-in expiry, exactly what’s needed for a windowed counter. This implementation buckets by user ID and a fixed time window:
// File: my-plugin.php
function my_plugin_check_and_increment_rate_limit( int $user_id, string $bucket_name, int $limit, int $window_seconds ): bool {
$window_key = floor( time() / $window_seconds );
$transient_key = "my_plugin_rl_{$bucket_name}_{$user_id}_{$window_key}";
$count = (int) get_transient( $transient_key );
if ( $count >= $limit ) {
return false;
}
set_transient( $transient_key, $count + 1, $window_seconds );
return true;
}
This is a fixed time-bucket counter: it resets cleanly at each window boundary rather than tracking a continuously sliding log of individual timestamps. That’s a reasonable, practical approximation for most agent rate limiting, and far simpler to implement correctly than a true sliding-window log. If you need stricter, smooth enforcement right at a window boundary, you’d store individual call timestamps instead and count how many fall within the trailing window, at the cost of more storage and complexity.
Step 3: Wire it into permission_callback
Check the limit before any execution happens, combined with the role and ownership checks from earlier lessons, not instead of them:
// File: my-plugin.php
add_action( 'wp_abilities_api_init', function () {
wp_register_ability(
'my-plugin/summarize-recent-comments',
array(
// ...label, input_schema...
'permission_callback' => function () {
if ( ! current_user_can( 'my_plugin_summarize_comments' ) ) {
return false;
}
$allowed = my_plugin_check_and_increment_rate_limit(
get_current_user_id(),
'summarize_comments',
20, // 20 calls
60 // per 60-second window
);
if ( ! $allowed ) {
return new WP_Error(
'rate_limited',
'This agent has exceeded its call limit for this action. Try again shortly.',
array( 'status' => 429 )
);
}
return true;
},
'execute_callback' => function ( $input ) {
// summarization logic
},
)
);
} );
Returning a WP_Error with a 429 status gives a well-behaved MCP client a clear,
standard signal to back off, rather than a generic denial indistinguishable from a
permissions failure.
Step 4: Daily cost control using wp_ai_client_prevent_prompt
For workflows where the ability itself triggers a further model call through the PHP AI Client SDK, wire the same counting pattern into the SDK’s own prevention filter, keyed by day instead of minutes, to approximate a daily budget:
// File: my-plugin.php
add_filter( 'wp_ai_client_prevent_prompt', function ( $should_prevent, $request ) {
$today_key = 'my_plugin_ai_cost_' . gmdate( 'Y-m-d' );
$count = (int) get_transient( $today_key );
$daily_limit = 200; // arbitrary example ceiling
if ( $count >= $daily_limit ) {
return true; // prevent this prompt from proceeding
}
set_transient( $today_key, $count + 1, DAY_IN_SECONDS );
return $should_prevent;
}, 10, 2 );
A per-call counter is a reasonable proxy for cost control but is not the same as tracking actual token usage or billed amount. If your model provider’s response includes token or usage data, capture and sum that instead of a flat call count for a more accurate budget signal, the transient storage pattern here works the same either way.
Test it: confirm the limit actually trips
Call the rate-limited ability more than its configured limit in quick succession from your AI client, or directly:
wp-env run cli wp eval '
for ( $i = 0; $i < 25; $i++ ) {
$ability = wp_get_ability( "my-plugin/summarize-recent-comments" );
$result = $ability->execute( array() );
echo $i . ": " . ( is_wp_error( $result ) ? $result->get_error_code() : "ok" ) . "\n";
}
'
You should see ok for the first 20 calls and rate_limited afterward, resetting once
the 60-second window passes.
Recap
Rate limiting and cost control are not features the MCP Adapter or the PHP AI Client
SDK ship. wp_ai_client_prevent_prompt is a real, usable enforcement point, but it only
blocks a prompt when you tell it to, it tracks nothing by itself. A transient-based
counter, bucketed by user and a fixed time window, is a practical, easy-to-reason-about
way to build the actual limiting logic, wired into permission_callback for ability
calls and into wp_ai_client_prevent_prompt for direct model usage. This closes out the
course: authentication, permissions, data protection, and now control over volume and
cost, all built from real, verifiable mechanisms rather than assumed ones.
Resources & further reading
- Transients API reference, developer.wordpress.org
- MCP Adapter repository, GitHub
- PHP AI Client SDK, GitHub