Agent Toolbelt (spec §5). Shows the ability checklist
* (enable/disable, persisted to the plugin option), the audit-log table, and a
* status line. The checklist is the only setting in v1 — defaults are the
* product.
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'Agent_Toolbelt_Admin' ) ) {
class Agent_Toolbelt_Admin
{
const NONCE_ACTION = 'agent_toolbelt_save';
const NONCE_FIELD = 'agent_toolbelt_nonce';
const AUDIT_LIMIT = 20;
/**
* Adds the Settings -> Agent Toolbelt page.
*/
public static function register_menu()
{
add_options_page(
__( 'Agent Toolbelt', 'agent-toolbelt' ),
__( 'Agent Toolbelt', 'agent-toolbelt' ),
'manage_options',
'agent-toolbelt',
array( __CLASS__, 'render_page' )
);
}
/**
* Adds a Settings action link on the Plugins screen, pointing to the
* Settings -> Agent Toolbelt page (plugin_action_links_* filter). Kept
* even while the kill switch is active — the page itself is where the
* owner sees that state.
*
* @param array $atb_links Existing action links (Deactivate, ...).
* @return array
*/
public static function action_links( $atb_links )
{
$atb_settings = ''
. esc_html__( 'Settings', 'agent-toolbelt' ) . '';
array_unshift( $atb_links, $atb_settings );
return $atb_links;
}
/**
* Renders the admin page: status line, ability checklist, audit table.
*/
public static function render_page()
{
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$atb_saved = self::maybe_save();
echo '
' . esc_html__( 'This site does not expose the WordPress Abilities API (requires WordPress 6.9+). The operations still work via WP-CLI.', 'agent-toolbelt' ) . '
';
}
if ( $atb_kill_switch_active ) {
echo '
' . esc_html__( 'The kill switch is on: no abilities are registered and agents cannot call any operation.', 'agent-toolbelt' ) . '
';
}
// Accuracy note (2026-07, v1.2): core ships the Abilities API and
// its REST endpoints; MCP exposure comes from the official MCP
// Adapter plugin (GitHub releases; wp.org release in preparation),
// whose default server lists abilities flagged meta.mcp.public —
// which the toolbelt sets for every enabled ability.
echo '
' . esc_html__( 'AI agents can call the enabled abilities below through the WordPress REST API (wp-abilities/v1, application-password auth), through the official WordPress MCP Adapter (enabled abilities are published to its default MCP server), or through any other MCP bridge that exposes WordPress Abilities. Only the enabled abilities are visible to them.', 'agent-toolbelt' ) . '
';
}
/**
* Ability checklist with risk badges and one-line descriptions.
*/
protected static function render_checklist()
{
echo '
' . esc_html__( 'WordPress will show the password once — give it to your agent together with your username. You can revoke it any time on your profile page.', 'agent-toolbelt' ) . '
';
} else {
echo '
' . esc_html__( 'Application passwords are not available on this site (WordPress requires HTTPS for them unless the environment is local). Enable HTTPS, then create one under Users → Profile.', 'agent-toolbelt' ) . '
' . esc_html__( 'MCP: the official WordPress MCP Adapter is active — your enabled abilities are published to its default MCP server:', 'agent-toolbelt' ) . ' ';
echo '' . esc_html( rest_url( 'mcp/mcp-adapter-default-server' ) ) . ' ';
echo esc_html__( '(streamable HTTP, same application-password auth; or serve over STDIO with `wp mcp serve`).', 'agent-toolbelt' ) . '
';
} else {
echo '
' . esc_html__( 'MCP: install the official WordPress MCP Adapter plugin to expose the enabled abilities as an MCP server — they are already flagged for it. Any other MCP bridge that reads WordPress Abilities works too.', 'agent-toolbelt' ) . '
' . esc_html__( 'Paste this into your agent\'s instructions (system prompt, CLAUDE.md, or similar):', 'agent-toolbelt' ) . '
';
echo ' ';
echo '';
echo '' . esc_html__( 'Copied.', 'agent-toolbelt' ) . '';
echo '';
}
/**
* The agent-facing briefing with the site's real URLs. Deliberately
* plain text: owners paste it into an agent's instruction file.
*
* @return string
*/
protected static function agent_briefing_text()
{
$atb_lines = array(
sprintf( 'You can operate the WordPress site %s through Agent Toolbelt (guarded maintenance abilities).', home_url( '/' ) ),
'',
'Discovery: GET ' . rest_url( 'wp-abilities/v1/abilities' ) . ' lists every ability you may use; abilities named agent-toolbelt/* are the guarded operations. Only what the site owner enabled is visible.',
'',
'Rules:',
'1. Call agent-toolbelt/site-status first: versions, pending updates, enabled abilities, and whether observe mode is on.',
'2. Preview before changing: every mutating ability accepts "dry_run": true. High-risk abilities REQUIRE a dry-run first — it returns a one-time confirm token you must send back to execute the same input.',
'3. Refusals are answers, not obstacles. {"details":{"refused":true,"reason":"..."}} tells you why: observe_mode (site is read-only for agents), rate_budget (wait retry_after_s seconds), confirm_token (redo the dry-run). Report refusals to the owner; never retry blindly.',
'4. Everything you do lands in an audit log the owner reads. agent-toolbelt/read-audit-log shows it to you too — use it to explain your past actions.',
'',
'REST calls: read-only abilities use GET (' . rest_url( 'wp-abilities/v1/abilities/agent-toolbelt/site-status/run' ) . '); pass GET input as PHP-array query params, e.g. ?input[limit]=3 — bare query args like ?limit=3 are silently ignored. Mutating abilities use POST with {"input":{...}} (' . rest_url( 'wp-abilities/v1/abilities/agent-toolbelt/purge-all-caches/run' ) . '). Authenticate with the application password (HTTP Basic auth).',
);
if ( self::mcp_adapter_active() ) {
$atb_lines[] = '';
$atb_lines[] = 'MCP: the same abilities are exposed on the default MCP server at ' . rest_url( 'mcp/mcp-adapter-default-server' ) . ' (streamable HTTP; send the Mcp-Session-Id header from initialize on every later call).';
}
return implode( "\n", $atb_lines );
}
/**
* Whether the official MCP Adapter actually BOOTS on this site.
* `class_exists( McpAdapter )` is NOT the signal: WooCommerce 10.9+
* bundles the adapter as a Composer library, so the class is loadable
* on any Woo site while the adapter never runs (Woo only boots it on
* rest_api_init when its `mcp_integration` feature flag is on — and
* the adapter can't be probed via did_action() here, because admin
* requests never fire rest_api_init).
*
* @return bool
*/
protected static function mcp_adapter_active()
{
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( is_plugin_active( 'mcp-adapter/mcp-adapter.php' ) ) {
return true;
}
if ( class_exists( '\\Automattic\\WooCommerce\\Utilities\\FeaturesUtil' )
&& is_callable( array( '\\Automattic\\WooCommerce\\Utilities\\FeaturesUtil', 'feature_is_enabled' ) )
&& \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'mcp_integration' ) ) {
return true;
}
return false;
}
/**
* Renders a small colored risk badge.
*
* @param string $atb_risk
* @return string Escaped HTML.
*/
protected static function risk_badge( $atb_risk )
{
$atb_colors = array(
'none' => '#3858e9',
'low' => '#00a32a',
'medium' => '#dba617',
'high' => '#d63638',
);
$atb_color = isset( $atb_colors[ $atb_risk ] ) ? $atb_colors[ $atb_risk ] : '#787c82';
return '' . esc_html( $atb_risk ) . '';
}
/**
* Audit-log table (newest first, spec §4/§5).
*/
protected static function render_audit_log()
{
echo '